Merge remote-tracking branch 'origin/main' into customeremail-overview

This commit is contained in:
Michael Kaufmann
2023-01-22 14:03:07 +01:00
37 changed files with 500 additions and 143 deletions

View File

@@ -269,7 +269,8 @@ return [
'traffic' => lng('menue.traffic.traffic'),
'traffic.http' => lng('menue.traffic.traffic') . " / HTTP",
'traffic.ftp' => lng('menue.traffic.traffic') . " / FTP",
'traffic.mail' => lng('menue.traffic.traffic') . " / Mail"
'traffic.mail' => lng('menue.traffic.traffic') . " / Mail",
'misc.documentation' => lng('admin.documentation'),
],
'save_method' => 'storeSettingField',
'advanced_mode' => true

View File

@@ -241,6 +241,16 @@ return [
'type' => 'checkbox',
'default' => true,
'save_method' => 'storeSettingField'
],
'system_le_domain_dnscheck_resolver' => [
'label' => lng('serversettings.le_domain_dnscheck_resolver'),
'settinggroup' => 'system',
'varname' => 'le_domain_dnscheck_resolver',
'type' => 'text',
'string_regexp' => '/^(([0-9]+ [a-z0-9\-\._]+, ?)*[0-9]+ [a-z0-9\-\._]+)?$/i',
'string_emptyallowed' => true,
'default' => '',
'save_method' => 'storeSettingField'
]
]
]

View File

