Compare commits

...

28 Commits
2.0.4 ... 2.0.8

Author SHA1 Message Date
Michael Kaufmann
090cfc26f2 set file-log (if enabled) to be in froxlor/logs/ folder; fix ssl param directive for dovecot in Ubuntu Bionic; set version to 2.0.8
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-14 13:09:42 +01:00
Michael Kaufmann
529890b5d2 fix typo in langauge-definition
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-12 22:27:45 +01:00
Michael Kaufmann
d4a6ab146d re-add total-disspace dashboard-display on customer dashboard
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-12 16:52:14 +01:00
Michael Kaufmann
e3f02879cf restore mandatory field on domain-formfields; add translated require message and correct error-placement of the message
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-12 15:16:42 +01:00
Michael Kaufmann
b52d6df777 [UI] change require of ipandport field in domains.add and domains.delete to one-of instead of all; fixes #1078
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-12 14:53:05 +01:00
Michael Kaufmann
9e671100ae acme-challenge path adjustments if docroot changed after update from 0.10.x (via apt)
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-12 14:21:14 +01:00
Michael Kaufmann
7e801ea502 Merge branch 'main' of github.com:Froxlor/Froxlor 2023-01-12 12:20:23 +01:00
Daniel
b68522f7d5 Fix formfield image preview path (#1077) 2023-01-12 12:19:31 +01:00
Michael Kaufmann
86852942e0 add missing language-strings for traffic page
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-12 11:30:52 +01:00
Michael Kaufmann
ec05c84f4d check whether let's encrypt is enabled at all and correct acme-alias configuration file if necessary/selected
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-12 09:40:35 +01:00
Michael Kaufmann
9e13c077e9 show command to regenerate cron.d-file if previous deletion of old files could not be done automatically, fixes #1076
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-12 08:33:19 +01:00
Maurice Preuß (envoyr)
da8d315e77 remove hardcoded logo height
Signed-off-by: Maurice Preuß (envoyr) <envoyr@froxlor.org>
2023-01-11 22:43:00 +01:00
Michael Kaufmann
cb67e3ae63 continue checking domains even if no config was found, thx knox
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-11 21:07:00 +01:00
Michael Kaufmann
82d15c4dc2 fixes for ValidateAcmeWebroot command
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-11 20:47:07 +01:00
Michael Kaufmann
6d048e2cee fix default mysql-dbserver for customers if not allowed to use the default (id=0) one; fixes #1075
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-11 19:41:24 +01:00
Michael Kaufmann
87bd80eea1 reenable access to ftp view for customers with ftps=0 because the main account is always being created
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-11 14:58:18 +01:00
Michael Kaufmann
80e442e396 set version to 2.0.7
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-10 22:15:57 +01:00
Michael Kaufmann
489ad375bd ensure latest userdata.inc.php layout for updaters/users of old format
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-10 16:54:20 +01:00
Michael Kaufmann
c420196e73 check explicitly for template existence and try to use default theme as fallback; fixes #1071
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-10 16:53:36 +01:00
Michael Kaufmann
cc6d8d5f8b fix login if non-standard ports are used for froxlor vhost
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-10 12:43:04 +01:00
Michael Kaufmann
24f47bc58b set version to 2.0.6
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-09 10:09:15 +01:00
Michael Kaufmann
c769c074e0 add Google CA to available acme.sh providers; fixes #1065
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-09 10:00:08 +01:00
dependabot[bot]
2ecb8eb034 Bump json5 from 1.0.1 to 1.0.2 (#1069)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 09:51:52 +01:00
Michael Kaufmann
6827c100c3 fix updating email account password-hashes in updater
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-09 09:50:51 +01:00
Michael Kaufmann
c402acd1bd disable correct mod_php in bionic-config-templates when fcgid/php-fpm is selected
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-09 09:25:29 +01:00
Michael Kaufmann
c4ec2509fa fix resetting of isemaildomain-flag of subdomains when nothing changed; fixes #1067
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-09 09:24:22 +01:00
Michael Kaufmann
0f382586ce set version to 2.0.5
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-08 23:24:43 +01:00
Michael Kaufmann
9c2f12ecb1 mysql-remote-server fixes
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-01-08 23:20:31 +01:00
42 changed files with 531 additions and 161 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ install/update.log
install/*.json install/*.json
lib/userdata.inc.php lib/userdata.inc.php
lib/userdata.inc.php.bak lib/userdata.inc.php.bak
lib/config.inc.php
logs/* logs/*
!logs/index.html !logs/index.html
.buildpath .buildpath

View File

@@ -180,7 +180,9 @@ return [
'letsencrypt' => 'Let\'s Encrypt (Live)', 'letsencrypt' => 'Let\'s Encrypt (Live)',
'buypass_test' => 'Buypass (Test / Staging)', 'buypass_test' => 'Buypass (Test / Staging)',
'buypass' => 'Buypass (Live)', 'buypass' => 'Buypass (Live)',
'zerossl' => 'ZeroSSL (Live)' 'zerossl' => 'ZeroSSL (Live)',
'google' => 'Google (Live)',
'google_test' => 'Google (Test / Staging)',
], ],
'save_method' => 'storeSettingField' 'save_method' => 'storeSettingField'
], ],

View File

@@ -43,12 +43,6 @@ use PHPMailer\PHPMailer\PHPMailer;
const AREA = 'admin'; const AREA = 'admin';
require __DIR__ . '/lib/init.php'; require __DIR__ . '/lib/init.php';
// get sql-root access data
Database::needRoot(true);
Database::needSqlData();
$sql_root = Database::getSqlData();
Database::needRoot(false);
if ($page == 'overview' && $userinfo['change_serversettings'] == '1') { if ($page == 'overview' && $userinfo['change_serversettings'] == '1') {
$settings_data = PhpHelper::loadConfigArrayDir('./actions/admin/settings/'); $settings_data = PhpHelper::loadConfigArrayDir('./actions/admin/settings/');
Settings::loadSettingsInto($settings_data); Settings::loadSettingsInto($settings_data);

View File

@@ -35,6 +35,7 @@ use Froxlor\Cli\UpdateCommand;
use Froxlor\Cli\InstallCommand; use Froxlor\Cli\InstallCommand;
use Froxlor\Cli\MasterCron; use Froxlor\Cli\MasterCron;
use Froxlor\Cli\UserCommand; use Froxlor\Cli\UserCommand;
use Froxlor\Cli\ValidateAcmeWebroot;
use Froxlor\Froxlor; use Froxlor\Froxlor;
// validate correct php version // validate correct php version
@@ -59,4 +60,5 @@ $application->add(new UpdateCommand());
$application->add(new InstallCommand()); $application->add(new InstallCommand());
$application->add(new MasterCron()); $application->add(new MasterCron());
$application->add(new UserCommand()); $application->add(new UserCommand());
$application->add(new ValidateAcmeWebroot());
$application->run(); $application->run();

View File

@@ -59,7 +59,6 @@ if ($page == 'overview' || $page == 'domains') {
$domain_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.domains.php'; $domain_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.domains.php';
$collection = (new Collection(SubDomains::class, $userinfo)) $collection = (new Collection(SubDomains::class, $userinfo))
->withPagination($domain_list_data['domain_list']['columns'], $domain_list_data['domain_list']['default_sorting']); ->withPagination($domain_list_data['domain_list']['columns'], $domain_list_data['domain_list']['default_sorting']);
$parentDomainCollection = (new Collection(SubDomains::class, $userinfo, ['sql_search' => ['d.parentdomainid' => 0]]));
} catch (Exception $e) { } catch (Exception $e) {
Response::dynamicError($e->getMessage()); Response::dynamicError($e->getMessage());
} }

View File

@@ -40,7 +40,7 @@ use Froxlor\UI\Response;
use Froxlor\CurrentUser; use Froxlor\CurrentUser;
// redirect if this customer page is hidden via settings // redirect if this customer page is hidden via settings
if (Settings::IsInList('panel.customer_hide_options', 'ftp') || $userinfo['ftps'] == 0) { if (Settings::IsInList('panel.customer_hide_options', 'ftp')) {
Response::redirectTo('customer_index.php'); Response::redirectTo('customer_index.php');
} }

View File

@@ -115,10 +115,14 @@ if ($page == 'overview') {
if ($usages) { if ($usages) {
$userinfo['diskspace_bytes_used'] = $usages['webspace'] * 1024; $userinfo['diskspace_bytes_used'] = $usages['webspace'] * 1024;
$userinfo['mailspace_used'] = $usages['mail'] * 1024;
$userinfo['dbspace_used'] = $usages['mysql'] * 1024;
$userinfo['total_bytes_used'] = ($usages['webspace'] + $usages['mail'] + $usages['mysql']) * 1024; $userinfo['total_bytes_used'] = ($usages['webspace'] + $usages['mail'] + $usages['mysql']) * 1024;
} else { } else {
$userinfo['diskspace_bytes_used'] = 0; $userinfo['diskspace_bytes_used'] = 0;
$userinfo['total_bytes_used'] = 0; $userinfo['total_bytes_used'] = 0;
$userinfo['mailspace_used'] = 0;
$userinfo['dbspace_used'] = 0;
} }
UI::twig()->addGlobal('userinfo', $userinfo); UI::twig()->addGlobal('userinfo', $userinfo);

View File

@@ -92,7 +92,7 @@ if ($page == 'overview' || $page == 'mysqls') {
$result = json_decode($json_result, true)['data']; $result = json_decode($json_result, true)['data'];
if (isset($result['databasename']) && $result['databasename'] != '') { if (isset($result['databasename']) && $result['databasename'] != '') {
Database::needRoot(true, $result['dbserver']); Database::needRoot(true, $result['dbserver'], false);
Database::needSqlData(); Database::needSqlData();
$sql_root = Database::getSqlData(); $sql_root = Database::getSqlData();
Database::needRoot(false); Database::needRoot(false);

View File

@@ -225,7 +225,7 @@ CREATE TABLE `panel_customers` (
`allowed_mysqlserver` text NOT NULL, `allowed_mysqlserver` text NOT NULL,
PRIMARY KEY (`customerid`), PRIMARY KEY (`customerid`),
UNIQUE KEY `loginname` (`loginname`) UNIQUE KEY `loginname` (`loginname`)
) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci ROW_FORMAT=DYNAMIC;
DROP TABLE IF EXISTS `panel_databases`; DROP TABLE IF EXISTS `panel_databases`;
@@ -642,7 +642,7 @@ opcache.validate_timestamps'),
('system', 'leprivatekey', 'unset'), ('system', 'leprivatekey', 'unset'),
('system', 'lepublickey', 'unset'), ('system', 'lepublickey', 'unset'),
('system', 'letsencryptca', 'letsencrypt'), ('system', 'letsencryptca', 'letsencrypt'),
('system', 'letsencryptchallengepath', '/var/www/froxlor'), ('system', 'letsencryptchallengepath', '/var/www/html/froxlor'),
('system', 'letsencryptkeysize', '4096'), ('system', 'letsencryptkeysize', '4096'),
('system', 'letsencryptreuseold', 0), ('system', 'letsencryptreuseold', 0),
('system', 'leenabled', '0'), ('system', 'leenabled', '0'),
@@ -696,7 +696,7 @@ opcache.validate_timestamps'),
('system', 'distribution', ''), ('system', 'distribution', ''),
('system', 'update_channel', 'stable'), ('system', 'update_channel', 'stable'),
('system', 'updatecheck_data', ''), ('system', 'updatecheck_data', ''),
('system', 'update_notify_last', '2.0.4'), ('system', 'update_notify_last', '2.0.8'),
('system', 'traffictool', 'goaccess'), ('system', 'traffictool', 'goaccess'),
('api', 'enabled', '0'), ('api', 'enabled', '0'),
('2fa', 'enabled', '1'), ('2fa', 'enabled', '1'),
@@ -740,8 +740,8 @@ opcache.validate_timestamps'),
('panel', 'logo_overridetheme', '0'), ('panel', 'logo_overridetheme', '0'),
('panel', 'logo_overridecustom', '0'), ('panel', 'logo_overridecustom', '0'),
('panel', 'settings_mode', '0'), ('panel', 'settings_mode', '0'),
('panel', 'version', '2.0.4'), ('panel', 'version', '2.0.8'),
('panel', 'db_version', '202212060'); ('panel', 'db_version', '202301120');
DROP TABLE IF EXISTS `panel_tasks`; DROP TABLE IF EXISTS `panel_tasks`;

View File

@@ -38,10 +38,9 @@ if (!defined('_CRON_UPDATE')) {
// last 0.10.x release // last 0.10.x release
if (Froxlor::isFroxlorVersion('0.10.38.3')) { if (Froxlor::isFroxlorVersion('0.10.38.3')) {
$update_to = '2.0.0-beta1'; $update_to = '2.0.0-beta1';
Update::showUpdateStep("Updating from 0.10.38.3 to ".$update_to, false); Update::showUpdateStep("Updating from 0.10.38.3 to " . $update_to, false);
Update::showUpdateStep("Removing unused table"); Update::showUpdateStep("Removing unused table");
Database::query("DROP TABLE IF EXISTS `panel_sessions`;"); Database::query("DROP TABLE IF EXISTS `panel_sessions`;");
@@ -66,8 +65,8 @@ if (Froxlor::isFroxlorVersion('0.10.38.3')) {
KEY customerid (customerid) KEY customerid (customerid)
) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci;"; ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci;";
Database::query($sql); Database::query($sql);
Database::query("SET SESSION innodb_strict_mode=OFF;");
// new customer allowed_mysqlserver field // new customer allowed_mysqlserver field
Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` ROW_FORMAT=DYNAMIC;");
Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` CHANGE COLUMN `customernumber` `customernumber` varchar(100) NOT NULL default '';"); Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` CHANGE COLUMN `customernumber` `customernumber` varchar(100) NOT NULL default '';");
Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` CHANGE COLUMN `allowed_phpconfigs` `allowed_phpconfigs` text NOT NULL;"); Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` CHANGE COLUMN `allowed_phpconfigs` `allowed_phpconfigs` text NOT NULL;");
Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` ADD `allowed_mysqlserver` text NOT NULL;"); Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` ADD `allowed_mysqlserver` text NOT NULL;");
@@ -146,7 +145,7 @@ if (Froxlor::isFroxlorVersion('0.10.38.3')) {
} }
Update::showUpdateStep("Adding new settings"); Update::showUpdateStep("Adding new settings");
$panel_settings_mode = isset($_POST['panel_settings_mode']) ? (int) $_POST['panel_settings_mode'] : 0; $panel_settings_mode = isset($_POST['panel_settings_mode']) ? (int)$_POST['panel_settings_mode'] : 0;
Settings::AddNew("panel.settings_mode", $panel_settings_mode); Settings::AddNew("panel.settings_mode", $panel_settings_mode);
$system_distribution = isset($_POST['system_distribution']) ? $_POST['system_distribution'] : ''; $system_distribution = isset($_POST['system_distribution']) ? $_POST['system_distribution'] : '';
Settings::AddNew("system.distribution", $system_distribution); Settings::AddNew("system.distribution", $system_distribution);
@@ -183,17 +182,16 @@ if (Froxlor::isFroxlorVersion('0.10.38.3')) {
Update::lastStepStatus(0); Update::lastStepStatus(0);
Update::showUpdateStep("Updating email account password-hashes"); Update::showUpdateStep("Updating email account password-hashes");
Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password` = REPLACE(`password`, '$1$', '{MD5-CRYPT}$1$') WHERE SUBSTRING(`password`, 1, 3) = '$1$'"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$1$', '{MD5-CRYPT}$1$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$1$'");
Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password` = REPLACE(`password`, '$5$', '{SHA256-CRYPT}$5$') WHERE SUBSTRING(`password`, 1, 3) = '$5$'"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$5$', '{SHA256-CRYPT}$5$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$5$'");
Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password` = REPLACE(`password`, '$6$', '{SHA512-CRYPT}$6$') WHERE SUBSTRING(`password`, 1, 3) = '$6$'"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$6$', '{SHA512-CRYPT}$6$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$6$'");
Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password` = REPLACE(`password`, '$2y$', '{BLF-CRYPT}$2y$') WHERE SUBSTRING(`password`, 1, 4) = '$2y$'"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$2y$', '{BLF-CRYPT}$2y$') WHERE SUBSTRING(`password_enc`, 1, 4) = '$2y$'");
Update::lastStepStatus(0); Update::lastStepStatus(0);
Froxlor::updateToVersion($update_to); Froxlor::updateToVersion($update_to);
} }
if (Froxlor::isDatabaseVersion('202112310')) { if (Froxlor::isDatabaseVersion('202112310')) {
Update::showUpdateStep("Adjusting traffic tool settings"); Update::showUpdateStep("Adjusting traffic tool settings");
$traffic_tool = Settings::Get('system.awstats_enabled') == 1 ? 'awstats' : 'webalizer'; $traffic_tool = Settings::Get('system.awstats_enabled') == 1 ? 'awstats' : 'webalizer';
Settings::AddNew("system.traffictool", $traffic_tool); Settings::AddNew("system.traffictool", $traffic_tool);
@@ -204,11 +202,16 @@ if (Froxlor::isDatabaseVersion('202112310')) {
} }
if (Froxlor::isDatabaseVersion('202211030')) { if (Froxlor::isDatabaseVersion('202211030')) {
Update::showUpdateStep("Creating backward compatibility for cronjob"); Update::showUpdateStep("Creating backward compatibility for cronjob");
$disabled = explode(',', ini_get('disable_functions'));
$exec_allowed = !in_array('exec', $disabled);
// check whether old files could be deleted in previous updates and if not,
// user should run cron to regenerate cron.d-file manually as he will run
// the other commands manually only after the update so this file would be deleted too
if ($exec_allowed) {
$complete_filedir = Froxlor::getInstallDir() . '/scripts'; $complete_filedir = Froxlor::getInstallDir() . '/scripts';
mkdir($complete_filedir, 0750, true); mkdir($complete_filedir, 0750, true);
$newCronBin = Froxlor::getInstallDir().'/bin/froxlor-cli'; $newCronBin = Froxlor::getInstallDir() . '/bin/froxlor-cli';
$compCron = <<<EOF $compCron = <<<EOF
<?php <?php
chmod('$newCronBin', 0755); chmod('$newCronBin', 0755);
@@ -216,8 +219,13 @@ chmod('$newCronBin', 0755);
exec('$newCronBin froxlor:cron -r 99'); exec('$newCronBin froxlor:cron -r 99');
exit; exit;
EOF; EOF;
file_put_contents($complete_filedir.'/froxlor_master_cronjob.php', $compCron); file_put_contents($complete_filedir . '/froxlor_master_cronjob.php', $compCron);
Update::lastStepStatus(0); Update::lastStepStatus(0);
} else {
$cron_run_cmd = 'chmod +x ' . FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/bin/froxlor-cli') . PHO_EOL;
$cron_run_cmd .= FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/bin/froxlor-cli') . ' froxlor:cron -r 99';
Update::lastStepStatus(1, 'manual commands needed', 'Please run the following commands manually:<br><pre>' . $cron_run_cmd . '</pre>');
}
Froxlor::updateToDbVersion('202212060'); Froxlor::updateToDbVersion('202212060');
} }
@@ -257,8 +265,11 @@ if (Froxlor::isFroxlorVersion('2.0.3')) {
$complete_filedir = Froxlor::getInstallDir() . '/scripts'; $complete_filedir = Froxlor::getInstallDir() . '/scripts';
// check if compat. cronjob still exists (most likely didn't run successfully b/c of error from former 2.0 release) // check if compat. cronjob still exists (most likely didn't run successfully b/c of error from former 2.0 release)
if (@file_exists($complete_filedir.'/froxlor_master_cronjob.php')) { if (@file_exists($complete_filedir . '/froxlor_master_cronjob.php')) {
Update::showUpdateStep("Adjusting backward compatibility for cronjob"); Update::showUpdateStep("Adjusting backward compatibility for cronjob");
$disabled = explode(',', ini_get('disable_functions'));
$exec_allowed = !in_array('exec', $disabled);
if ($exec_allowed) {
$newCronBin = Froxlor::getInstallDir() . '/bin/froxlor-cli'; $newCronBin = Froxlor::getInstallDir() . '/bin/froxlor-cli';
$compCron = <<<EOF $compCron = <<<EOF
<?php <?php
@@ -269,7 +280,67 @@ exit;
EOF; EOF;
file_put_contents($complete_filedir . '/froxlor_master_cronjob.php', $compCron); file_put_contents($complete_filedir . '/froxlor_master_cronjob.php', $compCron);
Update::lastStepStatus(0); Update::lastStepStatus(0);
} else {
$cron_run_cmd = 'chmod +x ' . FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/bin/froxlor-cli') . PHO_EOL;
$cron_run_cmd .= FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/bin/froxlor-cli') . ' froxlor:cron -r 99';
Update::lastStepStatus(1, 'manual commands needed', 'Please run the following commands manually:<br><pre>' . $cron_run_cmd . '</pre>');
}
} }
Froxlor::updateToVersion('2.0.4'); Froxlor::updateToVersion('2.0.4');
} }
if (Froxlor::isFroxlorVersion('2.0.4')) {
Update::showUpdateStep("Updating from 2.0.4 to 2.0.5", false);
Froxlor::updateToVersion('2.0.5');
}
if (Froxlor::isFroxlorVersion('2.0.5')) {
Update::showUpdateStep("Updating from 2.0.5 to 2.0.6", false);
Update::showUpdateStep("Updating possible missing email account password-hashes");
Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$1$', '{MD5-CRYPT}$1$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$1$'");
Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$5$', '{SHA256-CRYPT}$5$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$5$'");
Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$6$', '{SHA512-CRYPT}$6$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$6$'");
Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$2y$', '{BLF-CRYPT}$2y$') WHERE SUBSTRING(`password_enc`, 1, 4) = '$2y$'");
Update::lastStepStatus(0);
Froxlor::updateToVersion('2.0.6');
}
if (Froxlor::isFroxlorVersion('2.0.6')) {
Update::showUpdateStep("Updating from 2.0.6 to 2.0.7", false);
Update::showUpdateStep("Correcting allowed_mysqlserver for customers");
Database::query("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `allowed_mysqlserver` = '[0]' WHERE `allowed_mysqlserver` = ''");
Update::lastStepStatus(0);
Froxlor::updateToVersion('2.0.7');
}
if (Froxlor::isDatabaseVersion('202212060')) {
Update::showUpdateStep("Validating acme.sh challenge path");
$acmesh_challenge_dir = Settings::Get('system.letsencryptchallengepath');
$system_letsencryptchallengepath_upd = isset($_POST['system_letsencryptchallengepath_upd']) ? $_POST['system_letsencryptchallengepath_upd'] : $acmesh_challenge_dir;
if ($acmesh_challenge_dir != $system_letsencryptchallengepath_upd) {
Settings::Set('system.letsencryptchallengepath', $system_letsencryptchallengepath_upd);
Update::lastStepStatus(1, 'manual commands needed', 'Please reconfigure webserver service using <pre>bin/froxlor-cli froxlor:config-services</pre> or adjust the path manually in <pre>' . Settings::Get('system.letsencryptacmeconf') . '</pre>');
} else {
Update::lastStepStatus(0);
}
Froxlor::updateToDbVersion('202301120');
}
if (Froxlor::isFroxlorVersion('2.0.7')) {
Update::showUpdateStep("Updating from 2.0.7 to 2.0.8", false);
// adjust file-logging to be set to froxlor/logs/
$logtypes = explode(',', Settings::Get('logger.logtypes'));
if (in_array('file', $logtypes)) {
Update::showUpdateStep("Adjusting froxlor logfile for system-logging to be stored in logs/froxlor.log");
Settings::Set('logger.logfile', 'froxlor.log');
Update::lastStepStatus(0);
}
Froxlor::updateToVersion('2.0.8');
}

View File

@@ -34,9 +34,14 @@ $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).';
$return['system_le_domain_dnscheck_note'] = ['type' => 'infotext', 'value' => $description];
$question = '<strong>Validate DNS of domains when using Lets Encrypt&nbsp;'; $question = '<strong>Validate DNS of domains when using Lets Encrypt&nbsp;';
$return['system_le_domain_dnscheck'] = ['type' => 'checkbox', 'value' => 1, 'checked' => 1, 'label' => $question]; $return['system_le_domain_dnscheck'] = [
'type' => 'checkbox',
'value' => 1,
'checked' => 1,
'label' => $question,
'prior_infotext' => $description
];
} }
$preconfig['fields'] = $return; $preconfig['fields'] = $return;

View File

@@ -27,6 +27,7 @@ use Froxlor\Froxlor;
use Froxlor\FileDir; use Froxlor\FileDir;
use Froxlor\Config\ConfigParser; use Froxlor\Config\ConfigParser;
use Froxlor\Install\Update; use Froxlor\Install\Update;
use Froxlor\Settings;
$preconfig = [ $preconfig = [
'title' => '2.x updates', 'title' => '2.x updates',
@@ -36,7 +37,6 @@ $return = [];
if (Update::versionInUpdate($current_version, '2.0.0-beta1')) { if (Update::versionInUpdate($current_version, '2.0.0-beta1')) {
$description = 'We have rearranged the settings and split them into basic and advanced categories. This makes it easier for users who do not need all the detailed or very specific settings and options and gives a better overview of the basic/mostly used settings.'; $description = 'We have rearranged the settings and split them into basic and advanced categories. This makes it easier for users who do not need all the detailed or very specific settings and options and gives a better overview of the basic/mostly used settings.';
$return['panel_settings_mode_note'] = ['type' => 'infotext', 'value' => $description];
$question = '<strong>Chose settings mode (you can change that at any time)</strong>'; $question = '<strong>Chose settings mode (you can change that at any time)</strong>';
$return['panel_settings_mode'] = [ $return['panel_settings_mode'] = [
'type' => 'select', 'type' => 'select',
@@ -45,11 +45,11 @@ if (Update::versionInUpdate($current_version, '2.0.0-beta1')) {
1 => 'Advanced' 1 => 'Advanced'
], ],
'selected' => 1, 'selected' => 1,
'label' => $question 'label' => $question,
'prior_infotext' => $description
]; ];
$description = 'The configuration page now can preselect a distribution, please select your current distribution'; $description = 'The configuration page now can preselect a distribution, please select your current distribution';
$return['system_distribution_note'] = ['type' => 'infotext', 'value' => $description];
$question = '<strong>Select distribution</strong>'; $question = '<strong>Select distribution</strong>';
$config_dir = FileDir::makeCorrectDir(Froxlor::getInstallDir() . '/lib/configfiles/'); $config_dir = FileDir::makeCorrectDir(Froxlor::getInstallDir() . '/lib/configfiles/');
// show list of available distro's // show list of available distro's
@@ -68,9 +68,26 @@ if (Update::versionInUpdate($current_version, '2.0.0-beta1')) {
'type' => 'select', 'type' => 'select',
'select_var' => $distributions_select, 'select_var' => $distributions_select,
'selected' => '', 'selected' => '',
'label' => $question 'label' => $question,
'prior_infotext' => $description
]; ];
} }
if (Update::versionInUpdate($current_db_version, '202301120')) {
$acmesh_challenge_dir = Settings::Get('system.letsencryptchallengepath');
if ($acmesh_challenge_dir != Froxlor::getInstallDir()) {
$has_preconfig = true;
$description = 'ACME challenge docroot from settings differs from the current installation directory.';
$question = '<strong>Validate Let\'s Encrypt challenge path&nbsp;';
$return['system_letsencryptchallengepath_upd'] = [
'type' => 'text',
'value' => $acmesh_challenge_dir,
'placeholder' => Froxlor::getInstallDir(),
'label' => $question,
'prior_infotext' => $description
];
}
}
$preconfig['fields'] = $return; $preconfig['fields'] = $return;
return $preconfig; return $preconfig;

View File

@@ -73,12 +73,12 @@ class Mysqls extends ApiCommand implements ResourceEntity
$password = $this->getParam('mysql_password'); $password = $this->getParam('mysql_password');
// parameters // parameters
$dbserver = $this->getParam('mysql_server', true, 0);
$databasedescription = $this->getParam('description', true, ''); $databasedescription = $this->getParam('description', true, '');
$databasename = $this->getParam('custom_suffix', true, ''); $databasename = $this->getParam('custom_suffix', true, '');
$sendinfomail = $this->getBoolParam('sendinfomail', true, 0); $sendinfomail = $this->getBoolParam('sendinfomail', true, 0);
// get needed customer info to reduce the mysql-usage-counter by one // get needed customer info to reduce the mysql-usage-counter by one
$customer = $this->getCustomerData('mysqls'); $customer = $this->getCustomerData('mysqls');
$dbserver = $this->getParam('mysql_server', true, $this->getDefaultMySqlServer($customer));
// validation // validation
$password = Validate::validate($password, 'password', '', '', [], true); $password = Validate::validate($password, 'password', '', '', [], true);
@@ -90,7 +90,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
// validate whether the dbserver exists // validate whether the dbserver exists
$dbserver = Validate::validate($dbserver, html_entity_decode(lng('mysql.mysql_server')), '/^[0-9]+$/', '', 0, true); $dbserver = Validate::validate($dbserver, html_entity_decode(lng('mysql.mysql_server')), '/^[0-9]+$/', '', 0, true);
Database::needRoot(true, $dbserver); Database::needRoot(true, $dbserver, false);
Database::needSqlData(); Database::needSqlData();
$sql_root = Database::getSqlData(); $sql_root = Database::getSqlData();
Database::needRoot(false); Database::needRoot(false);
@@ -150,7 +150,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
$pma = Settings::Get('panel.phpmyadmin_url'); $pma = Settings::Get('panel.phpmyadmin_url');
} }
Database::needRoot(true, $dbserver); Database::needRoot(true, $dbserver, false);
Database::needSqlData(); Database::needSqlData();
$sql_root = Database::getSqlData(); $sql_root = Database::getSqlData();
Database::needRoot(false); Database::needRoot(false);
@@ -287,7 +287,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
} }
$result = Database::pexecute_first($result_stmt, $params, true, true); $result = Database::pexecute_first($result_stmt, $params, true, true);
if ($result) { if ($result) {
Database::needRoot(true, $result['dbserver']); Database::needRoot(true, $result['dbserver'], false);
$mbdata_stmt = Database::prepare(" $mbdata_stmt = Database::prepare("
SELECT SUM(data_length + index_length) as MB FROM information_schema.TABLES SELECT SUM(data_length + index_length) as MB FROM information_schema.TABLES
WHERE table_schema = :table_schema WHERE table_schema = :table_schema
@@ -364,7 +364,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
} }
// Begin root-session // Begin root-session
Database::needRoot(true, $result['dbserver']); Database::needRoot(true, $result['dbserver'], false);
$dbmgr = new DbManager($this->logger()); $dbmgr = 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) {
$dbmgr->getManager()->grantPrivilegesTo($result['databasename'], $password, $mysql_access_host, false, true); $dbmgr->getManager()->grantPrivilegesTo($result['databasename'], $password, $mysql_access_host, false, true);
@@ -449,7 +449,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
'dbserver' => $_dbserver['dbserver'] 'dbserver' => $_dbserver['dbserver']
], $query_fields), true, true); ], $query_fields), true, true);
// Begin root-session // Begin root-session
Database::needRoot(true, $_dbserver['dbserver']); Database::needRoot(true, $_dbserver['dbserver'], false);
while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) {
$mbdata_stmt = Database::prepare(" $mbdata_stmt = Database::prepare("
SELECT SUM(data_length + index_length) as MB FROM information_schema.TABLES SELECT SUM(data_length + index_length) as MB FROM information_schema.TABLES
@@ -536,7 +536,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
$id = $result['id']; $id = $result['id'];
// Begin root-session // Begin root-session
Database::needRoot(true, $result['dbserver']); 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']);
Database::needRoot(false); Database::needRoot(false);
@@ -558,4 +558,13 @@ class Mysqls extends ApiCommand implements ResourceEntity
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted database '" . $result['databasename'] . "'"); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted database '" . $result['databasename'] . "'");
return $this->response($result); return $this->response($result);
} }
private function getDefaultMySqlServer(array $customer) {
$allowed_mysqlservers = json_decode($customer['allowed_mysqlserver'] ?? '[]', true);
asort($allowed_mysqlservers, SORT_NUMERIC);
if (count($allowed_mysqlservers) == 1 && $allowed_mysqlservers[0] != 0) {
return (int) $allowed_mysqlservers[0];
}
return (int) array_shift($allowed_mysqlservers);
}
} }

View File

@@ -701,11 +701,13 @@ class SubDomains extends ApiCommand implements ResourceEntity
$wwwserveralias = ($selectserveralias == '1') ? '1' : '0'; $wwwserveralias = ($selectserveralias == '1') ? '1' : '0';
// if allowed, check for 'is email domain'-flag // if allowed, check for 'is email domain'-flag
if ($result['parentdomainid'] != '0' && ($result['subcanemaildomain'] == '1' || $result['subcanemaildomain'] == '2') && $isemaildomain != $result['isemaildomain']) { if ($isemaildomain != $result['isemaildomain']) {
if ($result['parentdomainid'] != '0' && ($result['subcanemaildomain'] == '1' || $result['subcanemaildomain'] == '2')) {
$isemaildomain = intval($isemaildomain); $isemaildomain = intval($isemaildomain);
} elseif ($result['parentdomainid'] != '0') { } elseif ($result['parentdomainid'] != '0') {
$isemaildomain = $result['subcanemaildomain'] == '3' ? 1 : 0; $isemaildomain = $result['subcanemaildomain'] == '3' ? 1 : 0;
} }
}
// check changes of openbasedir-path variable // check changes of openbasedir-path variable
if ($openbasedir_path > 2 && $openbasedir_path < 0) { if ($openbasedir_path > 2 && $openbasedir_path < 0) {

View File

@@ -37,7 +37,7 @@ use Symfony\Component\Console\Output\OutputInterface;
class CliCommand extends Command class CliCommand extends Command
{ {
protected function validateRequirements(InputInterface $input, OutputInterface $output): int protected function validateRequirements(InputInterface $input, OutputInterface $output, bool $ignore_has_updates = false): int
{ {
if (!file_exists(Froxlor::getInstallDir() . '/lib/userdata.inc.php')) { if (!file_exists(Froxlor::getInstallDir() . '/lib/userdata.inc.php')) {
$output->writeln("<error>Could not find froxlor's userdata.inc.php file. You should use this script only with an installed froxlor system.</>"); $output->writeln("<error>Could not find froxlor's userdata.inc.php file. You should use this script only with an installed froxlor system.</>");
@@ -51,7 +51,7 @@ class CliCommand extends Command
$output->writeln("<error>" . $e->getMessage() . "</>"); $output->writeln("<error>" . $e->getMessage() . "</>");
return self::INVALID; return self::INVALID;
} }
if (Froxlor::hasUpdates() || Froxlor::hasDbUpdates()) { if (!$ignore_has_updates && (Froxlor::hasUpdates() || Froxlor::hasDbUpdates())) {
if ((int)Settings::Get('system.cron_allowautoupdate') == 1) { if ((int)Settings::Get('system.cron_allowautoupdate') == 1) {
return $this->runUpdate($output); return $this->runUpdate($output);
} else { } else {

View File

@@ -0,0 +1,154 @@
<?php
/**
* This file is part of the Froxlor project.
* Copyright (c) 2010 the Froxlor Team (see authors).
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you can also view it online at
* https://files.froxlor.org/misc/COPYING.txt
*
* @copyright the authors
* @author Froxlor team <team@froxlor.org>
* @license https://files.froxlor.org/misc/COPYING.txt GPLv2
*/
namespace Froxlor\Cli;
use Froxlor\Cron\TaskId;
use Froxlor\Database\Database;
use Froxlor\FileDir;
use Froxlor\Froxlor;
use Froxlor\Settings;
use Froxlor\System\Cronjob;
use PDO;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
final class ValidateAcmeWebroot extends CliCommand
{
protected function configure()
{
$this->setName('froxlor:validate-acme-webroot');
$this->setDescription('Validates the Le_Webroot value is correct for froxlor managed domains with Let\s Encrypt certificate.');
$this->addOption('yes-to-all', 'A', InputOption::VALUE_NONE, 'Do not ask for confirmation, update files if necessary');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$result = self::SUCCESS;
$result = $this->validateRequirements($input, $output, true);
$io = new SymfonyStyle($input, $output);
if ((int) Settings::Get('system.leenabled') == 0) {
$io->info("Let's Encrypt not activated in froxlor settings.");
$result = self::INVALID;
}
if ($result == self::SUCCESS) {
$yestoall = $input->getOption('yes-to-all') !== false;
$helper = $this->getHelper('question');
$count_changes = 0;
// get all Let's Encrypt enabled domains
$sel_stmt = Database::prepare("SELECT id, domain FROM panel_domains WHERE `letsencrypt` = '1' AND aliasdomain IS NULL ORDER BY id ASC");
Database::pexecute($sel_stmt);
$domains = $sel_stmt->fetchAll(PDO::FETCH_ASSOC);
$upd_stmt = Database::prepare("UPDATE domain_ssl_settings SET expirationdate=NULL WHERE `domainid` = :did");
$acmesh_dir = dirname(Settings::Get('system.acmeshpath'));
$acmesh_challenge_dir = Settings::Get('system.letsencryptchallengepath');
if ($acmesh_challenge_dir != Froxlor::getInstallDir()) {
$io->warning([
"ACME challenge docroot from settings differs from the current installation directory.",
"Settings: '" . $acmesh_challenge_dir . "'",
"Default/recommended value: '" . Froxlor::getInstallDir() . "'",
]);
$question = new ConfirmationQuestion('Fix ACME challenge docroot setting? [yes] ', true, '/^(y|j)/i');
if ($yestoall || $helper->ask($input, $output, $question)) {
Settings::Set('system.letsencryptchallengepath', Froxlor::getInstallDir());
$former_value = $acmesh_challenge_dir;
$acmesh_challenge_dir = Froxlor::getInstallDir();
// need to update the corresponding acme-alias config-file
$acme_alias_file = Settings::Get('system.letsencryptacmeconf');
$sed_params = "s@".$former_value."@" . $acmesh_challenge_dir . "@";
FileDir::safe_exec('sed -i -e "' . $sed_params . '" ' . escapeshellarg($acme_alias_file));
$count_changes++;
}
}
foreach ($domains as $domain_arr) {
$domain = $domain_arr['domain'];
$acme_domain_conf = FileDir::makeCorrectFile($acmesh_dir . '/' . $domain . '/' . $domain . '.conf');
if (file_exists($acme_domain_conf)) {
$io->text("Getting info from " . $acme_domain_conf);
$conf_content = file_get_contents($acme_domain_conf);
} else {
$acme_domain_conf = FileDir::makeCorrectFile($acmesh_dir . '/' . $domain . '_ecc/' . $domain . '.conf');
if (file_exists($acme_domain_conf)) {
$io->text("Getting info from " . $acme_domain_conf);
$conf_content = file_get_contents($acme_domain_conf);
} else {
$io->info("No domain configuration file found in '" . $acmesh_dir . "'");
continue;
}
}
if (!empty($conf_content)) {
$lines = explode("\n", $conf_content);
foreach ($lines as $line) {
$val_key = explode("=", $line);
if ($val_key[0] == 'Le_Webroot') {
$domain_webroot = trim(trim($val_key[1], "'"), '"');
if ($domain_webroot != $acmesh_challenge_dir) {
$io->warning("Domain '" . $domain . "' has old/wrong Le_Webroot setting: '" . $domain_webroot . ' <> ' . $acmesh_challenge_dir . "'");
$question = new ConfirmationQuestion('Fix Le_Webroot? [yes] ', true, '/^(y|j)/i');
if ($yestoall || $helper->ask($input, $output, $question)) {
$sed_params = "s@Le_Webroot=.*@Le_Webroot='" . $acmesh_challenge_dir . "'@";
FileDir::safe_exec('sed -i -e "' . $sed_params . '" ' . escapeshellarg($acme_domain_conf));
Database::pexecute($upd_stmt, ['did' => $domain_arr['id']]);
$io->success("Correction of Le_Webroot successful");
$count_changes++;
} else {
continue;
}
} else {
$io->info("Domain '" . $domain . "' Le_Webroot value is correct");
}
break;
} else {
continue;
}
}
}
}
if ($count_changes > 0) {
if (Froxlor::hasUpdates() || Froxlor::hasDbUpdates()) {
Cronjob::inserttask(TaskId::REBUILD_VHOST);
} else {
$question = new ConfirmationQuestion('Changes detected. Force cronjob to refresh certificates? [yes] ', true, '/^(y|j)/i');
if ($yestoall || $helper->ask($input, $output, $question)) {
passthru(FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/bin/froxlor-cli') . ' froxlor:cron -f -d');
}
}
}
}
return $result;
}
}

View File

@@ -46,7 +46,9 @@ class AcmeSh extends FroxlorCron
'letsencrypt_test' => "https://acme-staging-v02.api.letsencrypt.org/directory", 'letsencrypt_test' => "https://acme-staging-v02.api.letsencrypt.org/directory",
'buypass' => "https://api.buypass.com/acme/directory", 'buypass' => "https://api.buypass.com/acme/directory",
'buypass_test' => "https://api.test4.buypass.no/acme/directory", 'buypass_test' => "https://api.test4.buypass.no/acme/directory",
'zerossl' => "https://acme.zerossl.com/v2/DV90" 'zerossl' => "https://acme.zerossl.com/v2/DV90",
'google' => "https://dv.acme-v02.api.pki.goog/directory",
'google_test' => "https://dv.acme-v02.test-api.pki.goog/directory",
]; ];
public static $no_inserttask = false; public static $no_inserttask = false;
private static $apiserver = ""; private static $apiserver = "";

View File

@@ -146,6 +146,7 @@ class BackupCron extends FroxlorCron
FileDir::safe_exec('mkdir -p ' . escapeshellarg(FileDir::makeCorrectDir($tmpdir . '/mysql'))); FileDir::safe_exec('mkdir -p ' . escapeshellarg(FileDir::makeCorrectDir($tmpdir . '/mysql')));
// get all customer database-names // get all customer database-names
// @fixme respect multiple dbservers
$sel_stmt = Database::prepare("SELECT `databasename` FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :cid"); $sel_stmt = Database::prepare("SELECT `databasename` FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :cid");
Database::pexecute($sel_stmt, [ Database::pexecute($sel_stmt, [
'cid' => $data['customerid'] 'cid' => $data['customerid']

View File

@@ -127,7 +127,7 @@ class TrafficCron extends FroxlorCron
while ($row_database = $databases_stmt->fetch(PDO::FETCH_ASSOC)) { while ($row_database = $databases_stmt->fetch(PDO::FETCH_ASSOC)) {
if ($last_dbserver != $row_database['dbserver']) { if ($last_dbserver != $row_database['dbserver']) {
Database::needRoot(true, $row_database['dbserver']); Database::needRoot(true, $row_database['dbserver'], true);
$last_dbserver = $row_database['dbserver']; $last_dbserver = $row_database['dbserver'];
$databases_list = []; $databases_list = [];

View File

@@ -28,6 +28,7 @@ namespace Froxlor\Database;
use Exception; use Exception;
use Froxlor\FileDir; use Froxlor\FileDir;
use Froxlor\Froxlor; use Froxlor\Froxlor;
use Froxlor\PhpHelper;
use Froxlor\Settings; use Froxlor\Settings;
use Froxlor\UI\Panel\UI; use Froxlor\UI\Panel\UI;
use PDO; use PDO;
@@ -79,6 +80,8 @@ class Database
private static $sqldata = null; private static $sqldata = null;
private static $need_dbname = true;
/** /**
* Wrapper for PDOStatement::execute so we can catch the PDOException * Wrapper for PDOStatement::execute so we can catch the PDOException
* and display the error nicely on the panel - also fetches the * and display the error nicely on the panel - also fetches the
@@ -135,7 +138,7 @@ class Database
require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; require Froxlor::getInstallDir() . "/lib/userdata.inc.php";
// le format // le format
if (isset($sql['root_user']) && isset($sql['root_password']) && !is_array($sql_root)) { if (isset($sql['root_user']) && isset($sql['root_password']) && empty($sql_root)) {
$sql_root = [ $sql_root = [
0 => [ 0 => [
'caption' => 'Default', 'caption' => 'Default',
@@ -145,12 +148,20 @@ class Database
'password' => $sql['root_password'] 'password' => $sql['root_password']
] ]
]; ];
unset($sql['root_user']);
unset($sql['root_password']);
// write new layout so this won't happen again
self::generateNewUserData($sql, $sql_root);
// re-read file
require Froxlor::getInstallDir() . "/lib/userdata.inc.php";
} }
$substitutions = [ $substitutions = [
$sql['password'] => 'DB_UNPRIV_PWD', $sql['password'] => 'DB_UNPRIV_PWD',
$sql_root[0]['password'] => 'DB_ROOT_PWD'
]; ];
foreach ($sql_root as $dbserver => $sql_root_data) {
$substitutions[$sql_root_data[$dbserver]]['password'] = 'DB_ROOT_PWD';
}
// hide username/password in messages // hide username/password in messages
$error_message = $error->getMessage(); $error_message = $error->getMessage();
@@ -341,12 +352,13 @@ class Database
* @param int $dbserver * @param int $dbserver
* optional * optional
*/ */
public static function needRoot($needroot = false, $dbserver = 0) public static function needRoot(bool $needroot = false, int $dbserver = 0, bool $need_db = true)
{ {
// force re-connecting to the db with corresponding user // force re-connecting to the db with corresponding user
// and set the $dbserver (mostly to 0 = default) // and set the $dbserver (mostly to 0 = default)
self::setServer($dbserver); self::setServer($dbserver);
self::$needroot = $needroot; self::$needroot = $needroot;
self::$need_dbname = $need_db;
} }
/** /**
@@ -405,7 +417,7 @@ class Database
require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; require Froxlor::getInstallDir() . "/lib/userdata.inc.php";
// le format // le format
if (self::$needroot == true && isset($sql['root_user']) && isset($sql['root_password']) && (!isset($sql_root) || !is_array($sql_root))) { if (isset($sql['root_user']) && isset($sql['root_password']) && (!isset($sql_root) || !is_array($sql_root))) {
$sql_root = [ $sql_root = [
0 => [ 0 => [
'caption' => 'Default', 'caption' => 'Default',
@@ -417,6 +429,10 @@ class Database
]; ];
unset($sql['root_user']); unset($sql['root_user']);
unset($sql['root_password']); unset($sql['root_password']);
// write new layout so this won't happen again
self::generateNewUserData($sql, $sql_root);
// re-read file
require Froxlor::getInstallDir() . "/lib/userdata.inc.php";
} }
// either root or unprivileged user // either root or unprivileged user
@@ -465,10 +481,11 @@ class Database
'ATTR_ERRMODE' => 'ERRMODE_EXCEPTION' 'ATTR_ERRMODE' => 'ERRMODE_EXCEPTION'
]; ];
$dbconf["dsn"] = [ $dbconf["dsn"] = ['charset' => 'utf8'];
'dbname' => $sql["db"],
'charset' => 'utf8' if (self::$need_dbname) {
]; $dbconf["dsn"]['dbname'] = $sql["db"];
}
if ($socket != null) { if ($socket != null) {
$dbconf["dsn"]['unix_socket'] = FileDir::makeCorrectFile($socket); $dbconf["dsn"]['unix_socket'] = FileDir::makeCorrectFile($socket);
@@ -578,4 +595,22 @@ class Database
} }
return $result; return $result;
} }
/**
* write new userdata.inc.php file
*/
private static function generateNewUserData(array $sql, array $sql_root)
{
$content = PhpHelper::parseArrayToPhpFile(
['sql' => $sql, 'sql_root' => $sql_root],
'automatically generated userdata.inc.php for froxlor'
);
chmod(Froxlor::getInstallDir() . "/lib/userdata.inc.php", 0700);
file_put_contents(Froxlor::getInstallDir() . "/lib/userdata.inc.php", $content);
chmod(Froxlor::getInstallDir() . "/lib/userdata.inc.php", 0400);
clearstatcache();
if (function_exists('opcache_invalidate')) {
@opcache_invalidate(Froxlor::getInstallDir() . "/lib/userdata.inc.php", true);
}
}
} }

View File

@@ -96,7 +96,7 @@ class DbManager
$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)) {
// require privileged access for target db-server // require privileged access for target db-server
Database::needRoot(true, $dbserver['dbserver']); Database::needRoot(true, $dbserver['dbserver'], false);
$dbm = new DbManager(FroxlorLogger::getInstanceOf()); $dbm = new DbManager(FroxlorLogger::getInstanceOf());
$users = $dbm->getManager()->getAllSqlUsers(false); $users = $dbm->getManager()->getAllSqlUsers(false);
@@ -144,7 +144,7 @@ class DbManager
*/ */
public function createDatabase($loginname = null, $password = null, int $dbserver = 0, $last_accnumber = 0) public function createDatabase($loginname = null, $password = null, int $dbserver = 0, $last_accnumber = 0)
{ {
Database::needRoot(true, $dbserver); Database::needRoot(true, $dbserver, false);
// 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') {
@@ -169,18 +169,17 @@ class DbManager
// now create the database itself // now create the database itself
$this->getManager()->createDatabase($username); $this->getManager()->createDatabase($username);
$this->log->logAction(FroxlorLogger::USR_ACTION, LOG_INFO, "created database '" . $username . "'");
// 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);
$this->log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "grant all privileges for '" . $username . "'@'" . $mysql_access_host . "'");
} }
$this->getManager()->flushPrivileges(); $this->getManager()->flushPrivileges();
Database::needRoot(false); Database::needRoot(false);
$this->log->logAction(FroxlorLogger::USR_ACTION, LOG_INFO, "created database '" . $username . "'");
return $username; return $username;
} }

View File

@@ -31,10 +31,10 @@ final class Froxlor
{ {
// Main version variable // Main version variable
const VERSION = '2.0.4'; const VERSION = '2.0.8';
// Database version (YYYYMMDDC where C is a daily counter) // Database version (YYYYMMDDC where C is a daily counter)
const DBVERSION = '202212060'; const DBVERSION = '202301120';
// Distribution branding-tag (used for Debian etc.) // Distribution branding-tag (used for Debian etc.)
const BRANDING = ''; const BRANDING = '';

View File

@@ -100,11 +100,17 @@ class FroxlorLogger
self::$ml->pushHandler(new SyslogHandler('froxlor', LOG_USER, Logger::DEBUG)); self::$ml->pushHandler(new SyslogHandler('froxlor', LOG_USER, Logger::DEBUG));
break; break;
case 'file': case 'file':
$logger_logfile = Settings::Get('logger.logfile'); $logger_logfile = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/logs/' . Settings::Get('logger.logfile'));
// is_writable needs an existing file to check if it's actually writable // is_writable needs an existing file to check if it's actually writable
@touch($logger_logfile); @touch($logger_logfile);
if (empty($logger_logfile) || !is_writable($logger_logfile)) { if (empty($logger_logfile) || !is_writable($logger_logfile)) {
Settings::Set('logger.logfile', '/tmp/froxlor.log'); Settings::Set('logger.logfile', 'froxlor.log');
$logger_logfile = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/logs/froxlor.log');
@touch($logger_logfile);
if (empty($logger_logfile) || !is_writable($logger_logfile)) {
// not writable in our own directory? Skip
break;
}
} }
self::$ml->pushHandler(new StreamHandler($logger_logfile, Logger::DEBUG)); self::$ml->pushHandler(new StreamHandler($logger_logfile, Logger::DEBUG));
break; break;

View File

@@ -32,7 +32,7 @@ class Mysql
public static function dbserver(array $attributes): string public static function dbserver(array $attributes): string
{ {
// get sql-root access data // get sql-root access data
Database::needRoot(true, (int)$attributes['data']); Database::needRoot(true, (int)$attributes['data'], false);
Database::needSqlData(); Database::needSqlData();
$sql_root = Database::getSqlData(); $sql_root = Database::getSqlData();
Database::needRoot(false); Database::needRoot(false);

View File

@@ -95,7 +95,7 @@ class UI
session_set_cookie_params([ session_set_cookie_params([
'lifetime' => self::$install_mode ? 7200 : 600, // will be renewed based on settings in lib/init.php 'lifetime' => self::$install_mode ? 7200 : 600, // will be renewed based on settings in lib/init.php
'path' => '/', 'path' => '/',
'domain' => $_SERVER['HTTP_HOST'], 'domain' => $_SERVER['SERVER_NAME'],
'secure' => self::requestIsHttps(), 'secure' => self::requestIsHttps(),
'httponly' => true, 'httponly' => true,
'samesite' => 'Strict' 'samesite' => 'Strict'
@@ -260,8 +260,17 @@ class UI
*/ */
public static function twigBuffer($name, array $context = []) public static function twigBuffer($name, array $context = [])
{ {
$template_file = self::getTheme() . '/' . $name;
if (!file_exists(Froxlor::getInstallDir() . '/templates/' . $template_file)) {
PhpHelper::phpErrHandler(E_USER_WARNING, "Template '" . $template_file . "' could not be found, trying fallback theme", __FILE__, __LINE__);
$template_file = self::$default_theme . '/'. $name;
if (!file_exists(Froxlor::getInstallDir() . '/templates/' . $template_file)) {
PhpHelper::phpErrHandler(E_USER_ERROR, "Unknown template '" . $template_file . "'", __FILE__, __LINE__);
}
}
self::$twigbuf[] = [ self::$twigbuf[] = [
self::getTheme() . '/' . $name => $context $template_file => $context
]; ];
} }

View File

@@ -3458,11 +3458,7 @@ ssl_key = <<SSL_KEY_FILE>
# auth_ssl_username_from_cert=yes. # auth_ssl_username_from_cert=yes.
#ssl_cert_username_field = commonName #ssl_cert_username_field = commonName
# SSL DH parameters ssl_dh_parameters_length = 2048
# Generate new params with `openssl dhparam -out /etc/dovecot/dh.pem 4096`
# Or migrate from old ssl-parameters.dat file with the command dovecot
# gives on startup when ssl_dh is unset.
ssl_dh = </usr/share/dovecot/dh.pem
# SSL protocols to use # SSL protocols to use
#ssl_protocols = !SSLv3 #ssl_protocols = !SSLv3
@@ -4678,7 +4674,7 @@ aliases: files
<command><![CDATA[mkdir -p {{settings.system.mod_fcgid_configdir}}]]></command> <command><![CDATA[mkdir -p {{settings.system.mod_fcgid_configdir}}]]></command>
<command><![CDATA[mkdir -p {{settings.system.mod_fcgid_tmpdir}}]]></command> <command><![CDATA[mkdir -p {{settings.system.mod_fcgid_tmpdir}}]]></command>
<command><![CDATA[chmod 1777 {{settings.system.mod_fcgid_tmpdir}}]]></command> <command><![CDATA[chmod 1777 {{settings.system.mod_fcgid_tmpdir}}]]></command>
<command><![CDATA[a2dismod php7.2]]></command> <command><![CDATA[a2dismod php7.4]]></command>
</commands> </commands>
<!-- instead of just restarting apache, we let the cronjob do all the <!-- instead of just restarting apache, we let the cronjob do all the
dirty work --> dirty work -->
@@ -4711,7 +4707,7 @@ aliases: files
</visibility> </visibility>
<visibility mode="true">{{settings.phpfpm.enabled_ownvhost}} <visibility mode="true">{{settings.phpfpm.enabled_ownvhost}}
</visibility> </visibility>
<command><![CDATA[a2dismod php7.2]]></command> <command><![CDATA[a2dismod php7.4]]></command>
</commands> </commands>
<!-- instead of just restarting apache, we let the cronjob do all the <!-- instead of just restarting apache, we let the cronjob do all the
dirty work --> dirty work -->

View File

@@ -30,6 +30,7 @@ return [
'title' => lng('admin.domain_add'), 'title' => lng('admin.domain_add'),
'image' => 'fa-solid fa-globe', 'image' => 'fa-solid fa-globe',
'self_overview' => ['section' => 'domains', 'page' => 'domains'], 'self_overview' => ['section' => 'domains', 'page' => 'domains'],
'id' => 'domain_add',
'sections' => [ 'sections' => [
'section_a' => [ 'section_a' => [
'title' => lng('domains.domainsettings'), 'title' => lng('domains.domainsettings'),

View File

@@ -30,6 +30,7 @@ return [
'title' => lng('admin.domain_edit'), 'title' => lng('admin.domain_edit'),
'image' => 'fa-solid fa-globe', 'image' => 'fa-solid fa-globe',
'self_overview' => ['section' => 'domains', 'page' => 'domains'], 'self_overview' => ['section' => 'domains', 'page' => 'domains'],
'id' => 'domain_edit',
'sections' => [ 'sections' => [
'section_a' => [ 'section_a' => [
'title' => lng('domains.domainsettings'), 'title' => lng('domains.domainsettings'),

View File

@@ -33,10 +33,16 @@ return [
'value' => $result['databasename'] 'value' => $result['databasename']
], ],
'mysql_server' => [ 'mysql_server' => [
'visible' => count($mysql_servers) > 1,
'type' => 'hidden',
'value' => $result['dbserver'] ?? 0,
],
'mysql_server_info' => [
'visible' => count($mysql_servers) > 1, 'visible' => count($mysql_servers) > 1,
'label' => lng('mysql.mysql_server'), 'label' => lng('mysql.mysql_server'),
'type' => 'label', 'type' => 'label',
'value' => $mysql_servers[$result['dbserver']] ?? 'unknown db server' 'disabled' => true,
'value' => $mysql_servers[$result['dbserver']] ?? 'unknown db server',
], ],
'description' => [ 'description' => [
'label' => lng('mysql.databasedescription'), 'label' => lng('mysql.databasedescription'),

View File

@@ -332,7 +332,7 @@ if (CurrentUser::hasSession()) {
$cookie_params = [ $cookie_params = [
'expires' => time() + Settings::Get('session.sessiontimeout'), 'expires' => time() + Settings::Get('session.sessiontimeout'),
'path' => '/', 'path' => '/',
'domain' => $_SERVER['HTTP_HOST'], 'domain' => $_SERVER['SERVER_NAME'],
'secure' => UI::requestIsHttps(), 'secure' => UI::requestIsHttps(),
'httponly' => true, 'httponly' => true,
'samesite' => 'Strict' 'samesite' => 'Strict'

View File

@@ -1494,7 +1494,10 @@ Vielen Dank, Ihr Administrator',
'title' => 'Log-Art(en)', 'title' => 'Log-Art(en)',
'description' => 'Wählen Sie hier die gewünschten Logtypen. Für Mehrfachauswahl, halten Sie während der Auswahl STRG gedrückt<br />Mögliche Logtypen sind: syslog, file, mysql', 'description' => 'Wählen Sie hier die gewünschten Logtypen. Für Mehrfachauswahl, halten Sie während der Auswahl STRG gedrückt<br />Mögliche Logtypen sind: syslog, file, mysql',
], ],
'logfile' => 'Log-Datei Pfad inklusive Dateinamen', 'logfile' => [
'title' => 'Dateiname der Logdatei',
'description' => 'Wird nur verwendet, wenn die Log-Art "file" ausgewählt ist. Diese Datei wird unter froxlor/logs/ geschrieben. Dieser Ordner ist vor Webzugriff geschützt.',
],
'logcron' => 'Logge Cronjobs', 'logcron' => 'Logge Cronjobs',
'logcronoption' => [ 'logcronoption' => [
'never' => 'Nie', 'never' => 'Nie',
@@ -2124,17 +2127,31 @@ Vielen Dank, Ihr Administrator',
], ],
'mb' => 'Traffic', 'mb' => 'Traffic',
'day' => 'Tag', 'day' => 'Tag',
'distribution' => '<span color="#019522">FTP</span> | <span color="#0000FF">HTTP</span> | <span color="#800000">Mail</span>', 'sumtotal' => 'Gesamt Traffic',
'sumhttp' => 'Gesamt HTTP-Traffic', 'sumhttp' => 'HTTP-Traffic',
'sumftp' => 'Gesamt FTP-Traffic', 'sumftp' => 'FTP-Traffic',
'summail' => 'Gesamt Mail-Traffic', 'summail' => 'Mail-Traffic',
'customer' => 'Kunde', 'customer' => 'Kunde',
'trafficoverview' => 'Übersicht Traffic je', 'trafficoverview' => 'Übersicht Traffic',
'bycustomers' => 'Traffic nach Kunden',
'details' => 'Details', 'details' => 'Details',
'http' => 'HTTP', 'http' => 'HTTP',
'ftp' => 'FTP', 'ftp' => 'FTP',
'mail' => 'Mail', 'mail' => 'Mail',
'nocustomers' => 'Es wird mindestens ein Kunde benötigt um die Traffic Statistiken anzuzeigen.', 'nocustomers' => 'Es wird mindestens ein Kunde benötigt um die Traffic Statistiken anzuzeigen.',
'top5customers' => 'Top 5 Kunden',
'nodata' => 'Keine Daten im angegebenen Zeitraum.',
'ranges' => [
'last24h' => 'die letzten 24 Std',
'last7d' => 'die letzten 7 Tage',
'last30d' => 'die letzten 30 Tage',
'cm' => 'Aktueller Monat',
'last3m' => 'die letzten 3 Monate',
'last6m' => 'die letzten 6 Monate',
'last12m' => 'die letzten 12 Monate',
'cy' => 'Aktuelles Jahr',
],
'byrange' => 'Nach angegebenem Zeitraum',
], ],
'translator' => '', 'translator' => '',
'update' => [ 'update' => [

View File

@@ -1613,7 +1613,10 @@ Yours sincerely, your administrator',
'title' => 'Log-type(s)', 'title' => 'Log-type(s)',
'description' => 'Specify logtypes. To select multiple types, hold down CTRL while selecting.<br />Available logtypes are: syslog, file, mysql', 'description' => 'Specify logtypes. To select multiple types, hold down CTRL while selecting.<br />Available logtypes are: syslog, file, mysql',
], ],
'logfile' => 'Logfile path including filename', 'logfile' => [
'title' => 'Filename for log',
'description' => 'Only used if log-type includes "file". This file will be created in froxlor/logs/. This folder is protected against public access.',
],
'logcron' => 'Log cronjobs', 'logcron' => 'Log cronjobs',
'logcronoption' => [ 'logcronoption' => [
'never' => 'Never', 'never' => 'Never',
@@ -2254,18 +2257,32 @@ Yours sincerely, your administrator',
'total' => 'Total', 'total' => 'Total',
], ],
'mb' => 'Traffic', 'mb' => 'Traffic',
'distribution' => '<font color="#019522">FTP</font> | <font color="#0000FF">HTTP</font> | <font color="#800000">Mail</font>', 'sumtotal' => 'Total traffic',
'sumhttp' => 'Total HTTP-Traffic', 'sumhttp' => 'HTTP traffic',
'sumftp' => 'Total FTP-Traffic', 'sumftp' => 'FTP traffic',
'summail' => 'Total Mail-Traffic', 'summail' => 'Mail traffic',
'customer' => 'Customer', 'customer' => 'Customer',
'domain' => 'Domain', 'domain' => 'Domain',
'trafficoverview' => 'Traffic summary by', 'trafficoverview' => 'Traffic summary',
'bycustomers' => 'Traffic by customers',
'details' => 'Details', 'details' => 'Details',
'http' => 'HTTP', 'http' => 'HTTP',
'ftp' => 'FTP', 'ftp' => 'FTP',
'mail' => 'Mail', 'mail' => 'Mail',
'nocustomers' => 'You need at least one customer to view the traffic reports.', 'nocustomers' => 'You need at least one customer to view the traffic reports.',
'top5customers' => 'Top 5 customers',
'nodata' => 'No data for given range found.',
'ranges' => [
'last24h' => 'last 24 hours',
'last7d' => 'last 7 days',
'last30d' => 'last 30 days',
'cm' => 'Current month',
'last3m' => 'last 3 months',
'last6m' => 'last 6 months',
'last12m' => 'last 12 months',
'cy' => 'Current year',
],
'byrange' => 'Specified by range',
], ],
'translator' => '', 'translator' => '',
'update' => [ 'update' => [

36
package-lock.json generated
View File

@@ -5103,9 +5103,9 @@
} }
}, },
"node_modules/img-loader/node_modules/json5": { "node_modules/img-loader/node_modules/json5": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"minimist": "^1.2.0" "minimist": "^1.2.0"
@@ -5437,9 +5437,9 @@
"dev": true "dev": true
}, },
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.1", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true, "dev": true,
"bin": { "bin": {
"json5": "lib/cli.js" "json5": "lib/cli.js"
@@ -8448,9 +8448,9 @@
} }
}, },
"node_modules/vue-style-loader/node_modules/json5": { "node_modules/vue-style-loader/node_modules/json5": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"minimist": "^1.2.0" "minimist": "^1.2.0"
@@ -12919,9 +12919,9 @@
}, },
"dependencies": { "dependencies": {
"json5": { "json5": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true, "dev": true,
"requires": { "requires": {
"minimist": "^1.2.0" "minimist": "^1.2.0"
@@ -13165,9 +13165,9 @@
"dev": true "dev": true
}, },
"json5": { "json5": {
"version": "2.2.1", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true "dev": true
}, },
"jsonfile": { "jsonfile": {
@@ -15379,9 +15379,9 @@
}, },
"dependencies": { "dependencies": {
"json5": { "json5": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true, "dev": true,
"requires": { "requires": {
"minimist": "^1.2.0" "minimist": "^1.2.0"

View File

@@ -50,7 +50,7 @@
</form> </form>
{# add translation for custom validations #} {# add translation for custom validations #}
{% if form_data.id is defined and form_data.id in ['customer_add', 'customer_edit'] %} {% if form_data.id is defined and form_data.id in ['customer_add', 'customer_edit', 'domain_add', 'domain_edit'] %}
<script>$(function() { $.extend($.validator.messages, {required: "{{ lng('error.requiredfield') }}"}) });</script> <script>$(function() { $.extend($.validator.messages, {required: "{{ lng('error.requiredfield') }}"}) });</script>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}

View File

@@ -2,6 +2,9 @@
{% if field.visible is not defined or (field.visible is defined and field.visible) or nohide == true %} {% if field.visible is not defined or (field.visible is defined and field.visible) or nohide == true %}
{% if norow == false and (field.type != 'hidden' or (field.type == 'hidden' and field.display is defined and field.display is not empty)) %} {% if norow == false and (field.type != 'hidden' or (field.type == 'hidden' and field.display is defined and field.display is not empty)) %}
<div class="row g-0 formfield d-flex align-items-center"> <div class="row g-0 formfield d-flex align-items-center">
{% if field.prior_infotext is defined and field.prior_infotext|length > 0 %}
<h5>{{ field.prior_infotext }}</h5>
{% endif %}
{% if field.label is iterable %} {% if field.label is iterable %}
<label for="{{ id }}" class="col-sm-6 col-form-label pe-3"> <label for="{{ id }}" class="col-sm-6 col-form-label pe-3">
{% if em %} {% if em %}
@@ -165,7 +168,7 @@
{% macro image(id, field) %} {% macro image(id, field) %}
{% if field.value is not empty %} {% if field.value is not empty %}
<img src="/{{ field.value }}" alt="Current Image" class="field-image-preview"><br> <img src="{{ field.value }}" alt="Current Image" class="field-image-preview"><br>
<div class="form-check form-switch mb-2"> <div class="form-check form-switch mb-2">
<input type="checkbox" value="1" name="{{ id }}_delete" class="form-check-input"> <input type="checkbox" value="1" name="{{ id }}_delete" class="form-check-input">
<label class="form-check-label"> <label class="form-check-label">
@@ -197,7 +200,7 @@
{% if field.next_to is defined %} {% if field.next_to is defined %}
<div class="input-group"> <div class="input-group">
{% endif %} {% endif %}
<select {% if field.visible is defined and field.visible == false %} disabled {% endif %} class="form-select {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" name="{{ id }}{% if field.select_mode is defined and field.select_mode == 'multiple' %}[]{% endif %}" id="{{ id }}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.select_mode is defined and field.select_mode == 'multiple' %} multiple="multiple" {% endif %}> <select {% if field.visible is defined and field.visible == false %} disabled {% endif %} class="form-select {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" name="{{ id }}{% if field.select_mode is defined and field.select_mode == 'multiple' %}[]{% endif %}" id="{{ id }}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.select_mode is defined and field.select_mode == 'multiple' %} multiple="multiple" {% endif %}{% if field.readonly is defined and field.readonly %} readonly {% endif %}>
{% for val,txt in field.select_var %} {% for val,txt in field.select_var %}
<option value="{{ val }}" {% if field.selected is defined and ((field.selected is not iterable and field.selected == val) or (field.selected is iterable and val in field.selected|keys)) %} selected="selected" {% endif %}>{{ txt|raw }}</option> <option value="{{ val }}" {% if field.selected is defined and ((field.selected is not iterable and field.selected == val) or (field.selected is iterable and val in field.selected|keys)) %} selected="selected" {% endif %}>{{ txt|raw }}</option>
{% endfor %} {% endfor %}

View File

@@ -1,19 +1,19 @@
$(document).ready(function() { $(document).ready(function () {
$('#customer_add,#customer_edit').each(function(){ $('#customer_add,#customer_edit').each(function () {
$(this).validate({ $(this).validate({
rules:{ rules: {
'name':{ 'name': {
required:function(){ required: function () {
return $('#company').val().length === 0 || $('#firstname').val().length > 0; return $('#company').val().length === 0 || $('#firstname').val().length > 0;
} }
}, },
'firstname':{ 'firstname': {
required:function(){ required: function () {
return $('#company').val().length === 0 || $('#name').val().length > 0; return $('#company').val().length === 0 || $('#name').val().length > 0;
} }
}, },
'company':{ 'company': {
required:function(){ required: function () {
return $('#name').val().length === 0 return $('#name').val().length === 0
&& $('#firstname').val().length === 0; && $('#firstname').val().length === 0;
} }
@@ -21,4 +21,17 @@ $(document).ready(function() {
}, },
}); });
}); });
$('#domain_add,#domain_edit').each(function () {
$(this).validate({
rules: {
'ipandport[]': {
required: true,
minlength: 1
}
},
errorPlacement: function(error, element) {
$(error).prependTo($(element).parent().parent());
}
});
});
}); });

View File

@@ -3,6 +3,10 @@
@import "~@fortawesome/fontawesome-free/css/all"; @import "~@fortawesome/fontawesome-free/css/all";
// Generic // Generic
.header-logo {
height: 24px;
}
.form-control-plaintext { .form-control-plaintext {
outline: none; outline: none;
} }

View File

@@ -1,7 +1,7 @@
{% macro ditem(lngstr, available, used, assigned = null, formatbytes = false) %} {% macro ditem(lngstr, available, used, assigned = null, formatbytes = false, byte_usage = null) %}
<div class="col border-end border-bottom p-3"> <div class="col border-end border-bottom p-3">
<div class="row mb-1"> <div class="row mb-1">
<div class="col text-truncate">{{ lng(lngstr) }}</div> <div class="col text-truncate">{{ lng(lngstr) }}{% if byte_usage %} <small>({{ byte_usage|formatBytes }})</small>{% endif %}</div>
<div class="col-auto"> <div class="col-auto">
<small>{% if formatbytes %}{{ used|formatBytes }}{% else %}{{ used }}{% endif %}/{% if available < 0 %}{{ lng('panel.unlimited') }}{% else %}{% if formatbytes %}{{ available|formatBytes }}{% else %}{{ available }}{% endif %}{% endif %}</small> <small>{% if formatbytes %}{{ used|formatBytes }}{% else %}{{ used }}{% endif %}/{% if available < 0 %}{{ lng('panel.unlimited') }}{% else %}{% if formatbytes %}{{ available|formatBytes }}{% else %}{{ available }}{% endif %}{% endif %}</small>
</div> </div>

View File

@@ -33,12 +33,13 @@
{% else %} {% else %}
{# customer-resources #} {# customer-resources #}
<div class="row row-cols-1 row-cols-sm-2 row-cols-xl-4 g-0"> <div class="row row-cols-1 row-cols-sm-2 row-cols-xl-4 g-0">
{{ dashboard.ditem('customer.total_diskspace', userinfo.diskspace_bytes, userinfo.total_bytes_used, null, true) }}
{{ dashboard.ditem('customer.diskspace', userinfo.diskspace_bytes, userinfo.diskspace_bytes_used, null, true) }} {{ dashboard.ditem('customer.diskspace', userinfo.diskspace_bytes, userinfo.diskspace_bytes_used, null, true) }}
{{ dashboard.ditem('customer.traffic', userinfo.traffic_bytes, userinfo.traffic_bytes_used, null, true) }} {{ dashboard.ditem('customer.traffic', userinfo.traffic_bytes, userinfo.traffic_bytes_used, null, true) }}
{{ dashboard.ditem('customer.subdomains', userinfo.subdomains, userinfo.subdomains_used) }} {{ dashboard.ditem('customer.subdomains', userinfo.subdomains, userinfo.subdomains_used) }}
{{ dashboard.ditem('customer.mysqls', userinfo.mysqls, userinfo.mysqls_used) }} {{ dashboard.ditem('customer.mysqls', userinfo.mysqls, userinfo.mysqls_used, null, false, userinfo.dbspace_used) }}
{{ dashboard.ditem('customer.emails', userinfo.emails, userinfo.emails_used) }} {{ dashboard.ditem('customer.emails', userinfo.emails, userinfo.emails_used) }}
{{ dashboard.ditem('customer.accounts', userinfo.email_accounts, userinfo.email_accounts_used) }} {{ dashboard.ditem('customer.accounts', userinfo.email_accounts, userinfo.email_accounts_used, null, false, userinfo.mailspace_used) }}
{{ dashboard.ditem('customer.forwarders', userinfo.email_forwarders, userinfo.email_forwarders_used) }} {{ dashboard.ditem('customer.forwarders', userinfo.email_forwarders, userinfo.email_forwarders_used) }}
{{ dashboard.ditem('customer.ftps', userinfo.ftps, userinfo.ftps_used) }} {{ dashboard.ditem('customer.ftps', userinfo.ftps, userinfo.ftps_used) }}
</div> </div>

View File

@@ -18,14 +18,14 @@
<!-- TODO: set url on change. e.g.: ?param=days:7 --> <!-- TODO: set url on change. e.g.: ?param=days:7 -->
<div class="d-flex justify-content-center justify-content-md-end"> <div class="d-flex justify-content-center justify-content-md-end">
<select class="form-select mb-3 mb-md-4 w-auto mt-md-n4" aria-label="select the traffic range" name="range" data-baseref="{{ linker({'section':'traffic'}) }}"> <select class="form-select mb-3 mb-md-4 w-auto mt-md-n4" aria-label="select the traffic range" name="range" data-baseref="{{ linker({'section':'traffic'}) }}">
<option value="hours:24" {% if range == 'hours:24' %}selected{% endif %}>last 24 hours</option> <option value="hours:24" {% if range == 'hours:24' %}selected{% endif %}>{{ lng('traffic.ranges.last24h') }}</option>
<option value="days:7" {% if range == 'days:7' %}selected{% endif %}>last 7 days</option> <option value="days:7" {% if range == 'days:7' %}selected{% endif %}>{{ lng('traffic.ranges.last7d') }}</option>
<option value="days:30" {% if range == 'days:30' %}selected{% endif %}>last 30 days</option> <option value="days:30" {% if range == 'days:30' %}selected{% endif %}>{{ lng('traffic.ranges.last30d') }}</option>
<option value="currentmonth" {% if range == 'currentmonth' %}selected{% endif %}>current month</option> <option value="currentmonth" {% if range == 'currentmonth' %}selected{% endif %}>{{ lng('traffic.ranges.cm') }}</option>
<option value="months:3" {% if range == 'months:3' %}selected{% endif %}>last 3 months</option> <option value="months:3" {% if range == 'months:3' %}selected{% endif %}>{{ lng('traffic.ranges.last3m') }}</option>
<option value="months:6" {% if range == 'months:6' %}selected{% endif %}>last 6 months</option> <option value="months:6" {% if range == 'months:6' %}selected{% endif %}>{{ lng('traffic.ranges.last6m') }}</option>
<option value="months:12" {% if range == 'months:12' %}selected{% endif %}>last 12 months</option> <option value="months:12" {% if range == 'months:12' %}selected{% endif %}>{{ lng('traffic.ranges.last12m') }}</option>
<option value="currentyear" {% if range == 'currentyear' %}selected{% endif %}>current year</option> <option value="currentyear" {% if range == 'currentyear' %}selected{% endif %}>{{ lng('traffic.ranges.cy') }}</option>
{% for yd in years_avail %} {% for yd in years_avail %}
{% if yd.year != "now"|date('Y') %} {% if yd.year != "now"|date('Y') %}
<option value="year:{{ yd.year }}" {% if range == 'year:' ~ yd.year %}selected{% endif %}>{{ yd.year }}</option> <option value="year:{{ yd.year }}" {% if range == 'year:' ~ yd.year %}selected{% endif %}>{{ yd.year }}</option>
@@ -50,37 +50,36 @@
<div class="row row-cols-2 row-cols-md-4 g-0"> <div class="row row-cols-2 row-cols-md-4 g-0">
<div class="col p-3 border-end"> <div class="col p-3 border-end">
<h3>{{ metrics.total|formatBytes }}</h3> <h3>{{ metrics.total|formatBytes }}</h3>
<span>Total</span> <span>{{ lng('traffic.months.total') }}</span>
</div> </div>
<div class="col p-3 border-end"> <div class="col p-3 border-end">
<h3>{{ metrics.http|formatBytes }}</h3> <h3>{{ metrics.http|formatBytes }}</h3>
<span>HTTP</span> <span>{{ lng('traffic.http') }}</span>
</div> </div>
<div class="col p-3 border-end"> <div class="col p-3 border-end">
<h3>{{ metrics.ftp|formatBytes }}</h3> <h3>{{ metrics.ftp|formatBytes }}</h3>
<span>FTP</span> <span>{{ lng('traffic.ftp') }}</span>
</div> </div>
<div class="col p-3 border-end"> <div class="col p-3 border-end">
<h3>{{ metrics.mail|formatBytes }}</h3> <h3>{{ metrics.mail|formatBytes }}</h3>
<span>Mail</span> <span>{{ lng('traffic.mail') }}</span>
</div> </div>
</div> </div>
</div> </div>
{% if userinfo.adminsession == 1 %} {% if userinfo.adminsession == 1 %}
<!-- Overview for given range by user --> <!-- Overview for given range by user -->
<h4 class="page-header">Traffic by customers</h4> <h4 class="page-header">{{ lng('traffic.bycustomers') }}</h4>
{% if users is not empty %} {% if users is not empty %}
<div class="card table-responsive"> <div class="card table-responsive">
<table class="table table-borderless table-striped align-middle mb-0 px-3"> <table class="table table-borderless table-striped align-middle mb-0 px-3">
<thead> <thead>
<tr> <tr>
<th scope="col">{{ lng('login.username') }}</th> <th scope="col">{{ lng('login.username') }}</th>
<th scope="col">Total</th> <th scope="col">{{ lng('traffic.months.total') }}</th>
<th scope="col">HTTP</th> <th scope="col">{{ lng('traffic.http') }}</th>
<th scope="col">FTP</th> <th scope="col">{{ lng('traffic.ftp') }}</th>
<th scope="col">Mail <th scope="col">{{ lng('traffic.mail') }}</th>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -101,19 +100,19 @@
{% else %} {% else %}
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<p>No data for given range found.</p> <p>{{ lng('traffic.nodata') }}</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
<script> <script>
const labelsS = ['HTTP', 'FTP', 'Mail']; const labelsS = ['{{ lng('traffic.http') }}', '{{ lng('traffic.ftp') }}', '{{ lng('traffic.mail') }}'];
const dataS = { const dataS = {
labels: labelsS, labels: labelsS,
datasets: [{ datasets: [{
label: 'Traffic summary', label: '{{ lng('traffic.trafficoverview') }}',
backgroundColor: ['rgb(255, 99, 132)', 'rgb(200, 199, 132)', 'rgb(255, 99, 0)'], backgroundColor: ['rgb(255, 99, 132)', 'rgb(200, 199, 132)', 'rgb(255, 99, 0)'],
data: [{value: '{{ metrics.http|default(0) }}', formatted: '{{ metrics.http|formatBytes }}'}, {value: '{{ metrics.ftp|default(0) }}', formatted: '{{ metrics.ftp|formatBytes }}'}, {value: '{{ metrics.mail|default(0) }}', formatted: '{{ metrics.mail|formatBytes }}'}] data: [{value: '{{ metrics.http|default(0) }}', formatted: '{{ metrics.http|formatBytes }}'}, {value: '{{ metrics.ftp|default(0) }}', formatted: '{{ metrics.ftp|formatBytes }}'}, {value: '{{ metrics.mail|default(0) }}', formatted: '{{ metrics.mail|formatBytes }}'}]
}] }]
@@ -130,7 +129,7 @@
plugins: { plugins: {
title: { title: {
display: true, display: true,
text: 'Total traffic' text: '{{ lng('traffic.sumtotal') }}'
}, },
legend: { legend: {
position: 'right' position: 'right'
@@ -161,7 +160,7 @@
const dataC = { const dataC = {
labels: labelsC, labels: labelsC,
datasets: [{ datasets: [{
label: 'Top 5 customers', label: '{{ lng('traffic.top5customers') }}',
backgroundColor: ['rgb(255, 99, 132)', 'rgb(200, 199, 132)', 'rgb(255, 99, 0)', 'rgb(100, 100, 132)', 'rgb(240, 150, 232)'], backgroundColor: ['rgb(255, 99, 132)', 'rgb(200, 199, 132)', 'rgb(255, 99, 0)', 'rgb(100, 100, 132)', 'rgb(240, 150, 232)'],
data: dataValues data: dataValues
}] }]
@@ -178,7 +177,7 @@
plugins: { plugins: {
title: { title: {
display: true, display: true,
text: 'Top 5 customers' text: '{{ lng('traffic.top5customers') }}'
}, },
legend: { legend: {
position: 'right' position: 'right'
@@ -220,7 +219,7 @@
labels: labelsC, labels: labelsC,
datasets: [ datasets: [
{ {
label: 'HTTP traffic', label: '{{ lng('traffic.sumhttp') }}',
backgroundColor: 'rgb(255, 99, 132)', backgroundColor: 'rgb(255, 99, 132)',
{% if range starts with 'days' or range == 'currentmonth' %} {% if range starts with 'days' or range == 'currentmonth' %}
data: [{% for d,dd in days %}{value: '{{ dd.http|default(0) }}', formatted: '{{ dd.http|formatBytes }}', axisv: '{{ d }}'},{% endfor %}], data: [{% for d,dd in days %}{value: '{{ dd.http|default(0) }}', formatted: '{{ dd.http|formatBytes }}', axisv: '{{ d }}'},{% endfor %}],
@@ -234,7 +233,7 @@
} }
}, },
{ {
label: 'FTP traffic', label: '{{ lng('traffic.sumftp') }}',
backgroundColor: 'rgb(200, 199, 132)', backgroundColor: 'rgb(200, 199, 132)',
{% if range starts with 'days' or range == 'currentmonth' %} {% if range starts with 'days' or range == 'currentmonth' %}
data: [{% for d,dd in days %}{value: '{{ dd.ftp|default(0) }}', formatted: '{{ dd.ftp|formatBytes }}', axisv: '{{ d }}'},{% endfor %}], data: [{% for d,dd in days %}{value: '{{ dd.ftp|default(0) }}', formatted: '{{ dd.ftp|formatBytes }}', axisv: '{{ d }}'},{% endfor %}],
@@ -248,7 +247,7 @@
} }
}, },
{ {
label: 'Mail traffic', label: '{{ lng('traffic.summail') }}',
backgroundColor: 'rgb(255, 99, 0)', backgroundColor: 'rgb(255, 99, 0)',
{% if range starts with 'days' or range == 'currentmonth' %} {% if range starts with 'days' or range == 'currentmonth' %}
data: [{% for d,dd in days %}{value: '{{ dd.mail|default(0) }}', formatted: '{{ dd.mail|formatBytes }}', axisv: '{{ d }}'},{% endfor %}], data: [{% for d,dd in days %}{value: '{{ dd.mail|default(0) }}', formatted: '{{ dd.mail|formatBytes }}', axisv: '{{ d }}'},{% endfor %}],
@@ -283,7 +282,7 @@
plugins: { plugins: {
title: { title: {
display: true, display: true,
text: 'Specified by range' text: '{{ lng('traffic.byrange') }}'
}, },
tooltip: { tooltip: {
enabled: true, enabled: true,

View File

@@ -25,7 +25,7 @@
</button> </button>
</div> </div>
<a class="navbar-brand me-0 {% if block('heading') %}shadow-sm{% endif %}" href="{{ linker({'section': 'index'}) }}"> <a class="navbar-brand me-0 {% if block('heading') %}shadow-sm{% endif %}" href="{{ linker({'section': 'index'}) }}">
<img src="{{ header_logo }}" alt="" width="auto" height="24" class="d-inline-block align-text-top ms-md-3"> <img src="{{ header_logo }}" alt="logo" class="header-logo d-inline-block align-text-top ms-md-3">
</a> </a>
<div class="order-0 order-md-1 d-flex flex-grow-0 flex-md-grow-1" id="navbar"> <div class="order-0 order-md-1 d-flex flex-grow-0 flex-md-grow-1" id="navbar">
<ul class="navbar-nav ms-md-auto me-3 me-lg-5"> <ul class="navbar-nav ms-md-auto me-3 me-lg-5">