Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5bece64ce | ||
|
|
0034681412 | ||
|
|
bd5b99dc1c | ||
|
|
2feb802094 | ||
|
|
7b08a71c59 | ||
|
|
2a84e9c120 | ||
|
|
d854e8e991 | ||
|
|
0a363910d6 | ||
|
|
b23d5cd909 | ||
|
|
3b753aa69d | ||
|
|
492cd288bc | ||
|
|
47938c5082 | ||
|
|
d090e48544 | ||
|
|
314e4407a0 | ||
|
|
dff7530cc5 | ||
|
|
19423c9644 | ||
|
|
42b3f1e59d | ||
|
|
1b77632fa8 | ||
|
|
867b7b1390 | ||
|
|
4c6ebde58c | ||
|
|
1e013d9e9a | ||
|
|
c56bc651b9 | ||
|
|
6cbdf45a7c | ||
|
|
715667e227 | ||
|
|
41de161555 | ||
|
|
1f1ea370c0 |
@@ -57,7 +57,7 @@ May be found in [COPYING](COPYING)
|
||||
### Tarball
|
||||
https://files.froxlor.org/releases/froxlor-latest.tar.gz [MD5](https://files.froxlor.org/releases/froxlor-latest.tar.gz.md5) [SHA1](https://files.froxlor.org/releases/froxlor-latest.tar.gz.sha1)
|
||||
|
||||
### Debian / Ubutnu repository
|
||||
### Debian / Ubuntu repository
|
||||
|
||||
[HowTo](https://docs.froxlor.org/latest/general/installation/apt-package.html)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -253,6 +253,9 @@ if ($action == '') {
|
||||
if (isset($_POST['prepare']) && $_POST['prepare'] == 'prepare') {
|
||||
// email templates
|
||||
$language = htmlentities(Validate::validate($_POST['language'], 'language', '/^[^\r\n\0"\']+$/', 'nolanguageselect'));
|
||||
if (!array_key_exists($language, $languages)) {
|
||||
Response::standardError('templatelanguageinvalid');
|
||||
}
|
||||
$template = Validate::validate($_POST['template'], 'template');
|
||||
|
||||
$result_stmt = Database::prepare("
|
||||
@@ -288,6 +291,9 @@ if ($action == '') {
|
||||
} elseif (isset($_POST['send']) && $_POST['send'] == 'send' && !isset($_POST['filesend'])) {
|
||||
// email templates
|
||||
$language = htmlentities(Validate::validate($_POST['language'], 'language', '/^[^\r\n\0"\']+$/', 'nolanguageselect'));
|
||||
if (!array_key_exists($language, $languages)) {
|
||||
Response::standardError('templatelanguageinvalid');
|
||||
}
|
||||
$template = Validate::validate($_POST['template'], 'template');
|
||||
$subject = Validate::validate($_POST['subject'], 'subject', '/^[^\r\n\0]+$/', 'nosubjectcreate');
|
||||
$mailbody = Validate::validate($_POST['mailbody'], 'mailbody', '/^[^\0]+$/', 'nomailbodycreate');
|
||||
|
||||
@@ -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
53
composer.lock
generated
@@ -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",
|
||||
|
||||
@@ -33,6 +33,7 @@ use Froxlor\Froxlor;
|
||||
use Froxlor\UI\Panel\UI;
|
||||
use Froxlor\UI\Request;
|
||||
use Froxlor\UI\Response;
|
||||
use Froxlor\Database\Database;
|
||||
|
||||
// This file is being included in admin_domains and customer_domains
|
||||
// and therefore does not need to require lib/init.php
|
||||
|
||||
@@ -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.8'),
|
||||
('system', 'update_notify_last', '2.0.10'),
|
||||
('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.8'),
|
||||
('panel', 'db_version', '202301120');
|
||||
('panel', 'version', '2.0.10'),
|
||||
('panel', 'db_version', '202301180');
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `panel_tasks`;
|
||||
|
||||
@@ -222,7 +222,7 @@ 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 = '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>');
|
||||
}
|
||||
@@ -281,7 +281,7 @@ 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 = '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>');
|
||||
}
|
||||
@@ -323,7 +323,26 @@ if (Froxlor::isDatabaseVersion('202212060')) {
|
||||
$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>');
|
||||
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);
|
||||
}
|
||||
@@ -344,3 +363,22 @@ if (Froxlor::isFroxlorVersion('2.0.7')) {
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
if (Froxlor::isFroxlorVersion('2.0.9')) {
|
||||
Update::showUpdateStep("Updating from 2.0.9 to 2.0.10", false);
|
||||
Froxlor::updateToVersion('2.0.10');
|
||||
}
|
||||
|
||||
@@ -74,17 +74,35 @@ if (Update::versionInUpdate($current_version, '2.0.0-beta1')) {
|
||||
}
|
||||
|
||||
if (Update::versionInUpdate($current_db_version, '202301120')) {
|
||||
$acmesh_challenge_dir = Settings::Get('system.letsencryptchallengepath');
|
||||
if ($acmesh_challenge_dir != Froxlor::getInstallDir()) {
|
||||
$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 ';
|
||||
$question = '<strong>Validate Let\'s Encrypt challenge path (recommended value: ' . $recommended . ')</strong>';
|
||||
$return['system_letsencryptchallengepath_upd'] = [
|
||||
'type' => 'text',
|
||||
'value' => $acmesh_challenge_dir,
|
||||
'placeholder' => Froxlor::getInstallDir(),
|
||||
'value' => $recommended,
|
||||
'placeholder' => $acmesh_challenge_dir,
|
||||
'label' => $question,
|
||||
'prior_infotext' => $description
|
||||
'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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ class Ajax
|
||||
$content = preg_replace("/[\r\n]+/", " ", strip_tags($item->description));
|
||||
$content = substr($content, 0, 150) . "...";
|
||||
|
||||
$items .= UI::twig()->render($this->theme . '/user/newsfeeditem.html.twig', [
|
||||
$items .= UI::twig()->render(UI::validateThemeTemplate('/user/newsfeeditem.html.twig', $this->theme), [
|
||||
'link' => $link,
|
||||
'title' => $title,
|
||||
'date' => $date,
|
||||
@@ -201,7 +201,7 @@ class Ajax
|
||||
$result['last_update_check'] = $uc_data['ts'];
|
||||
$result['channel'] = Settings::Get('system.update_channel');
|
||||
|
||||
$result_rendered = UI::twig()->render($this->theme . '/misc/version_top.html.twig', $result);
|
||||
$result_rendered = UI::twig()->render(UI::validateThemeTemplate('/misc/version_top.html.twig', $this->theme), $result);
|
||||
return $this->jsonResponse($result_rendered);
|
||||
} catch (Exception $e) {
|
||||
// don't display anything if just not allowed due to permissions
|
||||
|
||||
@@ -117,6 +117,6 @@ class Api
|
||||
|
||||
private function stripcslashesDeep($value)
|
||||
{
|
||||
return is_array($value) ? array_map([$this, 'stripcslashesDeep'], $value) : stripcslashes($value);
|
||||
return is_array($value) ? array_map([$this, 'stripcslashesDeep'], $value) : (!empty($value) ? stripcslashes($value) : null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ use Froxlor\Cron\TaskId;
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\FroxlorLogger;
|
||||
use Froxlor\System\Cronjob;
|
||||
use Froxlor\UI\Response;
|
||||
use Froxlor\Validate\Validate;
|
||||
use PDO;
|
||||
|
||||
@@ -41,6 +42,14 @@ use PDO;
|
||||
class Cronjobs extends ApiCommand implements ResourceEntity
|
||||
{
|
||||
|
||||
private array $allowed_intervals = [
|
||||
'MINUTE',
|
||||
'HOUR',
|
||||
'DAY',
|
||||
'WEEK',
|
||||
'MONTH'
|
||||
];
|
||||
|
||||
/**
|
||||
* You cannot add new cronjobs yet.
|
||||
*/
|
||||
@@ -118,6 +127,10 @@ class Cronjobs extends ApiCommand implements ResourceEntity
|
||||
$interval_value = Validate::validate($interval_value, 'interval_value', '/^([0-9]+)$/Di', 'stringisempty', [], true);
|
||||
$interval_interval = Validate::validate($interval_interval, 'interval_interval', '', '', [], true);
|
||||
|
||||
if (!in_array(strtoupper($interval_interval), $this->allowed_intervals)) {
|
||||
Response::standardError('invalidcronjobintervalvalue', implode(", ", $this->allowed_intervals), true);
|
||||
}
|
||||
|
||||
// put together interval value
|
||||
$interval = $interval_value . ' ' . strtoupper($interval_interval);
|
||||
|
||||
|
||||
@@ -157,16 +157,15 @@ class DirOptions extends ApiCommand implements ResourceEntity
|
||||
* this functions validates a given value as ErrorDocument
|
||||
* refs #267
|
||||
*
|
||||
* @param
|
||||
* string error-document-string
|
||||
* @param string $errdoc
|
||||
* @param bool $throw_exception
|
||||
*
|
||||
* @return string error-document-string
|
||||
*
|
||||
*/
|
||||
private function correctErrorDocument($errdoc = null, $throw_exception = false)
|
||||
private function correctErrorDocument(string $errdoc, $throw_exception = false)
|
||||
{
|
||||
if ($errdoc !== null && $errdoc != '') {
|
||||
if (trim($errdoc) != '') {
|
||||
// not a URL
|
||||
if ((strtoupper(substr($errdoc, 0, 5)) != 'HTTP:' && strtoupper(substr($errdoc, 0, 6)) != 'HTTPS:') || !Validate::validateUrl($errdoc)) {
|
||||
// a file
|
||||
@@ -176,14 +175,14 @@ class DirOptions extends ApiCommand implements ResourceEntity
|
||||
if (!substr($errdoc, 0, 1) == '/') {
|
||||
$errdoc = '/' . $errdoc;
|
||||
}
|
||||
} else {
|
||||
} elseif (preg_match('/^"([^\r\n\t\f\0"]+)"$/', $errdoc)) {
|
||||
// a string (check for ending ")
|
||||
// string won't work for lighty
|
||||
if (Settings::Get('system.webserver') == 'lighttpd') {
|
||||
Response::standardError('stringerrordocumentnotvalidforlighty', '', $throw_exception);
|
||||
} elseif (substr($errdoc, -1) != '"') {
|
||||
$errdoc .= '"';
|
||||
}
|
||||
} else {
|
||||
Response::standardError('invaliderrordocumentvalue', '', $throw_exception);
|
||||
}
|
||||
} else {
|
||||
if (Settings::Get('system.webserver') == 'lighttpd') {
|
||||
@@ -191,7 +190,7 @@ class DirOptions extends ApiCommand implements ResourceEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
return $errdoc;
|
||||
return trim($errdoc);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -87,7 +87,8 @@ class DirProtections extends ApiCommand implements ResourceEntity
|
||||
$path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path);
|
||||
$username = Validate::validate($username, 'username', '/^[a-zA-Z0-9][a-zA-Z0-9\-_]+\$?$/', '', [], true);
|
||||
$authname = Validate::validate($authname, 'directory_authname', '/^[a-zA-Z0-9][a-zA-Z0-9\-_ ]+\$?$/', '', [], true);
|
||||
Validate::validate($password, 'password', '', '', [], true);
|
||||
$password = Validate::validate($password, 'password', '', '', [], true);
|
||||
$password = Crypt::validatePassword($password, true);
|
||||
|
||||
// check for duplicate usernames for the path
|
||||
$username_path_check_stmt = Database::prepare("
|
||||
@@ -244,7 +245,8 @@ class DirProtections extends ApiCommand implements ResourceEntity
|
||||
|
||||
// validation
|
||||
$authname = Validate::validate($authname, 'directory_authname', '/^[a-zA-Z0-9][a-zA-Z0-9\-_ ]+\$?$/', '', [], true);
|
||||
Validate::validate($password, 'password', '', '', [], true);
|
||||
$password = Validate::validate($password, 'password', '', '', [], true);
|
||||
$password = Crypt::validatePassword($password, true);
|
||||
|
||||
$upd_query = "";
|
||||
$upd_params = [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -246,7 +246,10 @@ final class InstallCommand extends Command
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->io->error($e->getMessage());
|
||||
return $this->showStep($step, $extended, $decoded_input);
|
||||
if ($this->io->confirm('Retry?', empty($decoded_input))) {
|
||||
return $this->showStep($step, $extended, $decoded_input);
|
||||
}
|
||||
return self::FAILURE;
|
||||
}
|
||||
if ($step == 3) {
|
||||
// do actual install with data from $this->formfielddata
|
||||
@@ -297,7 +300,7 @@ final class InstallCommand extends Command
|
||||
$json_output = [];
|
||||
foreach ($fields['install']['sections'] as $section => $section_fields) {
|
||||
foreach ($section_fields['fields'] as $name => $field) {
|
||||
if ($name == 'system' || $name == 'manual_config') {
|
||||
if ($name == 'system' || $name == 'manual_config' || $name == 'target_servername') {
|
||||
continue;
|
||||
}
|
||||
if ($field['type'] == 'text' || $field['type'] == 'email') {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -69,21 +69,29 @@ 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 != 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: '" . Froxlor::getInstallDir() . "'",
|
||||
"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', Froxlor::getInstallDir());
|
||||
Settings::Set('system.letsencryptchallengepath', $recommended);
|
||||
$former_value = $acmesh_challenge_dir;
|
||||
$acmesh_challenge_dir = Froxlor::getInstallDir();
|
||||
$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 . "@";
|
||||
@@ -138,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');
|
||||
@@ -145,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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class ConfigDisplay
|
||||
if ($lasttype != '' && $lasttype != $_action['type']) {
|
||||
$commands = trim($commands);
|
||||
$numbrows = count(explode("\n", $commands));
|
||||
$configpage .= UI::twig()->render(self::$theme . '/settings/conf/command.html.twig', [
|
||||
$configpage .= UI::twig()->render(UI::validateThemeTemplate('/settings/conf/command.html.twig', self::$theme), [
|
||||
'commands' => $commands,
|
||||
'numbrows' => $numbrows
|
||||
]);
|
||||
@@ -182,7 +182,7 @@ class ConfigDisplay
|
||||
$commands = trim($commands_pre);
|
||||
if ($commands != "") {
|
||||
$numbrows = count(explode("\n", $commands));
|
||||
$commands_pre = UI::twig()->render(self::$theme . '/settings/conf/command.html.twig', [
|
||||
$commands_pre = UI::twig()->render(UI::validateThemeTemplate('/settings/conf/command.html.twig', self::$theme), [
|
||||
'commands' => $commands,
|
||||
'numbrows' => $numbrows
|
||||
]);
|
||||
@@ -190,12 +190,12 @@ class ConfigDisplay
|
||||
$commands = trim($commands_post);
|
||||
if ($commands != "") {
|
||||
$numbrows = count(explode("\n", $commands));
|
||||
$commands_post = UI::twig()->render(self::$theme . '/settings/conf/command.html.twig', [
|
||||
$commands_post = UI::twig()->render(UI::validateThemeTemplate('/settings/conf/command.html.twig', self::$theme), [
|
||||
'commands' => $commands,
|
||||
'numbrows' => $numbrows
|
||||
]);
|
||||
}
|
||||
$configpage .= UI::twig()->render(self::$theme . '/settings/conf/fileblock.html.twig', [
|
||||
$configpage .= UI::twig()->render(UI::validateThemeTemplate('/settings/conf/fileblock.html.twig', self::$theme), [
|
||||
'realname' => $realname,
|
||||
'commands_pre' => $commands_pre,
|
||||
'commands_file' => $commands_file,
|
||||
@@ -210,7 +210,7 @@ class ConfigDisplay
|
||||
$commands = trim($commands);
|
||||
if ($commands != '') {
|
||||
$numbrows = count(explode("\n", $commands));
|
||||
$configpage .= UI::twig()->render(self::$theme . '/settings/conf/command.html.twig', [
|
||||
$configpage .= UI::twig()->render(UI::validateThemeTemplate('/settings/conf/command.html.twig', self::$theme), [
|
||||
'commands' => $commands,
|
||||
'numbrows' => $numbrows
|
||||
]);
|
||||
@@ -233,7 +233,7 @@ class ConfigDisplay
|
||||
$file_content = htmlspecialchars($file_content);
|
||||
$numbrows = count(explode("\n", $file_content));
|
||||
//eval("\$files=\"" . \Froxlor\UI\Template::getTemplate("configfiles/configfiles_file") . "\";");
|
||||
$files = UI::twig()->render(self::$theme . '/settings/conf/file.html.twig', [
|
||||
$files = UI::twig()->render(UI::validateThemeTemplate('/settings/conf/file.html.twig', self::$theme), [
|
||||
'distro_editor' => self::$editor,
|
||||
'realname' => $realname,
|
||||
'numbrows' => $numbrows,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -147,9 +147,9 @@ class FileDir
|
||||
*/
|
||||
public static function makeSecurePath($path)
|
||||
{
|
||||
// check for bad characters, some are allowed with escaping
|
||||
// check for bad characters, some are allowed with escaping,
|
||||
// but we generally don't want them in our directory-names,
|
||||
// thx to aaronmueller for this snipped
|
||||
// thx to aaronmueller for this snippet
|
||||
$badchars = [
|
||||
':',
|
||||
';',
|
||||
@@ -161,7 +161,11 @@ class FileDir
|
||||
'$',
|
||||
'~',
|
||||
'?',
|
||||
"\0"
|
||||
"\0",
|
||||
"\n",
|
||||
"\r",
|
||||
"\t",
|
||||
"\f"
|
||||
];
|
||||
foreach ($badchars as $bc) {
|
||||
$path = str_replace($bc, "", $path);
|
||||
@@ -606,7 +610,7 @@ class FileDir
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public static function getFilesystemQuota()
|
||||
|
||||
@@ -31,10 +31,10 @@ final class Froxlor
|
||||
{
|
||||
|
||||
// Main version variable
|
||||
const VERSION = '2.0.8';
|
||||
const VERSION = '2.0.10';
|
||||
|
||||
// Database version (YYYYMMDDC where C is a daily counter)
|
||||
const DBVERSION = '202301120';
|
||||
const DBVERSION = '202301180';
|
||||
|
||||
// Distribution branding-tag (used for Debian etc.)
|
||||
const BRANDING = '';
|
||||
|
||||
@@ -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]
|
||||
]
|
||||
];
|
||||
|
||||
@@ -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,64 @@ 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) {
|
||||
if ($rr instanceof Net_DNS2_RR_A) {
|
||||
$ips[] = inet_ntop(inet_pton($rr->address));
|
||||
}
|
||||
}
|
||||
} catch (Net_DNS2_Exception $e) {
|
||||
// we can't do anything here, just continue
|
||||
}
|
||||
}
|
||||
if ($record["type"] == "AAAA") {
|
||||
// always use compressed ipv6 format
|
||||
$ip = inet_ntop(inet_pton($record["ipv6"]));
|
||||
$ips[] = $ip;
|
||||
|
||||
// get all ip addresses from the AAAA record and normalize them
|
||||
try {
|
||||
$answer = $resolver->query($host, 'AAAA')->answer;
|
||||
foreach ($answer as $rr) {
|
||||
if ($rr instanceof Net_DNS2_RR_AAAA) {
|
||||
$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']));
|
||||
}
|
||||
}
|
||||
if (count($ips) < 1) {
|
||||
return false;
|
||||
} else {
|
||||
return $ips;
|
||||
}
|
||||
|
||||
return count($ips) > 0 ? $ips : false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -92,7 +92,7 @@ class Text
|
||||
$result = $attributes['fields'];
|
||||
$apikey_data = include Froxlor::getInstallDir() . '/lib/formfields/formfield.api_key.php';
|
||||
|
||||
$body = UI::twig()->render(UI::getTheme() . '/user/inline-form.html.twig', [
|
||||
$body = UI::twig()->render(UI::validateThemeTemplate('/user/inline-form.html.twig'), [
|
||||
'formaction' => $linker->getLink(['section' => 'index', 'page' => 'apikeys']),
|
||||
'formdata' => $apikey_data['apikey'],
|
||||
'editid' => $attributes['fields']['id']
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class UI
|
||||
session_set_cookie_params([
|
||||
'lifetime' => self::$install_mode ? 7200 : 600, // will be renewed based on settings in lib/init.php
|
||||
'path' => '/',
|
||||
'domain' => $_SERVER['SERVER_NAME'],
|
||||
'domain' => explode(':', $_SERVER['HTTP_HOST'])[0],
|
||||
'secure' => self::requestIsHttps(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict'
|
||||
@@ -260,7 +260,18 @@ class UI
|
||||
*/
|
||||
public static function twigBuffer($name, array $context = [])
|
||||
{
|
||||
$template_file = self::getTheme() . '/' . $name;
|
||||
$template_file = self::validateThemeTemplate($name);
|
||||
|
||||
self::$twigbuf[] = [
|
||||
$template_file => $context
|
||||
];
|
||||
}
|
||||
|
||||
public static function validateThemeTemplate(string $name, string $theme = "") {
|
||||
if (empty(trim($theme))) {
|
||||
$theme = self::getTheme();
|
||||
}
|
||||
$template_file = $theme . '/' . $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;
|
||||
@@ -268,10 +279,7 @@ class UI
|
||||
PhpHelper::phpErrHandler(E_USER_ERROR, "Unknown template '" . $template_file . "'", __FILE__, __LINE__);
|
||||
}
|
||||
}
|
||||
|
||||
self::$twigbuf[] = [
|
||||
$template_file => $context
|
||||
];
|
||||
return $template_file;
|
||||
}
|
||||
|
||||
public static function getTheme()
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,7 +332,7 @@ if (CurrentUser::hasSession()) {
|
||||
$cookie_params = [
|
||||
'expires' => time() + Settings::Get('session.sessiontimeout'),
|
||||
'path' => '/',
|
||||
'domain' => $_SERVER['SERVER_NAME'],
|
||||
'domain' => explode(':', $_SERVER['HTTP_HOST'])[0],
|
||||
'secure' => UI::requestIsHttps(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict'
|
||||
|
||||
@@ -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/',
|
||||
|
||||
@@ -110,6 +110,11 @@ return [
|
||||
'class' => 'text-center',
|
||||
'callback' => [Text::class, 'boolean'],
|
||||
],
|
||||
'c.lastlogin_succ' => [
|
||||
'label' => lng('admin.lastlogin_succ'),
|
||||
'field' => 'lastlogin_succ',
|
||||
'callback' => [Text::class, 'timestamp'],
|
||||
],
|
||||
'c.phpenabled' => [
|
||||
'label' => lng('admin.phpenabled'),
|
||||
'field' => 'phpenabled',
|
||||
@@ -138,7 +143,7 @@ return [
|
||||
'class' => 'text-center',
|
||||
'callback' => [Text::class, 'boolean'],
|
||||
],
|
||||
'api_allowed' => [
|
||||
'c.api_allowed' => [
|
||||
'label' => lng('usersettings.api_allowed.title'),
|
||||
'field' => 'api_allowed',
|
||||
'class' => 'text-center',
|
||||
|
||||
@@ -773,6 +773,7 @@ return [
|
||||
'destinationiswrong' => 'Die Weiterleitungsadresse "%s" enthält ungültige Zeichen oder ist nicht vollständig.',
|
||||
'backupfoldercannotbedocroot' => 'Der Ordner für Backups darf nicht das Heimatverzeichnis sein, wählen Sie einen Ordner unterhalb des Heimatverzeichnisses, z.B. /backups',
|
||||
'templatelanguagecombodefined' => 'Die gewählte Kombination aus Sprache und Vorlage ist bereits definiert.',
|
||||
'templatelanguageinvalid' => 'Die gewählte Sprache existiert nicht',
|
||||
'ipstillhasdomains' => 'Die IP/Port-Kombination, die Sie löschen wollen, ist noch bei einer oder mehreren Domains eingetragen. Bitte ändern Sie die Domains vorher auf eine andere IP/Port-Kombination, um diese löschen zu können.',
|
||||
'cantdeletedefaultip' => 'Sie können die Standard-IP/Port-Kombination für Reseller nicht löschen. Bitte setzen Sie eine andere IP/Port-Kombination als Standard, um diese löschen zu können.',
|
||||
'cantdeletesystemip' => 'Sie können die letzte System-IP-Adresse nicht löschen. Entweder legen Sie eine neue IP/Port-Kombination an oder Sie ändern die System-IP-Adresse.',
|
||||
@@ -836,6 +837,7 @@ return [
|
||||
'notrequiredpasswordcomplexity' => 'Die vorgegebene Passwort-Komplexität wurde nicht erfüllt.<br />Bitte kontaktieren Sie Ihren Administrator, wenn Sie Fragen zur Komplexitäts-Vorgabe haben.',
|
||||
'stringerrordocumentnotvalidforlighty' => 'Ein Text als Fehlerdokument funktioniert leider in LigHTTPd nicht, bitte geben Sie einen Pfad zu einer Datei an',
|
||||
'urlerrordocumentnotvalidforlighty' => 'Eine URL als Fehlerdokument funktioniert leider in LigHTTPd nicht, bitte geben Sie einen Pfad zu einer Datei an',
|
||||
'invaliderrordocumentvalue' => 'Der angegebene Wert für das Fehlederdokument ist keine gültige Datei, URL oder Text-Zeile.',
|
||||
'intvaluetoolow' => 'Die angegebene Zahl ist zu klein (Feld "%s")',
|
||||
'intvaluetoohigh' => 'Die angegebene Zahl ist zu groß (Feld "%s")',
|
||||
'phpfpmstillenabled' => 'PHP-FPM ist derzeit aktiviert. Bitte deaktivieren Sie es, um FCGID zu aktivieren',
|
||||
@@ -919,6 +921,7 @@ return [
|
||||
'pathmustberelative' => 'Der Benutzer hat nicht die benötigten Berechtigungen, um Pfade außerhalb des Kunden-Heimatverzeichnisses anzugeben. Bitte einen relativen Pfad angeben (kein führendes /).',
|
||||
'mysqlserverstillhasdbs' => 'Datenbank-Server kann für den Kunden nicht entfernt werden, da sich dort noch Datenbanken befinden.',
|
||||
'domaincannotbeedited' => 'Keine Berechtigung, um die Domain %s zu bearbeiten',
|
||||
'invalidcronjobintervalvalue' => 'Cronjob Intervall muss einer der folgenden Werte sein: %s',
|
||||
],
|
||||
'extras' => [
|
||||
'description' => 'Hier können Sie zusätzliche Extras einrichten, wie zum Beispiel einen Verzeichnisschutz.<br />Die Änderungen sind erst nach einer kurzen Zeit wirksam.',
|
||||
@@ -1965,6 +1968,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',
|
||||
],
|
||||
|
||||
@@ -839,6 +839,7 @@ return [
|
||||
'destinationiswrong' => 'The forwarder %s contains invalid character(s) or is incomplete.',
|
||||
'backupfoldercannotbedocroot' => 'The folder for backups cannot be your homedir, please chose a folder within your homedir, e.g. /backups',
|
||||
'templatelanguagecombodefined' => 'The selected language/template combination has already been defined.',
|
||||
'templatelanguageinvalid' => 'The selected language does not exist',
|
||||
'ipstillhasdomains' => 'The IP/Port combination you want to delete still has domains assigned to it, please reassign those to other IP/Port combinations before deleting this IP/Port combination.',
|
||||
'cantdeletedefaultip' => 'You cannot delete the default IP/Port combination, please make another IP/Port combination default for before deleting this IP/Port combination.',
|
||||
'cantdeletesystemip' => 'You cannot delete the last system IP, either create a new IP/Port combination for the system IP or change the system IP.',
|
||||
@@ -904,6 +905,7 @@ return [
|
||||
'notrequiredpasswordcomplexity' => 'The specified password-complexity was not satisfied.<br />Please contact your administrator if you have any questions about the complexity-specification',
|
||||
'stringerrordocumentnotvalidforlighty' => 'A string as ErrorDocument does not work in lighttpd, please specify a path to a file',
|
||||
'urlerrordocumentnotvalidforlighty' => 'An URL as ErrorDocument does not work in lighttpd, please specify a path to a file',
|
||||
'invaliderrordocumentvalue' => 'The value given as ErrorDocument does not seem to be a valid file, URL or string.',
|
||||
'intvaluetoolow' => 'The given number is too low (field %s)',
|
||||
'intvaluetoohigh' => 'The given number is too high (field %s)',
|
||||
'phpfpmstillenabled' => 'PHP-FPM is currently active. Please deactivate it before activating FCGID',
|
||||
@@ -988,6 +990,7 @@ return [
|
||||
'pathmustberelative' => 'The user does not have the permission to specify directories outside the customers home-directory. Please specify a relative path (no leading /).',
|
||||
'mysqlserverstillhasdbs' => 'Cannot remove database server from customers allow-list as there are still databases on it.',
|
||||
'domaincannotbeedited' => 'You are not permitted to edit the domain %s',
|
||||
'invalidcronjobintervalvalue' => 'Cronjob interval must be one of: %s',
|
||||
],
|
||||
'extras' => [
|
||||
'description' => 'Here you can add some extras, for example directory protection.<br />The system will need some time to apply the new settings after every change.',
|
||||
@@ -2084,6 +2087,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',
|
||||
],
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -54,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' %}
|
||||
@@ -119,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">
|
||||
@@ -151,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 %}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
{% if stype == 'system' %}
|
||||
<div class="form-check">
|
||||
{% set recommended = false %}
|
||||
{% if
|
||||
{% if
|
||||
(dtype == get_setting('system.traffictool')) or
|
||||
(dtype == 'libnssextrausers' and (get_setting('system.mod_fcgid') == '1' or get_setting('phpfpm.enabled') == '1' or get_setting('system.apacheitksupport') == '1')) or
|
||||
(dtype == 'logrotate') or
|
||||
@@ -72,7 +72,7 @@
|
||||
<div class="form-check">
|
||||
{% set recommended = false %}
|
||||
{% if
|
||||
(dtype == 'apache22' and get_setting('system.webserver') == 'apache2' and get_setting('system.apache24') == '0') or
|
||||
(dtype == 'apache22' and get_setting('system.webserver') == 'apache2' and get_setting('system.apache24') == '0') or
|
||||
(dtype == 'apache24' and get_setting('system.webserver') == 'apache2' and get_setting('system.apache24') == '1') or
|
||||
(dtype == 'lighttpd' and get_setting('system.webserver') == 'lighttpd') or
|
||||
(dtype == 'nginx' and get_setting('system.webserver') == 'nginx') or
|
||||
@@ -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>
|
||||
|
||||
@@ -191,4 +191,49 @@ class DirOptionsTest extends TestCase
|
||||
$this->expectExceptionMessage("Directory option with id #1 could not be found");
|
||||
DirOptions::getLocal($admin_userdata, $data)->get();
|
||||
}
|
||||
|
||||
public function testCustomerDirOptionsAddMalformed()
|
||||
{
|
||||
global $admin_userdata;
|
||||
|
||||
// get customer
|
||||
$json_result = Customers::getLocal($admin_userdata, array(
|
||||
'loginname' => 'test1'
|
||||
))->get();
|
||||
$customer_userdata = json_decode($json_result, true)['data'];
|
||||
|
||||
$data = [
|
||||
'path' => '/testmalformed',
|
||||
'error404path' => '/"'.PHP_EOL.'something/../../../../weird 404.html'.PHP_EOL.'#'
|
||||
];
|
||||
$json_result = DirOptions::getLocal($customer_userdata, $data)->add();
|
||||
$result = json_decode($json_result, true)['data'];
|
||||
$expected = '/"something/././././weird\ 404.html#';
|
||||
$this->assertEquals($expected, $result['error404path']);
|
||||
}
|
||||
|
||||
public function testCustomerDirOptionsAddMalformedInvalid()
|
||||
{
|
||||
global $admin_userdata;
|
||||
|
||||
// get customer
|
||||
$json_result = Customers::getLocal($admin_userdata, array(
|
||||
'loginname' => 'test1'
|
||||
))->get();
|
||||
$customer_userdata = json_decode($json_result, true)['data'];
|
||||
|
||||
$data = [
|
||||
'path' => '/testmalformed',
|
||||
'error404path' => '"'.PHP_EOL.'IncludeOptional /something/else/'.PHP_EOL.'#'
|
||||
];
|
||||
$this->expectExceptionMessage("The value given as ErrorDocument does not seem to be a valid file, URL or string.");
|
||||
DirOptions::getLocal($customer_userdata, $data)->add();
|
||||
|
||||
$data = [
|
||||
'path' => '/testmalformed',
|
||||
'error404path' => '"something"oh no a quote within the string"'
|
||||
];
|
||||
$this->expectExceptionMessage("The value given as ErrorDocument does not seem to be a valid file, URL or string.");
|
||||
DirOptions::getLocal($customer_userdata, $data)->add();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user