@@ -92,6 +92,7 @@ if ($userinfo['change_serversettings'] == '1') {
if ($distribution != "" && isset($_POST['finish'])) {
unset($_POST['finish']);
unset($_POST['csrf_token']);
$params = $_POST;
$params['distro'] = $distribution;
$params['system'] = [];
@@ -121,8 +122,6 @@ if ($userinfo['change_serversettings'] == '1') {
'distribution' => $distribution
]);
} else {
// @fixme check set distribution from settings
$cfg_formfield = [
'config' => [
'title' => lng('admin.configfiles.serverconfiguration'),

View File

@@ -52,7 +52,8 @@
"voku/anti-xss": "^4.1",
"twig/twig": "^3.3",
"erusev/parsedown": "^1.7",
"symfony/console": "^5.4"
"symfony/console": "^5.4",
"pear/net_dns2": "^1.5"
},
"require-dev": {
"phpunit/phpunit": "^9",

53
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f8370edea3c85bcb7b681926a1fff04e",
"content-hash": "41e7a3bc0e13b47c4f245334b113c3be",
"packages": [
{
"name": "erusev/parsedown",
@@ -198,6 +198,57 @@
],
"time": "2022-06-09T08:53:42+00:00"
},
{
"name": "pear/net_dns2",
"version": "v1.5.3",
"source": {
"type": "git",
"url": "https://github.com/mikepultz/netdns2.git",
"reference": "dc8053772132a855b8bb6193422a959995f3a773"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mikepultz/netdns2/zipball/dc8053772132a855b8bb6193422a959995f3a773",
"reference": "dc8053772132a855b8bb6193422a959995f3a773",
"shasum": ""
},
"require": {
"php": ">=5.4"
},
"require-dev": {
"phpunit/phpunit": "^9"
},
"type": "library",
"autoload": {
"psr-0": {
"Net_DNS2": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Mike Pultz",
"email": "mike@mikepultz.com",
"homepage": "https://mikepultz.com/",
"role": "lead"
}
],
"description": "Native PHP DNS Resolver and Updater Library",
"homepage": "https://netdns2.com/",
"keywords": [
"PEAR",
"dns",
"network"
],
"support": {
"issues": "https://github.com/mikepultz/netdns2/issues",
"source": "https://github.com/mikepultz/netdns2"
},
"time": "2022-11-28T19:16:31+00:00"
},
{
"name": "phpmailer/phpmailer",
"version": "v6.6.3",

View File

@@ -115,10 +115,14 @@ if ($page == 'overview') {
if ($usages) {
$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;
} else {
$userinfo['diskspace_bytes_used'] = 0;
$userinfo['total_bytes_used'] = 0;
$userinfo['mailspace_used'] = 0;
$userinfo['dbspace_used'] = 0;
}
UI::twig()->addGlobal('userinfo', $userinfo);

View File

@@ -642,7 +642,7 @@ opcache.validate_timestamps'),
('system', 'leprivatekey', 'unset'),
('system', 'lepublickey', 'unset'),
('system', 'letsencryptca', 'letsencrypt'),
('system', 'letsencryptchallengepath', '/var/www/froxlor'),
('system', 'letsencryptchallengepath', '/var/www/html/froxlor'),
('system', 'letsencryptkeysize', '4096'),
('system', 'letsencryptreuseold', 0),
('system', 'leenabled', '0'),
@@ -670,6 +670,7 @@ opcache.validate_timestamps'),
('system', 'leaccount', ''),
('system', 'nssextrausers', '1'),
('system', 'le_domain_dnscheck', '1'),
('system', 'le_domain_dnscheck_resolver', '1.1.1.1'),
('system', 'ssl_protocols', 'TLSv1.2'),
('system', 'tlsv13_cipher_list', ''),
('system', 'honorcipherorder', '0'),
@@ -696,7 +697,7 @@ opcache.validate_timestamps'),
('system', 'distribution', ''),
('system', 'update_channel', 'stable'),
('system', 'updatecheck_data', ''),
('system', 'update_notify_last', '2.0.7'),
('system', 'update_notify_last', '2.0.9'),
('system', 'traffictool', 'goaccess'),
('api', 'enabled', '0'),
('2fa', 'enabled', '1'),
@@ -740,8 +741,8 @@ opcache.validate_timestamps'),
('panel', 'logo_overridetheme', '0'),
('panel', 'logo_overridecustom', '0'),
('panel', 'settings_mode', '0'),
('panel', 'version', '2.0.7'),
('panel', 'db_version', '202212060');
('panel', 'version', '2.0.9'),
('panel', 'db_version', '202301180');
DROP TABLE IF EXISTS `panel_tasks`;

View File

@@ -38,10 +38,9 @@ if (!defined('_CRON_UPDATE')) {
// last 0.10.x release
if (Froxlor::isFroxlorVersion('0.10.38.3')) {
$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");
Database::query("DROP TABLE IF EXISTS `panel_sessions`;");
@@ -146,7 +145,7 @@ if (Froxlor::isFroxlorVersion('0.10.38.3')) {
}
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);
$system_distribution = isset($_POST['system_distribution']) ? $_POST['system_distribution'] : '';
Settings::AddNew("system.distribution", $system_distribution);
@@ -193,7 +192,6 @@ if (Froxlor::isFroxlorVersion('0.10.38.3')) {
}
if (Froxlor::isDatabaseVersion('202112310')) {
Update::showUpdateStep("Adjusting traffic tool settings");
$traffic_tool = Settings::Get('system.awstats_enabled') == 1 ? 'awstats' : 'webalizer';
Settings::AddNew("system.traffictool", $traffic_tool);
@@ -204,7 +202,6 @@ if (Froxlor::isDatabaseVersion('202112310')) {
}
if (Froxlor::isDatabaseVersion('202211030')) {
Update::showUpdateStep("Creating backward compatibility for cronjob");
$disabled = explode(',', ini_get('disable_functions'));
$exec_allowed = !in_array('exec', $disabled);
@@ -225,8 +222,8 @@ EOF;
file_put_contents($complete_filedir . '/froxlor_master_cronjob.php', $compCron);
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';
$cron_run_cmd = 'chmod +x ' . FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/bin/froxlor-cli') . PHP_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>');
}
@@ -268,8 +265,11 @@ if (Froxlor::isFroxlorVersion('2.0.3')) {
$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)
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");
$disabled = explode(',', ini_get('disable_functions'));
$exec_allowed = !in_array('exec', $disabled);
if ($exec_allowed) {
$newCronBin = Froxlor::getInstallDir() . '/bin/froxlor-cli';
$compCron = <<<EOF
<?php
@@ -280,8 +280,12 @@ exit;
EOF;
file_put_contents($complete_filedir . '/froxlor_master_cronjob.php', $compCron);
Update::lastStepStatus(0);
} else {
$cron_run_cmd = 'chmod +x ' . FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/bin/froxlor-cli') . PHP_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');
}
@@ -312,3 +316,64 @@ if (Froxlor::isFroxlorVersion('2.0.6')) {
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);
if ((int) Settings::Get('system.leenabled') == 1) {
// create JSON string for --apply
$dist = Settings::Get('system.distribution');
$webserver = Settings::Get('system.webserver');
if ($webserver == 'apache2') {
$webserver = 'apache22';
if (Settings::Get('system.apache24')) {
$webserver = 'apache24';
}
}
$apply_json = '{"http":"' . $webserver . '","dns":"x","smtp":"x","mail":"x","ftp":"x","distro":"' . $dist . '","system":[]}';
Update::lastStepStatus(1, 'manual commands needed',
"Please reconfigure webserver service using <pre>bin/froxlor-cli froxlor:config-services --apply='" . $apply_json . "'</pre>" .
'<br>or adjust the path manually in <pre>' . Settings::Get('system.letsencryptacmeconf') . '</pre>' .
'<br><br>In case you already have certificates issued, run the following command to validate and correct the webroot used for renewal:<br>' .
'<pre>bin/froxlor-cli froxlor:validate-acme-webroot</pre><br>'
);
} else {
Update::lastStepStatus(0);
}
} 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');
}
if (Froxlor::isDatabaseVersion('202301120')) {
Update::showUpdateStep("Adding new setting for DNS resolver when using Let's Encrypt");
$system_le_domain_dnscheck_resolver = isset($_POST['system_le_domain_dnscheck_resolver']) ? $_POST['system_le_domain_dnscheck_resolver'] : '1.1.1.1';
Settings::AddNew("system.le_domain_dnscheck_resolver", $system_le_domain_dnscheck_resolver);
Update::lastStepStatus(0);
Froxlor::updateToDbVersion('202301180');
}
if (Froxlor::isFroxlorVersion('2.0.8')) {
Update::showUpdateStep("Updating from 2.0.8 to 2.0.9", false);
Froxlor::updateToVersion('2.0.9');
}

View File

@@ -34,9 +34,14 @@ $return = [];
if (Update::versionInUpdate($current_db_version, '202004140')) {
$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).';
$return['system_le_domain_dnscheck_note'] = ['type' => 'infotext', 'value' => $description];
$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;

View File

@@ -27,6 +27,7 @@ use Froxlor\Froxlor;
use Froxlor\FileDir;
use Froxlor\Config\ConfigParser;
use Froxlor\Install\Update;
use Froxlor\Settings;
$preconfig = [
'title' => '2.x updates',
@@ -36,7 +37,6 @@ $return = [];
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.';
$return['panel_settings_mode_note'] = ['type' => 'infotext', 'value' => $description];
$question = '<strong>Chose settings mode (you can change that at any time)</strong>';
$return['panel_settings_mode'] = [
'type' => 'select',
@@ -45,11 +45,11 @@ if (Update::versionInUpdate($current_version, '2.0.0-beta1')) {
1 => 'Advanced'
],
'selected' => 1,
'label' => $question
'label' => $question,
'prior_infotext' => $description
];
$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>';
$config_dir = FileDir::makeCorrectDir(Froxlor::getInstallDir() . '/lib/configfiles/');
// show list of available distro's
@@ -68,9 +68,44 @@ if (Update::versionInUpdate($current_version, '2.0.0-beta1')) {
'type' => 'select',
'select_var' => $distributions_select,
'selected' => '',
'label' => $question
'label' => $question,
'prior_infotext' => $description
];
}
if (Update::versionInUpdate($current_db_version, '202301120')) {
$acmesh_challenge_dir = rtrim(FileDir::makeCorrectDir(Settings::Get('system.letsencryptchallengepath')), "/");
$recommended = rtrim(FileDir::makeCorrectDir(Froxlor::getInstallDir()), "/");
if ((int) Settings::Get('system.leenabled') == 1 && $acmesh_challenge_dir != $recommended) {
$has_preconfig = true;
$description = 'ACME challenge docroot from settings differs from the current installation directory.';
$question = '<strong>Validate Let\'s Encrypt challenge path (recommended value: ' . $recommended . ')</strong>';
$return['system_letsencryptchallengepath_upd'] = [
'type' => 'text',
'value' => $recommended,
'placeholder' => $acmesh_challenge_dir,
'label' => $question,
'prior_infotext' => $description,
'mandatory' => true,
];
}
}
if (Update::versionInUpdate($current_db_version, '202301180')) {
if ((int) Settings::Get('system.leenabled') == 1) {
$has_preconfig = true;
$description = 'Froxlor now supports to set an external DNS resolver for the Let\'s Encrypt pre-check.';
$question = '<strong>Specify a DNS resolver IP (recommended value: 1.1.1.1 or similar)</strong>';
$return['system_le_domain_dnscheck_resolver'] = [
'type' => 'text',
'pattern' => '^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$|^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$|^\s*$',
'value' => '1.1.1.1',
'placeholder' => '1.1.1.1',
'label' => $question,
'prior_infotext' => $description,
];
}
}
$preconfig['fields'] = $return;
return $preconfig;

View File

@@ -559,7 +559,7 @@ class Domains extends ApiCommand implements ResourceEntity
// validate dns if lets encrypt is enabled to check whether we can use it at all
if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') {
$domain_ips = PhpHelper::gethostbynamel6($domain);
$domain_ips = PhpHelper::gethostbynamel6($domain, true, Settings::Get('system.le_domain_dnscheck_resolver'));
$selected_ips = $this->getIpsFromIdArray($ssl_ipandports);
if ($domain_ips == false || count(array_intersect($selected_ips, $domain_ips)) <= 0) {
Response::standardError('invaliddnsforletsencrypt', '', true);
@@ -1523,7 +1523,7 @@ class Domains extends ApiCommand implements ResourceEntity
// validate dns if lets encrypt is enabled to check whether we can use it at all
if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') {
$domain_ips = PhpHelper::gethostbynamel6($result['domain']);
$domain_ips = PhpHelper::gethostbynamel6($result['domain'], true, Settings::Get('system.le_domain_dnscheck_resolver'));
$selected_ips = $this->getIpsFromIdArray($ssl_ipandports);
if ($domain_ips == false || count(array_intersect($selected_ips, $domain_ips)) <= 0) {
Response::standardError('invaliddnsforletsencrypt', '', true);

View File

@@ -262,7 +262,7 @@ class SubDomains extends ApiCommand implements ResourceEntity
// validate dns if lets encrypt is enabled to check whether we can use it at all
if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') {
$our_ips = Domain::getIpsOfDomain($domain_check['id']);
$domain_ips = PhpHelper::gethostbynamel6($completedomain);
$domain_ips = PhpHelper::gethostbynamel6($completedomain, true, Settings::Get('system.le_domain_dnscheck_resolver'));
if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) {
Response::standardError('invaliddnsforletsencrypt', '', true);
}
@@ -738,7 +738,7 @@ class SubDomains extends ApiCommand implements ResourceEntity
// validate dns if lets encrypt is enabled to check whether we can use it at all
if ($result['letsencrypt'] != $letsencrypt && $letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') {
$our_ips = Domain::getIpsOfDomain($result['parentdomainid']);
$domain_ips = PhpHelper::gethostbynamel6($result['domain']);
$domain_ips = PhpHelper::gethostbynamel6($result['domain'], true, Settings::Get('system.le_domain_dnscheck_resolver'));
if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) {
Response::standardError('invaliddnsforletsencrypt', '', true);
}

View File

@@ -44,7 +44,7 @@ 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->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');
}
@@ -56,6 +56,11 @@ final class ValidateAcmeWebroot extends CliCommand
$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');
@@ -64,9 +69,37 @@ final class ValidateAcmeWebroot extends CliCommand
$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);
// check for froxlor-vhost
if (Settings::Get('system.le_froxlor_enabled') == '1') {
$domains[] = [
'id' => 0,
'domain' => Settings::Get('system.hostname')
];
}
$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');
$acmesh_challenge_dir = rtrim(FileDir::makeCorrectDir(Settings::Get('system.letsencryptchallengepath')), "/");
$recommended = rtrim(FileDir::makeCorrectDir(Froxlor::getInstallDir()), "/");
if ($acmesh_challenge_dir != $recommended) {
$io->warning([
"ACME challenge docroot from settings differs from the current installation directory.",
"Settings: '" . $acmesh_challenge_dir . "'",
"Default/recommended value: '" . $recommended . "'",
]);
$question = new ConfirmationQuestion('Fix ACME challenge docroot setting? [yes] ', true, '/^(y|j)/i');
if ($yestoall || $helper->ask($input, $output, $question)) {
Settings::Set('system.letsencryptchallengepath', $recommended);
$former_value = $acmesh_challenge_dir;
$acmesh_challenge_dir = $recommended;
// 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');
@@ -113,6 +146,7 @@ final class ValidateAcmeWebroot extends CliCommand
}
if ($count_changes > 0) {
if (Froxlor::hasUpdates() || Froxlor::hasDbUpdates()) {
$io->info("Changes detected but froxlor has been updated. Inserting task to rebuild vhosts after update.");
Cronjob::inserttask(TaskId::REBUILD_VHOST);
} else {
$question = new ConfirmationQuestion('Changes detected. Force cronjob to refresh certificates? [yes] ', true, '/^(y|j)/i');
@@ -120,6 +154,8 @@ final class ValidateAcmeWebroot extends CliCommand
passthru(FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/bin/froxlor-cli') . ' froxlor:cron -f -d');
}
}
} else {
$io->success("No changes necessary.");
}
}

View File

@@ -521,7 +521,7 @@ EOC;
foreach ($loop_domains as $idx => $domain) {
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Validating DNS of " . $domain);
// ips according to NS
$domain_ips = PhpHelper::gethostbynamel6($domain);
$domain_ips = PhpHelper::gethostbynamel6($domain, true, Settings::Get('system.le_domain_dnscheck_resolver'));
if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) {
// no common ips...
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $domain . " due to no system known IP address via DNS check");
@@ -557,7 +557,7 @@ EOC;
if (Settings::Get('system.letsencryptreuseold') != '1') {
$acmesh_cmd .= " --always-force-new-domain-key";
}
if (Settings::Get('system.letsencryptca') == 'letsencrypt_test') {
if (substr(Settings::Get('system.letsencryptca'), -5) == '_test') {
$acmesh_cmd .= " --staging";
}
if ($force) {

View File

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

View File

@@ -100,11 +100,17 @@ class FroxlorLogger
self::$ml->pushHandler(new SyslogHandler('froxlor', LOG_USER, Logger::DEBUG));
break;
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
@touch($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));
break;

View File

@@ -101,7 +101,7 @@ class Preconfig
$agree = [
'title' => 'Check',
'fields' => [
'update_changesagreed' => ['type' => 'checkbox', 'value' => 1, 'label' => '<strong>I have read the update notifications above and I am aware of the changes made to my system.</strong>'],
'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]
]
];

View File

@@ -27,6 +27,8 @@ namespace Froxlor;
use Exception;
use Froxlor\UI\Panel\UI;
use Net_DNS2_Exception;
use Net_DNS2_Resolver;
use Throwable;
use voku\helper\AntiXSS;
@@ -244,45 +246,60 @@ class PhpHelper
* ipv6 aware gethostbynamel function
*
* @param string $host
* @param boolean $try_a
* default true
* @param boolean $try_a default true
* @param string|null $nameserver set additional resolver nameserver to use (e.g. 1.1.1.1)
* @return boolean|array
*/
public static function gethostbynamel6($host, $try_a = true)
public static function gethostbynamel6(string $host, bool $try_a = true, string $nameserver = null)
{
$dns6 = @dns_get_record($host, DNS_AAAA);
if (!is_array($dns6)) {
// no record or failed to check
$dns6 = [];
}
if ($try_a == true) {
$dns4 = @dns_get_record($host, DNS_A);
if (!is_array($dns4)) {
// no record or failed to check
$dns4 = [];
}
$dns = array_merge($dns4, $dns6);
} else {
$dns = $dns6;
}
$ips = [];
foreach ($dns as $record) {
if ($record["type"] == "A") {
// always use compressed ipv6 format
$ip = inet_ntop(inet_pton($record["ip"]));
$ips[] = $ip;
try {
// set the default nameservers to use, use the system default if none are provided
$resolver = new Net_DNS2_Resolver($nameserver ? ['nameservers' => [$nameserver]] : []);
// get all ip addresses from the A record and normalize them
if ($try_a) {
try {
$answer = $resolver->query($host, 'A')->answer;
foreach ($answer as $rr) {
$ips[] = inet_ntop(inet_pton($rr->address));
}
if ($record["type"] == "AAAA") {
// always use compressed ipv6 format
$ip = inet_ntop(inet_pton($record["ipv6"]));
$ips[] = $ip;
} catch (Net_DNS2_Exception $e) {
// we can't do anything here, just continue
}
}
if (count($ips) < 1) {
return false;
} else {
return $ips;
// get all ip addresses from the AAAA record and normalize them
try {
$answer = $resolver->query($host, 'AAAA')->answer;
foreach ($answer as $rr) {
$ips[] = inet_ntop(inet_pton($rr->address));
}
} catch (Net_DNS2_Exception $e) {
// we can't do anything here, just continue
}
} catch (Net_DNS2_Exception $e) {
// fallback to php's dns_get_record if Net_DNS2 has no resolver available, but this may cause
// problems if the system's dns is not configured correctly; for example, the acme pre-check
// will fail because some providers put a local ip in /etc/hosts
// get all ip addresses from the A record and normalize them
if ($try_a) {
$answer = @dns_get_record($host, DNS_A);
foreach ($answer as $rr) {
$ips[] = inet_ntop(inet_pton($rr['ip']));
}
}
// get all ip addresses from the AAAA record and normalize them
$answer = @dns_get_record($host, DNS_AAAA);
foreach ($answer as $rr) {
$ips[] = inet_ntop(inet_pton($rr['ipv6']));
}
}
return count($ips) > 0 ? $ips : false;
}
/**

View File

@@ -87,6 +87,10 @@ class FroxlorTwig extends AbstractExtension
new TwigFunction('linker', [
$this,
'getLink'
]),
new TwigFunction('mix', [
$this,
'getMix'
])
];
}
@@ -158,4 +162,9 @@ class FroxlorTwig extends AbstractExtension
{
return 'froxlortwig';
}
public function getMix($mix = '')
{
return mix($mix);
}
}

View File

@@ -4,5 +4,11 @@
* change the options below to either true or false
*/
return [
'enable_webupdate' => false
/**
* enable/disable the possibility to update froxlor from within the web-interface,
* recommended value for debian/ubuntu package users is false to rely on apt and not have version mixup.
* This is also useful for providers that manage the servers but give admin access to froxlor to handle
* updates the way the providers does it (e.g. automation, etc.)
*/
'enable_webupdate' => false,
];

View File

@@ -3458,11 +3458,7 @@ ssl_key = <<SSL_KEY_FILE>
# auth_ssl_username_from_cert=yes.
#ssl_cert_username_field = commonName
# SSL DH parameters
# 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_dh_parameters_length = 2048
# SSL protocols to use
#ssl_protocols = !SSLv3

View File

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

View File

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

View File

@@ -26,6 +26,14 @@
use Froxlor\Language;
use Froxlor\UI\Request;
/**
* Render a template with the given data.
* Mostly used if we have no template-engine (twig).
*
* @param $template
* @param $attributes
* @return array|false|string|string[]
*/
function view($template, $attributes)
{
$view = file_get_contents(dirname(__DIR__) . '/templates/' . $template);
@@ -33,11 +41,26 @@ function view($template, $attributes)
return str_replace(array_keys($attributes), array_values($attributes), $view);
}
/**
* Get the current translation for a given string.
*
* @param string $identifier
* @param array $arguments
* @return array|string
*/
function lng(string $identifier, array $arguments = [])
{
return Language::getTranslation($identifier, $arguments);
}
/**
* Get the value of a request variable.
*
* @param string $identifier
* @param string|null $default
* @param string|null $session
* @return mixed|string|null
*/
function old(string $identifier, string $default = null, string $session = null)
{
if ($session && isset($_SESSION[$session])) {
@@ -45,3 +68,26 @@ function old(string $identifier, string $default = null, string $session = null)
}
return Request::any($identifier, $default);
}
/**
* Loading the mix manifest file from given theme.
* This file contains the hashed filenames of the assets.
* It must be always placed in the theme assets folder.
*
* @param $filename
* @return mixed|string
*/
function mix($filename)
{
if (preg_match('/templates\/(.+)\/assets\/(.+)\/(.+)/', $filename, $matches)) {
$mixManifest = dirname(__DIR__) . '/templates/' . $matches[1] . '/assets/mix-manifest.json';
if (file_exists($mixManifest)) {
$manifest = json_decode(file_get_contents($mixManifest), true);
$key = '/' . $matches[2] . '/' . $matches[3];
if ($manifest && !empty($manifest[$key])) {
$filename = 'templates/' . $matches[1] . '/assets' . $manifest[$key];
}
}
}
return $filename;
}

View File

@@ -277,14 +277,14 @@ if (is_array($_themeoptions) && array_key_exists('js', $_themeoptions['variants'
if (is_array($_themeoptions['variants'][$themevariant]['js'])) {
foreach ($_themeoptions['variants'][$themevariant]['js'] as $jsfile) {
if (file_exists('templates/' . $theme . '/assets/js/' . $jsfile)) {
$js .= '<script type="text/javascript" src="templates/' . $theme . '/assets/js/' . $jsfile . '"></script>' . "\n";
$js .= '<script type="text/javascript" src="' . mix('templates/' . $theme . '/assets/js/' . $jsfile) . '"></script>' . "\n";
}
}
}
if (is_array($_themeoptions['variants'][$themevariant]['css'])) {
foreach ($_themeoptions['variants'][$themevariant]['css'] as $cssfile) {
if (file_exists('templates/' . $theme . '/assets/css/' . $cssfile)) {
$css .= '<link href="templates/' . $theme . '/assets/css/' . $cssfile . '" rel="stylesheet" type="text/css" />' . "\n";
$css .= '<link href="' . mix('templates/' . $theme . '/assets/css/' . $cssfile) . '" rel="stylesheet" type="text/css" />' . "\n";
}
}
}

View File

@@ -158,6 +158,7 @@ return [
'docs' => [
'label' => lng('admin.documentation'),
'icon' => 'fa-solid fa-circle-info',
'show_element' => (!Settings::IsInList('panel.customer_hide_options', 'misc.documentation')),
'elements' => [
[
'url' => 'https://docs.froxlor.org/v2/user-guide/',

View File

@@ -1494,7 +1494,10 @@ Vielen Dank, Ihr Administrator',
'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',
],
'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',
'logcronoption' => [
'never' => 'Nie',
@@ -1962,6 +1965,10 @@ Vielen Dank, Ihr Administrator',
'title' => 'Validiere DNS der Domains wenn Let\'s Encrypt genutzt wird',
'description' => 'Wenn aktiviert wird froxlor überprüfen ob die DNS Einträge der Domains, welche ein Let\'s Encrypt Zertifikat beantragt, mindestens auf eine der System IP Adressen auflöst.',
],
'le_domain_dnscheck_resolver' => [
'title' => 'DNS Resolver für die DNS Überprüfung',
'description' => 'IP Adresse des DNS Servers, welcher für die DNS Überprüfung genutzt werden soll. Wenn leer, wird der Standard DNS Resolver des Systems genutzt.',
],
'phpsettingsforsubdomains' => [
'description' => 'Wenn ja, wird die gewählte PHP-Config für alle Subdomains übernommen',
],
@@ -2124,17 +2131,31 @@ Vielen Dank, Ihr Administrator',
],
'mb' => 'Traffic',
'day' => 'Tag',
'distribution' => '<span color="#019522">FTP</span> | <span color="#0000FF">HTTP</span> | <span color="#800000">Mail</span>',
'sumhttp' => 'Gesamt HTTP-Traffic',
'sumftp' => 'Gesamt FTP-Traffic',
'summail' => 'Gesamt Mail-Traffic',
'sumtotal' => 'Gesamt Traffic',
'sumhttp' => 'HTTP-Traffic',
'sumftp' => 'FTP-Traffic',
'summail' => 'Mail-Traffic',
'customer' => 'Kunde',
'trafficoverview' => 'Übersicht Traffic je',
'trafficoverview' => 'Übersicht Traffic',
'bycustomers' => 'Traffic nach Kunden',
'details' => 'Details',
'http' => 'HTTP',
'ftp' => 'FTP',
'mail' => 'Mail',
'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' => '',
'update' => [

View File

@@ -1613,7 +1613,10 @@ Yours sincerely, your administrator',
'title' => 'Log-type(s)',
'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',
'logcronoption' => [
'never' => 'Never',
@@ -2081,6 +2084,10 @@ Yours sincerely, your administrator',
'title' => 'Validate DNS of domains when using Let\'s Encrypt',
'description' => 'If activated, froxlor will validate whether the domain which requests a Let\'s Encrypt certificate resolves to at least one of the system ip addresses.',
],
'le_domain_dnscheck_resolver' => [
'title' => 'Use a external nameserver for DNS validation',
'description' => 'If set, froxlor will use this DNS to validate the DNS of domains when using Let\'s Encrypt. If empty, the system\'s default DNS resolver will be used.',
],
'phpsettingsforsubdomains' => [
'description' => 'If yes the chosen php-config will be updated to all subdomains',
],
@@ -2254,18 +2261,32 @@ Yours sincerely, your administrator',
'total' => 'Total',
],
'mb' => 'Traffic',
'distribution' => '<font color="#019522">FTP</font> | <font color="#0000FF">HTTP</font> | <font color="#800000">Mail</font>',
'sumhttp' => 'Total HTTP-Traffic',
'sumftp' => 'Total FTP-Traffic',
'summail' => 'Total Mail-Traffic',
'sumtotal' => 'Total traffic',
'sumhttp' => 'HTTP traffic',
'sumftp' => 'FTP traffic',
'summail' => 'Mail traffic',
'customer' => 'Customer',
'domain' => 'Domain',
'trafficoverview' => 'Traffic summary by',
'trafficoverview' => 'Traffic summary',
'bycustomers' => 'Traffic by customers',
'details' => 'Details',
'http' => 'HTTP',
'ftp' => 'FTP',
'mail' => 'Mail',
'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' => '',
'update' => [

View File

@@ -11,7 +11,7 @@
<!-- CSS -->
{% if theme_css is empty %}
<link href="{{ basehref|default('') }}templates/Froxlor/assets/css/main.css" rel="stylesheet" type="text/css" />
<link href="{{ basehref|default('') }}{{ mix('templates/Froxlor/assets/css/main.css') }}" rel="stylesheet" type="text/css" />
{% else %}
{{ theme_css|raw }}
{% endif %}
@@ -19,7 +19,7 @@
<!-- Scripts -->
{% if theme_js is empty %}
<script type="text/javascript" src="{{ basehref|default('') }}templates/Froxlor/assets/js/main.js"></script>
<script type="text/javascript" src="{{ basehref|default('') }}{{ mix('templates/Froxlor/assets/js/main.js') }}"></script>
{% else %}
{{ theme_js|raw }}
{% endif %}

View File

@@ -50,7 +50,7 @@
</form>
{# 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>
{% endif %}
{% 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 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">
{% if field.prior_infotext is defined and field.prior_infotext|length > 0 %}
<h5>{{ field.prior_infotext }}</h5>
{% endif %}
{% if field.label is iterable %}
<label for="{{ id }}" class="col-sm-6 col-form-label pe-3">
{% if em %}
@@ -51,6 +54,8 @@
{{ _self.input_ul(id, field) }}
{% elseif field.type == 'checkbox' %}
{{ _self.bool(id, field) }}
{% elseif field.type == 'checkrequired' %}
{{ _self.chk_required(id, field) }}
{% elseif field.type == 'select' %}
{{ _self.select(id, field) }}
{% elseif field.type == 'textarea' %}
@@ -116,6 +121,12 @@
{% endif %}
{% endmacro %}
{% macro chk_required(id, field) %}
<div class="form-check form-switch">
<input type="checkbox" value="{{ field.value }}" id="{{ id }}" name="{{ id }}" class="form-check-input" {% if field.mandatory is defined and field.mandatory == 1 %} required {% endif %} />
</div>
{% endmacro %}
{% macro infotext(id, field) %}
{% if field.next_to is defined %}
<div class="input-group">
@@ -148,7 +159,7 @@
{% if field.next_to is defined %}
<div class="input-group">
{% endif %}
<input type="{{ field.type }}" {% if field.visible is defined and field.visible == false %} 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 %}/>
<input type="{{ field.type }}" {% if field.visible is defined and field.visible == false %} 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 %}/>
{% if field.type == 'hidden' and field.display is defined %}
<input type="text" readonly class="form-control-plaintext" value="{{ field.display|raw }}">
{% endif %}
@@ -165,7 +176,7 @@
{% macro image(id, field) %}
{% 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">
<input type="checkbox" value="1" name="{{ id }}_delete" class="form-check-input">
<label class="form-check-label">

View File

@@ -112,7 +112,6 @@
{{ lng('admin.configfiles.recommendednote') }}
</div>
<div class="col-12 col-md-6 text-end">
<input type="hidden" name="dist" value="{{ distribution }}"/>
<button type="button" class="btn btn-outline-secondary" id="selectRecommendedConfig">{{ lng('admin.configfiles.selectrecommended') }}</button>
<button type="button" class="btn btn-outline-secondary" id="downloadSelectionAsJson">
<i class="fa-solid fa-download"></i>

View File

@@ -1,19 +1,19 @@
$(document).ready(function() {
$('#customer_add,#customer_edit').each(function(){
$(document).ready(function () {
$('#customer_add,#customer_edit').each(function () {
$(this).validate({
rules:{
'name':{
required:function(){
rules: {
'name': {
required: function () {
return $('#company').val().length === 0 || $('#firstname').val().length > 0;
}
},
'firstname':{
required:function(){
'firstname': {
required: function () {
return $('#company').val().length === 0 || $('#name').val().length > 0;
}
},
'company':{
required:function(){
'company': {
required: function () {
return $('#name').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

@@ -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="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">
<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>

View File

@@ -33,12 +33,13 @@
{% else %}
{# customer-resources #}
<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.traffic', userinfo.traffic_bytes, userinfo.traffic_bytes_used, null, true) }}
{{ 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.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.ftps', userinfo.ftps, userinfo.ftps_used) }}
</div>

View File

@@ -18,14 +18,14 @@
<!-- TODO: set url on change. e.g.: ?param=days:7 -->
<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'}) }}">
<option value="hours:24" {% if range == 'hours:24' %}selected{% endif %}>last 24 hours</option>
<option value="days:7" {% if range == 'days:7' %}selected{% endif %}>last 7 days</option>
<option value="days:30" {% if range == 'days:30' %}selected{% endif %}>last 30 days</option>
<option value="currentmonth" {% if range == 'currentmonth' %}selected{% endif %}>current month</option>
<option value="months:3" {% if range == 'months:3' %}selected{% endif %}>last 3 months</option>
<option value="months:6" {% if range == 'months:6' %}selected{% endif %}>last 6 months</option>
<option value="months:12" {% if range == 'months:12' %}selected{% endif %}>last 12 months</option>
<option value="currentyear" {% if range == 'currentyear' %}selected{% endif %}>current year</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 %}>{{ lng('traffic.ranges.last7d') }}</option>
<option value="days:30" {% if range == 'days:30' %}selected{% endif %}>{{ lng('traffic.ranges.last30d') }}</option>
<option value="currentmonth" {% if range == 'currentmonth' %}selected{% endif %}>{{ lng('traffic.ranges.cm') }}</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 %}>{{ lng('traffic.ranges.last6m') }}</option>
<option value="months:12" {% if range == 'months:12' %}selected{% endif %}>{{ lng('traffic.ranges.last12m') }}</option>
<option value="currentyear" {% if range == 'currentyear' %}selected{% endif %}>{{ lng('traffic.ranges.cy') }}</option>
{% for yd in years_avail %}
{% if yd.year != "now"|date('Y') %}
<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="col p-3 border-end">
<h3>{{ metrics.total|formatBytes }}</h3>
<span>Total</span>
<span>{{ lng('traffic.months.total') }}</span>
</div>
<div class="col p-3 border-end">
<h3>{{ metrics.http|formatBytes }}</h3>
<span>HTTP</span>
<span>{{ lng('traffic.http') }}</span>
</div>
<div class="col p-3 border-end">
<h3>{{ metrics.ftp|formatBytes }}</h3>
<span>FTP</span>
<span>{{ lng('traffic.ftp') }}</span>
</div>
<div class="col p-3 border-end">
<h3>{{ metrics.mail|formatBytes }}</h3>
<span>Mail</span>
<span>{{ lng('traffic.mail') }}</span>
</div>
</div>
</div>
{% if userinfo.adminsession == 1 %}
<!-- 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 %}
<div class="card table-responsive">
<table class="table table-borderless table-striped align-middle mb-0 px-3">
<thead>
<tr>
<th scope="col">{{ lng('login.username') }}</th>
<th scope="col">Total</th>
<th scope="col">HTTP</th>
<th scope="col">FTP</th>
<th scope="col">Mail
</th>
<th scope="col">{{ lng('traffic.months.total') }}</th>
<th scope="col">{{ lng('traffic.http') }}</th>
<th scope="col">{{ lng('traffic.ftp') }}</th>
<th scope="col">{{ lng('traffic.mail') }}</th>
</tr>
</thead>
<tbody>
@@ -101,19 +100,19 @@
{% else %}
<div class="card">
<div class="card-body">
<p>No data for given range found.</p>
<p>{{ lng('traffic.nodata') }}</p>
</div>
</div>
{% endif %}
{% endif %}
<script>
const labelsS = ['HTTP', 'FTP', 'Mail'];
const labelsS = ['{{ lng('traffic.http') }}', '{{ lng('traffic.ftp') }}', '{{ lng('traffic.mail') }}'];
const dataS = {
labels: labelsS,
datasets: [{
label: 'Traffic summary',
label: '{{ lng('traffic.trafficoverview') }}',
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 }}'}]
}]
@@ -130,7 +129,7 @@
plugins: {
title: {
display: true,
text: 'Total traffic'
text: '{{ lng('traffic.sumtotal') }}'
},
legend: {
position: 'right'
@@ -161,7 +160,7 @@
const dataC = {
labels: labelsC,
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)'],
data: dataValues
}]
@@ -178,7 +177,7 @@
plugins: {
title: {
display: true,
text: 'Top 5 customers'
text: '{{ lng('traffic.top5customers') }}'
},
legend: {
position: 'right'
@@ -220,7 +219,7 @@
labels: labelsC,
datasets: [
{
label: 'HTTP traffic',
label: '{{ lng('traffic.sumhttp') }}',
backgroundColor: 'rgb(255, 99, 132)',
{% 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 %}],
@@ -234,7 +233,7 @@
}
},
{
label: 'FTP traffic',
label: '{{ lng('traffic.sumftp') }}',
backgroundColor: 'rgb(200, 199, 132)',
{% 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 %}],
@@ -248,7 +247,7 @@
}
},
{
label: 'Mail traffic',
label: '{{ lng('traffic.summail') }}',
backgroundColor: 'rgb(255, 99, 0)',
{% 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 %}],
@@ -283,7 +282,7 @@
plugins: {
title: {
display: true,
text: 'Specified by range'
text: '{{ lng('traffic.byrange') }}'
},
tooltip: {
enabled: true,

View File

@@ -10,4 +10,5 @@ mix
.copyDirectory('node_modules/@fortawesome/fontawesome-free/webfonts', 'templates/Froxlor/assets/webfonts')
.js('templates/Froxlor/src/js/main.js', 'js')
.sass('templates/Froxlor/src/scss/main.scss', 'css')
.sass('templates/Froxlor/src/scss/dark.scss', 'css');
.sass('templates/Froxlor/src/scss/dark.scss', 'css')
.version();