Compare commits

...

58 Commits

Author SHA1 Message Date
32f5b0d5e9 new Theme
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-30 16:52:34 +01:00
53a6485a6e Maketank Theme migration
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-30 13:52:59 +01:00
f2643ac887 env test 3
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-13 12:46:20 +01:00
e37687a85d env test 3
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-13 12:44:29 +01:00
ccbc3286a5 env test 2
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-13 12:39:58 +01:00
929a562324 env test 2
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-13 12:36:50 +01:00
3704cf6621 env test 2
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-13 12:35:09 +01:00
10238a1466 env test
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-12 14:04:03 +01:00
9002ddf4a2 env test
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-12-12 14:03:10 +01:00
8a2de5a44a env test
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-12-12 13:59:34 +01:00
96c0af18dd npm and compose
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-12 13:50:23 +01:00
5bb228ce78 npm and compose
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-12 13:48:08 +01:00
804128280c npm and compose 2023-12-12 13:47:32 +01:00
5b8e918f75 ssh test
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-12 13:45:23 +01:00
0e3e83d184 ssh test
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-12-12 10:51:21 +01:00
8ced61c6aa bogus edit for pipeline trigger
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-12-11 15:31:39 +01:00
29a2ab7567 2.0 upgrade test first
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-12-07 12:39:20 +01:00
Michael Kaufmann
166ec0575b set version to 2.0.24 for upcoming maintenance release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-10-06 11:18:18 +02:00
Andreu Trepat Rubirola
215e749ba8 added ca language (#1184) 2023-09-24 15:22:33 +02:00
Michael Kaufmann
506cccd7c8 fix vhost-cleaning regex for nginx-location directives; fixes #1185
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-09-12 15:20:56 +02:00
Michael Kaufmann
6d9014c29b fix API permission error in navigation when customer-hide-options include 'domains'; fixes #1183
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-09-07 15:34:06 +02:00
Michael Kaufmann
10555bff76 set version to 2.0.23 for upcoming bugfix release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-09-03 20:16:18 +02:00
Michael Kaufmann
37aa7af4da check for existing userinfo if settings are being imported via cli
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-09-02 17:11:06 +02:00
Michael Kaufmann
4b75369597 only check non-admin resources if user is not an admin in navigation
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-09-02 15:53:15 +02:00
Michael Kaufmann
9d0e463906 set version to 2.0.22 for upcoming maintenance release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-09-01 09:58:33 +02:00
Daniel
a7198f58ce Fix"Add" shortcut link in email address navigation (#1169)
Seems to have changed when adding the domain-filter overview for email addresses, but not updated in the navigation.
2023-08-13 08:19:32 +02:00
Michael Kaufmann
47be4b2847 remove shortcode for --diff-params in configdiff command
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-08-12 09:04:58 +02:00
Daniel
b0fae4bd14 Add config-diff CLI Command (#1168)
---------

Co-authored-by: Michael Kaufmann <d00p@froxlor.org>
2023-08-12 09:03:16 +02:00
Michael Kaufmann
4711a41436 correct validation of hostingplan name and description
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-08-11 13:57:21 +02:00
Michael Kaufmann
faa71ceaef forgot to save one file for the last commit
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-08-11 12:13:33 +02:00
Michael Kaufmann
2d30394150 correctly redirect to last-page if session is timed out and remove passing script/qrystr url parameters
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-08-11 12:09:23 +02:00
Michael Kaufmann
99c1182af8 adjustments in installation for debian 12 and fcgid / disabling mod_php; thx to Konstantin
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-07-27 11:25:03 +02:00
Michael Kaufmann
d9abe58dd2 adjust proftpd config for debian 12 bookworm
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-07-22 13:00:11 +02:00
Michael Kaufmann
23034b8ad2 rework path to certificates non-ecc/ecc, regardless of current setting
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-07-21 08:54:29 +02:00
Michael Kaufmann
1cae5638d3 fix optional-flag for IpsAndPorts.add() and IpsAndPorts.update()
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-07-16 17:09:45 +02:00
Michael Kaufmann
ce9a5f97a3 validate non-empy admin-name in Admins.update()
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-07-16 16:44:46 +02:00
Michael Kaufmann
c38b90deef Merge branch 'main' of github.com:Froxlor/Froxlor 2023-07-07 09:52:37 +02:00
Michael Kaufmann
13daa7d6fa set version to 2.0.21 for upcoming maintenance release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-07-07 09:50:50 +02:00
Michael Kaufmann
b0e43d332d validate generated config-json parameter string
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-07-07 09:47:17 +02:00
jabertwo
75c8754fb4 Fix typo in pathDescriptionSubdomain (#1156) 2023-06-26 11:03:48 +02:00
Michael Kaufmann
e0fa64f897 fix update-check unit-tests now that the current testing version is at 2.1.0-dev1
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-06-08 12:33:48 +02:00
Michael Kaufmann
ed72fd1766 exclude password fields from being filtered/escaped by AntiXSS, fixes #1150
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-06-08 12:14:13 +02:00
Michael Kaufmann
826ae36647 adjust log-levels in API methods
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-06-05 12:13:38 +02:00
Michael Kaufmann
9ddf24539e remove hidden fields from login/passwd-reset; refs #1102
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-06-05 12:10:39 +02:00
Grigory Morozov
3940c1429d Correcting Nginx location match, fixes #1153 2023-06-05 08:06:44 +02:00
Michael Kaufmann
c236d9eaab set version to 2.0.20 for upcoming release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-06-02 20:13:36 +02:00
Michael Kaufmann
688994e40c idna encode umlaut-emailaddresses when adding email-forwarder
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-29 20:52:57 +02:00
Michael Kaufmann
9facaee809 re-enable fcgid/php-fpm activation-validate-check
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-28 15:49:06 +02:00
Michael Kaufmann
a7dd5f4685 show 0 value of resource-fields if value is empty, fixes #1149
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-28 10:46:28 +02:00
Michael Kaufmann
da810ea953 secure filename of local-archive in webupdate
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-25 09:51:55 +02:00
Michael Kaufmann
51b6e067e8 idna encode umlaut-emailaddresses when adding/editing email-account; use correct password-suggestion-layout in change-email-account formfield
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-25 08:26:39 +02:00
Michael Kaufmann
34cf6698bc remove superfluous try_files in nginx config if php-backend (non-fastcgi) is used
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-15 20:14:26 +02:00
Michael Kaufmann
4642160724 add same loginfail restrictions for entering 2fa code as for user/pwd login
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-12 10:36:27 +02:00
Nicolas Thumann
78a259ef3b Fix IPv6 address in cookie domain (#1137)
* Implement getCookieHost to extract cookie host from HTTP_HOST
2023-05-10 08:26:08 +02:00
Nicolas Thumann
68cf4ab69a Fix typo in English privileged_passwd (#1136) 2023-05-09 18:52:43 +02:00
Michael Kaufmann
d5661d492d set version to 2.0.19 for bugfix release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-07 11:07:31 +02:00
Michael Kaufmann
6900898ae1 typo in updater
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-07 11:03:21 +02:00
Michael Kaufmann
d90fb7fa68 fix mysql-pdo check on installation, set version to 2.0.18 for bugfix release
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
2023-05-07 10:54:47 +02:00
186 changed files with 12324 additions and 6510 deletions

53
.drone.yml Normal file
View File

@@ -0,0 +1,53 @@
kind: pipeline
name: deploy-froxlor
type: docker
platform:
os: linux
arch: arm64
trigger:
branch:
- upgrade-2.0
event:
include:
- push
environment:
DEPLOY_HOST: rechner.maketank.net
DEPLOY_DIR: ~/froxlor-test
steps:
- name: deploy
image: cr.wks/drone/drone-rsync:latest
settings:
hosts: ["rechner02.maketank.net"]
source: ./
target: ~/froxlor-test
user: www-data
exclude: ['vendor', '.git*', '*drone.yml', '.settings', '.buildpath', '.editorconfig', '.project', '.travis.yml', 'node_modules']
args: '-v --delete'
log_level: quiet
key:
from_secret: ssh-www-data-maketank-rsa
command_timeout: 10m
- name: compose
image: appleboy/drone-ssh
settings:
host:
- rechner02.maketank.net
username: www-data
key:
from_secret: ssh-www-data-maketank-rsa
script:
- cd ~/froxlor-test && composer install --no-dev
- name: npm
image: appleboy/drone-ssh
settings:
host:
- rechner02.maketank.net
username: www-data
key:
from_secret: ssh-www-data-maketank-rsa
script:
- cd ~/froxlor-test && npm install && npm run build

1
.gitignore vendored
View File

@@ -18,7 +18,6 @@ img/
vendor/
node_modules/
fonts/
templates/*
!templates/index.html
!templates/Froxlor/
templates/Froxlor/assets/mix-manifest.json

View File

@@ -77,6 +77,7 @@ if (($page == 'admins' || $page == 'overview') && $userinfo['change_serversettin
$result['switched_user'] = CurrentUser::getData();
$result['adminsession'] = 1;
$result['userid'] = $result['adminid'];
session_regenerate_id(true);
CurrentUser::setData($result);
$log->logAction(

View File

@@ -28,7 +28,7 @@ require __DIR__ . '/lib/init.php';
use Froxlor\Froxlor;
use Froxlor\FroxlorLogger;
use Froxlor\Http\HttpClient;
use Froxlor\FileDir;
use Froxlor\Install\AutoUpdate;
use Froxlor\Settings;
use Froxlor\UI\Panel\UI;
@@ -132,7 +132,7 @@ elseif ($page == 'getdownload') {
elseif ($page == 'extract') {
if (isset($_POST['send']) && $_POST['send'] == 'send') {
$toExtract = isset($_POST['archive']) ? $_POST['archive'] : null;
$localArchive = Froxlor::getInstallDir() . '/updates/' . $toExtract;
$localArchive = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/updates/' . $toExtract);
$log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "Extracting " . $localArchive . " to " . Froxlor::getInstallDir());
$result = AutoUpdate::extractZip($localArchive);
if ($result > 0) {
@@ -146,7 +146,7 @@ elseif ($page == 'extract') {
Response::redirectTo('admin_updates.php');
} else {
$toExtract = isset($_GET['archive']) ? $_GET['archive'] : null;
$localArchive = Froxlor::getInstallDir() . '/updates/' . $toExtract;
$localArchive = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/updates/' . $toExtract);
}
if (!file_exists($localArchive)) {

View File

@@ -33,6 +33,7 @@ use Froxlor\Settings;
use Froxlor\UI\Panel\UI;
use Froxlor\UI\Request;
use Froxlor\UI\Response;
use Froxlor\Validate\Validate;
if ($userinfo['change_serversettings'] == '1') {
if ($action == 'setconfigured') {
@@ -91,6 +92,7 @@ if ($userinfo['change_serversettings'] == '1') {
}
if ($distribution != "" && isset($_POST['finish'])) {
$valid_keys = ['http', 'dns', 'smtp', 'mail', 'ftp', 'system', 'distro'];
unset($_POST['finish']);
unset($_POST['csrf_token']);
$params = $_POST;
@@ -99,6 +101,20 @@ if ($userinfo['change_serversettings'] == '1') {
foreach ($_POST['system'] as $sysdaemon) {
$params['system'][] = $sysdaemon;
}
// validate params
foreach ($params as $key => $value) {
if (!in_array($key, $valid_keys)) {
unset($params[$key]);
continue;
}
if (!is_array($value)) {
$params[$key] = Validate::validate($value, $key);
} else {
foreach ($value as $subkey => $subvalue) {
$params[$key][$subkey] = Validate::validate($subvalue, $key.'.'.$subkey);
}
}
}
$params_content = json_encode($params);
$params_filename = FileDir::makeCorrectFile(Froxlor::getInstallDir() . 'install/' . Froxlor::genSessionId() . '.json');
file_put_contents($params_filename, $params_content);

View File

@@ -93,6 +93,7 @@ if (($page == 'customers' || $page == 'overview') && $userinfo['customers'] != '
$result['switched_user'] = CurrentUser::getData();
$result['adminsession'] = 0;
$result['userid'] = $result['customerid'];
session_regenerate_id(true);
CurrentUser::setData($result);
$log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "switched user and is now '" . $destination_user . "'");

View File

@@ -53,6 +53,7 @@ if ($action == 'logout') {
if (is_array(CurrentUser::getField('switched_user'))) {
$result = CurrentUser::getData();
$result = $result['switched_user'];
session_regenerate_id(true);
CurrentUser::setData($result);
$target = (isset($_GET['target']) ? $_GET['target'] : 'index');
$redirect = "admin_" . $target . ".php";

View File

@@ -26,6 +26,7 @@
declare(strict_types=1);
use Froxlor\Cli\ConfigDiff;
use Symfony\Component\Console\Application;
use Froxlor\Cli\RunApiCommand;
use Froxlor\Cli\ConfigServices;
@@ -61,4 +62,5 @@ $application->add(new InstallCommand());
$application->add(new MasterCron());
$application->add(new UserCommand());
$application->add(new ValidateAcmeWebroot());
$application->add(new ConfigDiff());
$application->run();

View File

@@ -52,6 +52,7 @@ if ($action == 'logout') {
if (is_array(CurrentUser::getField('switched_user'))) {
$result = CurrentUser::getData();
$result = $result['switched_user'];
session_regenerate_id(true);
CurrentUser::setData($result);
$target = (isset($_GET['target']) ? $_GET['target'] : 'index');
$redirect = "admin_" . $target . ".php";

101
index.php
View File

@@ -40,7 +40,6 @@ use Froxlor\UI\Panel\UI;
use Froxlor\UI\Response;
use Froxlor\User;
use Froxlor\Validate\Validate;
use Froxlor\Language;
if ($action == '') {
$action = 'login';
@@ -53,9 +52,15 @@ if ($action == '2fa_entercode') {
Response::redirectTo('index.php');
exit();
}
$smessage = isset($_GET['showmessage']) ? (int)$_GET['showmessage'] : 0;
$message = "";
if ($smessage > 0) {
$message = lng('error.2fa_wrongcode');
}
// show template to enter code
UI::view('login/enter2fa.html.twig', [
'pagetitle' => lng('login.2fa')
'pagetitle' => lng('login.2fa'),
'message' => $message
]);
} elseif ($action == '2fa_verify') {
// verify code from 2fa code-enter form
@@ -68,25 +73,25 @@ if ($action == '2fa_entercode') {
// verify entered code
$tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname'));
$result = ($_SESSION['secret_2fa'] == 'email' ? true : $tfa->verifyCode($_SESSION['secret_2fa'], $code, 3));
// get user-data
$table = $_SESSION['uidtable_2fa'];
$field = $_SESSION['uidfield_2fa'];
$uid = $_SESSION['uid_2fa'];
$isadmin = $_SESSION['unfo_2fa'];
// either the code is valid when using authenticator-app, or we will select userdata by id and entered code
// which is temporarily stored for the customer when using email-2fa
if ($result) {
// get user-data
$table = $_SESSION['uidtable_2fa'];
$field = $_SESSION['uidfield_2fa'];
$uid = $_SESSION['uid_2fa'];
$isadmin = $_SESSION['unfo_2fa'];
$sel_param = [
'uid' => $uid
];
if ($_SESSION['secret_2fa'] == 'email') {
// verify code by selecting user by id and the temp. stored code,
// so only if it's the correct code, we get the user-data
$sel_stmt = Database::prepare("SELECT * FROM $table WHERE `" . $field . "` = :uid AND `data_2fa` = :code");
$sel_stmt = Database::prepare("SELECT * FROM " . $table . " WHERE `" . $field . "` = :uid AND `data_2fa` = :code");
$sel_param['code'] = $code;
} else {
// Authenticator-verification has already happened at this point, so just get the user-data
$sel_stmt = Database::prepare("SELECT * FROM $table WHERE `" . $field . "` = :uid");
$sel_stmt = Database::prepare("SELECT * FROM " . $table . " WHERE `" . $field . "` = :uid");
}
$userinfo = Database::pexecute_first($sel_stmt, $sel_param);
// whoops, no (valid) user? Start again
@@ -108,19 +113,54 @@ if ($action == '2fa_entercode') {
// when using email-2fa, remove the one-time-code
if ($userinfo['type_2fa'] == '1') {
$del_stmt = Database::prepare("UPDATE $table SET `data_2fa` = '' WHERE `" . $field . "` = :uid");
$del_stmt = Database::prepare("UPDATE " . $table . " SET `data_2fa` = '' WHERE `" . $field . "` = :uid");
$userinfo = Database::pexecute_first($del_stmt, [
'uid' => $uid
]);
}
exit();
}
// wrong 2fa code - treat like "wrong password"
$stmt = Database::prepare("
UPDATE " . $table . "
SET `lastlogin_fail`= :lastlogin_fail, `loginfail_count`=`loginfail_count`+1
WHERE `" . $field . "`= :uid
");
Database::pexecute($stmt, [
"lastlogin_fail" => time(),
"uid" => $uid
]);
// get data for processing further
$stmt = Database::prepare("
SELECT `loginname`, `loginfail_count`, `lastlogin_fail` FROM " . $table . "
WHERE `" . $field . "`= :uid
");
$fail_user = Database::pexecute_first($stmt, [
"uid" => $uid
]);
if ($fail_user['loginfail_count'] >= Settings::Get('login.maxloginattempts') && $fail_user['lastlogin_fail'] > (time() - Settings::Get('login.deactivatetime'))) {
// Log failed login
$rstlog = FroxlorLogger::getInstanceOf([
'loginname' => $_SERVER['REMOTE_ADDR']
]);
$rstlog->logAction(FroxlorLogger::LOGIN_ACTION, LOG_WARNING, "User '" . $fail_user['loginname'] . "' entered wrong 2fa code too often.");
unset($fail_user);
Response::redirectTo('index.php', [
'showmessage' => '3'
]);
exit();
}
unset($fail_user);
// back to form
Response::redirectTo('index.php', [
'showmessage' => '2'
'action' => '2fa_entercode',
'showmessage' => '1'
]);
exit();
} elseif ($action == 'login') {
if (isset($_POST['send']) && $_POST['send'] == 'send') {
if (!empty($_POST)) {
$loginname = Validate::validate($_POST['loginname'], 'loginname');
$password = Validate::validate($_POST['password'], 'password');
@@ -390,13 +430,18 @@ if ($action == '2fa_entercode') {
}
$lastqrystr = "";
if (isset($_REQUEST['qrystr']) && $_REQUEST['qrystr'] != "") {
$lastqrystr = htmlspecialchars($_REQUEST['qrystr'], ENT_QUOTES);
$lastqrystr = urlencode($_REQUEST['qrystr']);
}
if (!empty($lastscript)) {
$_SESSION['lastscript'] = $lastscript;
}
if (!empty($lastqrystr)) {
$_SESSION['lastqrystr'] = $lastqrystr;
}
UI::view('login/login.html.twig', [
'pagetitle' => 'Login',
'lastscript' => $lastscript,
'lastqrystr' => $lastqrystr,
'upd_in_progress' => $update_in_progress,
'message' => $message,
'successmsg' => $successmessage
@@ -408,7 +453,7 @@ if ($action == 'forgotpwd') {
$adminchecked = false;
$message = '';
if (isset($_POST['send']) && $_POST['send'] == 'send') {
if (!empty($_POST)) {
$loginname = Validate::validate($_POST['loginname'], 'loginname');
$email = Validate::validateEmail($_POST['loginemail']);
$result_stmt = Database::prepare("SELECT `adminid`, `customerid`, `customernumber`, `firstname`, `name`, `company`, `email`, `loginname`, `def_language`, `deactivated` FROM `" . TABLE_PANEL_CUSTOMERS . "`
@@ -592,7 +637,7 @@ if ($action == 'forgotpwd') {
UI::view('login/fpwd.html.twig', [
'pagetitle' => lng('login.presend'),
'action' => $action,
'formaction' => 'index.php?action=' . $action,
'message' => $message,
]);
}
@@ -615,7 +660,7 @@ if ($action == 'resetpwd') {
$check = substr($activationcode, 40, 10);
if (substr(md5($third . $timestamp), 0, 10) == $check && $timestamp >= time() - 86400) {
if (isset($_POST['send']) && $_POST['send'] == 'send') {
if (!empty($_POST)) {
$stmt = Database::prepare("SELECT `userid`, `admin` FROM `" . TABLE_PANEL_ACTIVATION . "`
WHERE `activationcode` = :activationcode");
$result = Database::pexecute_first($stmt, [
@@ -692,6 +737,7 @@ if ($action == 'resetpwd') {
function finishLogin($userinfo)
{
if (isset($userinfo['userid']) && $userinfo['userid'] != '') {
session_regenerate_id(true);
CurrentUser::setData($userinfo);
$language = $userinfo['def_language'] ?? Settings::Get('panel.standardlanguage');
@@ -705,29 +751,34 @@ function finishLogin($userinfo)
}
$qryparams = [];
if (isset($_POST['qrystr']) && $_POST['qrystr'] != "") {
parse_str(urldecode($_POST['qrystr']), $qryparams);
if (!empty($_SESSION['lastqrystr'])) {
parse_str(urldecode($_SESSION['lastqrystr']), $qryparams);
unset($_SESSION['lastqrystr']);
}
if ($userinfo['adminsession'] == '1') {
if (Froxlor::hasUpdates() || Froxlor::hasDbUpdates()) {
Response::redirectTo('admin_updates.php?page=overview');
} else {
if (isset($_POST['script']) && $_POST['script'] != "") {
if (preg_match("/customer\_/", $_POST['script']) === 1) {
if (!empty($_SESSION['lastscript'])) {
$lastscript = $_SESSION['lastscript'];
unset($_SESSION['lastscript']);
if (preg_match("/customer\_/", $lastscript) === 1) {
Response::redirectTo('admin_customers.php', [
"page" => "customers"
]);
} else {
Response::redirectTo($_POST['script'], $qryparams);
Response::redirectTo($lastscript, $qryparams);
}
} else {
Response::redirectTo('admin_index.php', $qryparams);
}
}
} else {
if (isset($_POST['script']) && $_POST['script'] != "") {
Response::redirectTo($_POST['script'], $qryparams);
if (!empty($_SESSION['lastscript'])) {
$lastscript = $_SESSION['lastscript'];
unset($_SESSION['lastscript']);
Response::redirectTo($lastscript, $qryparams);
} else {
Response::redirectTo('customer_index.php', $qryparams);
}

View File

@@ -697,7 +697,7 @@ opcache.validate_timestamps'),
('system', 'distribution', ''),
('system', 'update_channel', 'stable'),
('system', 'updatecheck_data', ''),
('system', 'update_notify_last', '2.0.17'),
('system', 'update_notify_last', '2.0.24'),
('system', 'traffictool', 'goaccess'),
('system', 'req_limit_per_interval', 60),
('system', 'req_limit_interval', 60),
@@ -744,7 +744,7 @@ opcache.validate_timestamps'),
('panel', 'logo_overridetheme', '0'),
('panel', 'logo_overridecustom', '0'),
('panel', 'settings_mode', '0'),
('panel', 'version', '2.0.17'),
('panel', 'version', '2.0.24'),
('panel', 'db_version', '202304260');

View File

@@ -149,7 +149,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;
Settings::AddNew("panel.settings_mode", $panel_settings_mode);
$system_distribution = isset($_POST['system_distribution']) ? $_POST['system_distribution'] : '';
$system_distribution = isset($_POST['system_distribution']) ? $_POST['system_distribution'] : 'bullseye';
Settings::AddNew("system.distribution", $system_distribution);
Settings::AddNew("system.update_channel", 'stable');
Settings::AddNew("system.updatecheck_data", '');
@@ -482,3 +482,38 @@ if (Froxlor::isFroxlorVersion('2.0.16')) {
Update::showUpdateStep("Updating from 2.0.16 to 2.0.17", false);
Froxlor::updateToVersion('2.0.17');
}
if (Froxlor::isFroxlorVersion('2.0.17')) {
Update::showUpdateStep("Updating from 2.0.17 to 2.0.18", false);
Froxlor::updateToVersion('2.0.18');
}
if (Froxlor::isFroxlorVersion('2.0.18')) {
Update::showUpdateStep("Updating from 2.0.18 to 2.0.19", false);
Froxlor::updateToVersion('2.0.19');
}
if (Froxlor::isFroxlorVersion('2.0.19')) {
Update::showUpdateStep("Updating from 2.0.19 to 2.0.20", false);
Froxlor::updateToVersion('2.0.20');
}
if (Froxlor::isFroxlorVersion('2.0.20')) {
Update::showUpdateStep("Updating from 2.0.20 to 2.0.21", false);
Froxlor::updateToVersion('2.0.21');
}
if (Froxlor::isFroxlorVersion('2.0.21')) {
Update::showUpdateStep("Updating from 2.0.21 to 2.0.22", false);
Froxlor::updateToVersion('2.0.22');
}
if (Froxlor::isFroxlorVersion('2.0.22')) {
Update::showUpdateStep("Updating from 2.0.22 to 2.0.23", false);
Froxlor::updateToVersion('2.0.23');
}
if (Froxlor::isFroxlorVersion('2.0.23')) {
Update::showUpdateStep("Updating from 2.0.23 to 2.0.24", false);
Froxlor::updateToVersion('2.0.24');
}

View File

@@ -54,7 +54,7 @@ if (Update::versionInUpdate($current_version, '2.0.0-beta1')) {
$config_dir = FileDir::makeCorrectDir(Froxlor::getInstallDir() . '/lib/configfiles/');
// show list of available distro's
$distros = glob($config_dir . '*.xml');
$distributions_select[''] = '-';
// selection is required $distributions_select[''] = '-';
// read in all the distros
foreach ($distros as $_distribution) {
// get configparser object

View File

@@ -39,12 +39,12 @@ abstract class ApiParameter
/**
*
* @param array $params
* @param array|null $params
* optional, array of parameters (var=>value) for the command
*
* @throws Exception
*/
public function __construct($params = null)
public function __construct(array $params = null)
{
if (!is_null($params)) {
$params = $this->trimArray($params);
@@ -57,7 +57,7 @@ abstract class ApiParameter
*
* @param array $input
*
* @return array
* @return string|array
*/
private function trimArray($input)
{
@@ -79,9 +79,9 @@ abstract class ApiParameter
/**
* get specific parameter which also has and unlimited-field
*
* @param string $param
* @param string|null $param
* parameter to get out of the request-parameter list
* @param string $ul_field
* @param string|null $ul_field
* parameter to get out of the request-parameter list
* @param bool $optional
* default: false
@@ -91,7 +91,7 @@ abstract class ApiParameter
* @return mixed
* @throws Exception
*/
protected function getUlParam($param = null, $ul_field = null, $optional = false, $default = 0)
protected function getUlParam(string $param = null, string $ul_field = null, bool $optional = false, $default = 0)
{
$param_value = (int)$this->getParam($param, $optional, $default);
$ul_field_value = $this->getBoolParam($ul_field, true, 0);
@@ -102,11 +102,11 @@ abstract class ApiParameter
}
/**
* get specific parameter from the parameterlist;
* get specific parameter from the parameter list;
* check for existence and != empty if needed.
* Maybe more in the future
*
* @param string $param
* @param string|null $param
* parameter to get out of the request-parameter list
* @param bool $optional
* default: false
@@ -116,7 +116,7 @@ abstract class ApiParameter
* @return mixed
* @throws Exception
*/
protected function getParam($param = null, $optional = false, $default = '')
protected function getParam(string $param = null, bool $optional = false, $default = '')
{
// does it exist?
if (!isset($this->cmd_params[$param])) {
@@ -128,7 +128,7 @@ abstract class ApiParameter
return $default;
}
// is it empty? - test really on string, as value 0 is being seen as empty by php
if ($this->cmd_params[$param] === "") {
if (!is_array($this->cmd_params[$param]) && trim($this->cmd_params[$param]) === "") {
if ($optional === false) {
// get module + function for better error-messages
$inmod = $this->getModFunctionString();
@@ -142,7 +142,7 @@ abstract class ApiParameter
/**
* returns "module::function()" for better error-messages (missing parameter etc.)
* makes debugging a whole lot more comfortable
* makes debugging a lot more comfortable
*
* @param int $level
* depth of backtrace, default 2
@@ -152,7 +152,7 @@ abstract class ApiParameter
*
* @return string
*/
private function getModFunctionString($level = 1, $max_level = 5, $trace = null)
private function getModFunctionString(int $level = 1, int $max_level = 5, $trace = null)
{
// which class called us
$_class = get_called_class();
@@ -174,7 +174,7 @@ abstract class ApiParameter
/**
* getParam wrapper for boolean parameter
*
* @param string $param
* @param string|null $param
* parameter to get out of the request-parameter list
* @param bool $optional
* default: false
@@ -183,7 +183,7 @@ abstract class ApiParameter
*
* @return string
*/
protected function getBoolParam($param = null, $optional = false, $default = false)
protected function getBoolParam(string $param = null, bool $optional = false, $default = false)
{
$_default = '0';
if ($default) {

View File

@@ -95,7 +95,7 @@ class Admins extends ApiCommand implements ResourceEntity
public function listing()
{
if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) {
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list admins");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list admins");
$query_fields = [];
$result_stmt = Database::prepare("
SELECT *
@@ -407,7 +407,7 @@ class Admins extends ApiCommand implements ResourceEntity
];
$result = Database::pexecute_first($result_stmt, $params, true, true);
if ($result) {
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] get admin '" . $result['loginname'] . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] get admin '" . $result['loginname'] . "'");
return $this->response($result);
}
$key = ($id > 0 ? "id #" . $id : "loginname '" . $loginname . "'");
@@ -584,6 +584,18 @@ class Admins extends ApiCommand implements ResourceEntity
$theme = Settings::Get('panel.default_theme');
}
if (empty(trim($name))) {
Response::standardError([
'stringisempty',
'admin.name'
], '', true);
}
if (empty(trim($email))) {
Response::standardError([
'stringisempty',
'admin.email'
], '', true);
}
if (!Validate::validateEmail($email)) {
Response::standardError('emailiswrong', $email, true);
} else {
@@ -705,7 +717,7 @@ class Admins extends ApiCommand implements ResourceEntity
WHERE `adminid` = :adminid
");
Database::pexecute($upd_stmt, $upd_data, true, true);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] edited admin '" . $result['loginname'] . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] edited admin '" . $result['loginname'] . "'");
// get all admin-data for return-array
$result = $this->apiCall('Admins.get', [

View File

@@ -97,7 +97,7 @@ class Certificates extends ApiCommand implements ResourceEntity
}
if (!$has_cert) {
$this->addOrUpdateCertificate($domain['id'], $ssl_cert_file, $ssl_key_file, $ssl_ca_file, $ssl_cert_chainfile, true);
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added ssl-certificate for '" . $domain['domain'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added ssl-certificate for '" . $domain['domain'] . "'");
$result = $this->apiCall('Certificates.get', [
'id' => $domain['id']
]);
@@ -248,7 +248,7 @@ class Certificates extends ApiCommand implements ResourceEntity
$ssl_ca_file = $this->getParam('ssl_ca_file', true, '');
$ssl_cert_chainfile = $this->getParam('ssl_cert_chainfile', true, '');
$this->addOrUpdateCertificate($domain['id'], $ssl_cert_file, $ssl_key_file, $ssl_ca_file, $ssl_cert_chainfile, false);
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] updated ssl-certificate for '" . $domain['domain'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated ssl-certificate for '" . $domain['domain'] . "'");
$result = $this->apiCall('Certificates.get', [
'id' => $domain['id']
]);
@@ -470,7 +470,7 @@ class Certificates extends ApiCommand implements ResourceEntity
if ($chk['letsencrypt'] == '1') {
Cronjob::inserttask(TaskId::DELETE_DOMAIN_SSL, $chk['domain']);
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] removed ssl-certificate for '" . $chk['domain'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] removed ssl-certificate for '" . $chk['domain'] . "'");
return $this->response($result);
}
throw new Exception("Unable to determine SSL certificate. Maybe no access?", 406);

View File

@@ -147,7 +147,7 @@ class Cronjobs extends ApiCommand implements ResourceEntity
// insert task to re-generate the cron.d-file
Cronjob::inserttask(TaskId::REBUILD_CRON);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] cronjob with description '" . $result['module'] . '/' . $result['cronfile'] . "' has been updated by '" . $this->getUserDetail('loginname') . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] cronjob with description '" . $result['module'] . '/' . $result['cronfile'] . "' has been updated by '" . $this->getUserDetail('loginname') . "'");
$result = $this->apiCall('Cronjobs.get', [
'id' => $id
]);
@@ -177,7 +177,7 @@ class Cronjobs extends ApiCommand implements ResourceEntity
public function listing()
{
if ($this->isAdmin()) {
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list cronjobs");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list cronjobs");
$query_fields = [];
$result_stmt = Database::prepare("
SELECT `c`.* FROM `" . TABLE_PANEL_CRONRUNS . "` `c` " . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit());

View File

@@ -194,7 +194,7 @@ class CustomerBackups extends ApiCommand implements ResourceEntity
$result[] = $entry;
}
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list customer-backups");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list customer-backups");
return $this->response([
'count' => count($result),
'list' => $result

View File

@@ -895,7 +895,7 @@ class Customers extends ApiCommand implements ResourceEntity
$result['dbspace_used'] = 0;
}
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get customer '" . $result['loginname'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get customer '" . $result['loginname'] . "'");
return $this->response($result);
}
$key = ($id > 0 ? "id #" . $id : "loginname '" . $loginname . "'");
@@ -1327,7 +1327,7 @@ class Customers extends ApiCommand implements ResourceEntity
'vu' => $valid_until
], true, true);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] " . ($deactivated ? 'deactivated' : 'reactivated') . " user '" . $result['loginname'] . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] " . ($deactivated ? 'deactivated' : 'reactivated') . " user '" . $result['loginname'] . "'");
Cronjob::inserttask(TaskId::REBUILD_VHOST);
}
@@ -1538,7 +1538,7 @@ class Customers extends ApiCommand implements ResourceEntity
Database::query($admin_update_query);
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] edited user '" . $result['loginname'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] edited user '" . $result['loginname'] . "'");
/*
* move customer to another admin/reseller; #1166
@@ -1911,7 +1911,7 @@ class Customers extends ApiCommand implements ResourceEntity
// now, recalculate the resource-usage for the old and the new admin
User::updateCounters(false);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] moved user '" . $c_result['loginname'] . "' from admin/reseller '" . $c_result['adminname'] . " to admin/reseller '" . $a_result['loginname'] . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] moved user '" . $c_result['loginname'] . "' from admin/reseller '" . $c_result['adminname'] . " to admin/reseller '" . $a_result['loginname'] . "'");
$result = $this->apiCall('Customers.get', [
'id' => $c_result['customerid']

View File

@@ -144,7 +144,7 @@ class DirOptions extends ApiCommand implements ResourceEntity
];
Database::pexecute($stmt, $params, true, true);
$id = Database::lastInsertId();
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added directory-option for '" . $userpath . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added directory-option for '" . $userpath . "'");
Cronjob::inserttask(TaskId::REBUILD_VHOST);
$result = $this->apiCall('DirOptions.get', [
@@ -247,7 +247,7 @@ class DirOptions extends ApiCommand implements ResourceEntity
$params['id'] = $id;
$result = Database::pexecute_first($result_stmt, $params, true, true);
if ($result) {
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get directory options for '" . $result['path'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get directory options for '" . $result['path'] . "'");
return $this->response($result);
}
$key = "id #" . $id;
@@ -331,7 +331,7 @@ class DirOptions extends ApiCommand implements ResourceEntity
"id" => $id
];
Database::pexecute($stmt, $params, true, true);
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] edited directory options for '" . str_replace($customer['documentroot'], '/', $result['path']) . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] edited directory options for '" . str_replace($customer['documentroot'], '/', $result['path']) . "'");
}
$result = $this->apiCall('DirOptions.get', [
@@ -379,7 +379,7 @@ class DirOptions extends ApiCommand implements ResourceEntity
while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) {
$result[] = $row;
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list directory-options");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list directory-options");
return $this->response([
'count' => count($result),
'list' => $result
@@ -478,7 +478,7 @@ class DirOptions extends ApiCommand implements ResourceEntity
"customerid" => $customer_data['customerid'],
"id" => $id
], true, true);
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] deleted directory-option for '" . str_replace($customer_data['documentroot'], '/', $result['path']) . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] deleted directory-option for '" . str_replace($customer_data['documentroot'], '/', $result['path']) . "'");
Cronjob::inserttask(TaskId::REBUILD_VHOST);
return $this->response($result);
}

View File

@@ -129,7 +129,7 @@ class DirProtections extends ApiCommand implements ResourceEntity
];
Database::pexecute($stmt, $params, true, true);
$id = Database::lastInsertId();
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added directory-protection for '" . $username . " (" . $path . ")'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added directory-protection for '" . $username . " (" . $path . ")'");
Cronjob::inserttask(TaskId::REBUILD_VHOST);
$result = $this->apiCall('DirProtections.get', [
@@ -196,7 +196,7 @@ class DirProtections extends ApiCommand implements ResourceEntity
$params['idun'] = ($id <= 0 ? $username : $id);
$result = Database::pexecute_first($result_stmt, $params, true, true);
if ($result) {
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get directory protection for '" . $result['path'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get directory protection for '" . $result['path'] . "'");
return $this->response($result);
}
$key = ($id > 0 ? "id #" . $id : "username '" . $username . "'");
@@ -279,7 +279,7 @@ class DirProtections extends ApiCommand implements ResourceEntity
Cronjob::inserttask(TaskId::REBUILD_VHOST);
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] updated directory-protection '" . $result['username'] . " (" . $result['path'] . ")'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated directory-protection '" . $result['username'] . " (" . $result['path'] . ")'");
$result = $this->apiCall('DirProtections.get', [
'id' => $result['id']
]);
@@ -325,7 +325,7 @@ class DirProtections extends ApiCommand implements ResourceEntity
while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) {
$result[] = $row;
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list directory-protections");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list directory-protections");
return $this->response([
'count' => count($result),
'list' => $result
@@ -413,7 +413,7 @@ class DirProtections extends ApiCommand implements ResourceEntity
"id" => $id
]);
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] deleted htpasswd for '" . $result['username'] . " (" . $result['path'] . ")'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted htpasswd for '" . $result['username'] . " (" . $result['path'] . ")'");
Cronjob::inserttask(TaskId::REBUILD_VHOST);
return $this->response($result);
}

View File

@@ -413,7 +413,7 @@ class DomainZones extends ApiCommand implements ResourceEntity
$zone = Dns::createDomainZone($id);
$zonefile = (string)$zone;
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get dns-zone for '" . $result['domain'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get dns-zone for '" . $result['domain'] . "'");
return $this->response(explode("\n", $zonefile));
}

View File

@@ -898,7 +898,7 @@ class Domains extends ApiCommand implements ResourceEntity
$result['ipsandports'] = $this->getIpsForDomain($result['id']);
}
$result['domain_hascert'] = $this->getHasCertValueForDomain((int)$result['id'], (int)$result['parentdomainid']);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] get domain '" . $result['domain'] . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] get domain '" . $result['domain'] . "'");
return $this->response($result);
}
$key = ($id > 0 ? "id #" . $id : "domainname '" . $domainname . "'");
@@ -1801,7 +1801,7 @@ class Domains extends ApiCommand implements ResourceEntity
Database::pexecute($upd_stmt, [
'id' => $id
], true, true);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] removed specialsettings on all subdomains of domain #" . $id);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] removed specialsettings on all subdomains of domain #" . $id);
}
$wwwserveralias = ($serveraliasoption == '1') ? '1' : '0';
@@ -2221,7 +2221,7 @@ class Domains extends ApiCommand implements ResourceEntity
// remove domain from acme.sh / lets encrypt if used
Cronjob::inserttask(TaskId::DELETE_DOMAIN_SSL, $result['domain']);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] deleted domain/subdomains (#" . $result['id'] . ")");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] deleted domain/subdomains (#" . $result['id'] . ")");
User::updateCounters();
Cronjob::inserttask(TaskId::REBUILD_VHOST);
// Using nameserver, insert a task which rebuilds the server config

View File

@@ -99,6 +99,11 @@ class EmailAccounts extends ApiCommand implements ResourceEntity
Response::standardError('notallowedtouseaccounts', '', true);
}
if (!empty($emailaddr)) {
$idna_convert = new IdnaWrapper();
$emailaddr = $idna_convert->encode($emailaddr);
}
// get email address
$result = $this->apiCall('Emails.get', [
'id' => $id,
@@ -306,7 +311,7 @@ class EmailAccounts extends ApiCommand implements ResourceEntity
}
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added email account for '" . $result['email_full'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added email account for '" . $result['email_full'] . "'");
$result = $this->apiCall('Emails.get', [
'emailaddr' => $result['email_full']
]);
@@ -357,6 +362,11 @@ class EmailAccounts extends ApiCommand implements ResourceEntity
$ea_optional = $id > 0;
$emailaddr = $this->getParam('emailaddr', $ea_optional, '');
if (!empty($emailaddr)) {
$idna_convert = new IdnaWrapper();
$emailaddr = $idna_convert->encode($emailaddr);
}
// validation
$result = $this->apiCall('Emails.get', [
'id' => $id,
@@ -450,7 +460,7 @@ class EmailAccounts extends ApiCommand implements ResourceEntity
Admins::increaseUsage($customer['adminid'], 'email_quota_used', '', ($quota - $result['quota']));
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] updated email account '" . $result['email_full'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated email account '" . $result['email_full'] . "'");
$result = $this->apiCall('Emails.get', [
'emailaddr' => $result['email_full']
]);
@@ -556,7 +566,7 @@ class EmailAccounts extends ApiCommand implements ResourceEntity
Customers::decreaseUsage($customer['customerid'], 'email_accounts_used');
Customers::decreaseUsage($customer['customerid'], 'email_quota_used', '', $quota);
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] deleted email account for '" . $result['email_full'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted email account for '" . $result['email_full'] . "'");
return $this->response($result);
}
}

View File

@@ -89,7 +89,7 @@ class EmailDomains extends ApiCommand implements ResourceEntity
while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) {
$result[] = $row;
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE,
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO,
"[API] list email-domains");
return $this->response([
'count' => count($result),

View File

@@ -77,6 +77,11 @@ class EmailForwarders extends ApiCommand implements ResourceEntity
$idna_convert = new IdnaWrapper();
$destination = $idna_convert->encode($destination);
if (!empty($emailaddr)) {
$idna_convert = new IdnaWrapper();
$emailaddr = $idna_convert->encode($emailaddr);
}
$result = $this->apiCall('Emails.get', [
'id' => $id,
'emailaddr' => $emailaddr
@@ -116,7 +121,7 @@ class EmailForwarders extends ApiCommand implements ResourceEntity
// update customer usage
Customers::increaseUsage($customer['customerid'], 'email_forwarders_used');
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added email forwarder for '" . $result['email_full'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added email forwarder for '" . $result['email_full'] . "'");
$result = $this->apiCall('Emails.get', [
'emailaddr' => $result['email_full']
@@ -293,7 +298,7 @@ class EmailForwarders extends ApiCommand implements ResourceEntity
// update customer usage
Customers::decreaseUsage($customer['customerid'], 'email_forwarders_used');
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] deleted email forwarder for '" . $result['email_full'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] deleted email forwarder for '" . $result['email_full'] . "'");
$result = $this->apiCall('Emails.get', [
'emailaddr' => $result['email_full']

View File

@@ -159,7 +159,7 @@ class Emails extends ApiCommand implements ResourceEntity
// update customer usage
Customers::increaseUsage($customer['customerid'], 'emails_used');
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added email address '" . $email_full . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added email address '" . $email_full . "'");
$result = $this->apiCall('Emails.get', [
'emailaddr' => $email_full
@@ -199,7 +199,7 @@ class Emails extends ApiCommand implements ResourceEntity
);
$result = Database::pexecute_first($result_stmt, $params, true, true);
if ($result) {
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get email address '" . $result['email_full'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get email address '" . $result['email_full'] . "'");
return $this->response($result);
}
$key = ($id > 0 ? "id #" . $id : "emailaddr '" . $emailaddr . "'");
@@ -294,7 +294,7 @@ class Emails extends ApiCommand implements ResourceEntity
"id" => $id
];
Database::pexecute($stmt, $params, true, true);
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] toggled catchall-flag for email address '" . $result['email_full'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] toggled catchall-flag for email address '" . $result['email_full'] . "'");
$result = $this->apiCall('Emails.get', [
'emailaddr' => $result['email_full']
@@ -340,7 +340,7 @@ class Emails extends ApiCommand implements ResourceEntity
while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) {
$result[] = $row;
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list email-addresses");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list email-addresses");
return $this->response([
'count' => count($result),
'list' => $result
@@ -445,7 +445,7 @@ class Emails extends ApiCommand implements ResourceEntity
], true, true);
Customers::decreaseUsage($customer['customerid'], 'emails_used');
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] deleted email address '" . $result['email_full'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted email address '" . $result['email_full'] . "'");
return $this->response($result);
}
}

View File

@@ -64,7 +64,7 @@ class FpmDaemons extends ApiCommand implements ResourceEntity
public function listing()
{
if ($this->isAdmin()) {
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list fpm-daemons");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list fpm-daemons");
$query_fields = [];
$result_stmt = Database::prepare("
SELECT * FROM `" . TABLE_PANEL_FPMDAEMONS . "`" . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit());
@@ -258,7 +258,7 @@ class FpmDaemons extends ApiCommand implements ResourceEntity
$id = Database::lastInsertId();
Cronjob::inserttask(TaskId::REBUILD_VHOST);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] fpm-daemon with description '" . $description . "' has been created by '" . $this->getUserDetail('loginname') . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] fpm-daemon with description '" . $description . "' has been created by '" . $this->getUserDetail('loginname') . "'");
$result = $this->apiCall('FpmDaemons.get', [
'id' => $id
]);
@@ -384,7 +384,7 @@ class FpmDaemons extends ApiCommand implements ResourceEntity
Database::pexecute($upd_stmt, $upd_data, true, true);
Cronjob::inserttask(TaskId::REBUILD_VHOST);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] fpm-daemon with description '" . $description . "' has been updated by '" . $this->getUserDetail('loginname') . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] fpm-daemon with description '" . $description . "' has been updated by '" . $this->getUserDetail('loginname') . "'");
$result = $this->apiCall('FpmDaemons.get', [
'id' => $id
]);
@@ -433,7 +433,7 @@ class FpmDaemons extends ApiCommand implements ResourceEntity
], true, true);
Cronjob::inserttask(TaskId::REBUILD_VHOST);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] fpm-daemon setting '" . $result['description'] . "' has been deleted by '" . $this->getUserDetail('loginname') . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] fpm-daemon setting '" . $result['description'] . "' has been deleted by '" . $this->getUserDetail('loginname') . "'");
return $this->response($result);
}
throw new Exception("Not allowed to execute given command.", 403);

View File

@@ -72,7 +72,7 @@ class Froxlor extends ApiCommand
if (empty($uc_data) || empty($response) || $uc_data['ts'] + self::UPDATE_CHECK_INTERVAL < time() || $uc_data['channel'] != Settings::Get('system.update_channel') || $force_ucheck) {
// log our actions
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] checking for updates");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] checking for updates");
// check for new version
$aucheck = AutoUpdate::checkVersion();
@@ -142,7 +142,7 @@ class Froxlor extends ApiCommand
{
if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) {
$json_str = $this->getParam('json_str');
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "User " . $this->getUserDetail('loginname') . " imported settings");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "User " . $this->getUserDetail('loginname') . " imported settings");
try {
SImExporter::import($json_str);
Cronjob::inserttask(TaskId::REBUILD_VHOST);

View File

@@ -257,7 +257,7 @@ class Ftps extends ApiCommand implements ResourceEntity
Customers::increaseUsage($customer['customerid'], 'ftp_lastaccountnumber');
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added ftp-account '" . $username . " (" . $path . ")'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added ftp-account '" . $username . " (" . $path . ")'");
Cronjob::inserttask(TaskId::CREATE_FTP);
if ($sendinfomail == 1) {
@@ -302,7 +302,7 @@ class Ftps extends ApiCommand implements ResourceEntity
$this->mailer()->clearAddresses();
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] added ftp-user '" . $username . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added ftp-user '" . $username . "'");
$result = $this->apiCall('Ftps.get', [
'username' => $username
@@ -367,7 +367,7 @@ class Ftps extends ApiCommand implements ResourceEntity
$params['idun'] = ($id <= 0 ? $username : $id);
$result = Database::pexecute_first($result_stmt, $params, true, true);
if ($result) {
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get ftp-user '" . $result['username'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get ftp-user '" . $result['username'] . "'");
return $this->response($result);
}
$key = ($id > 0 ? "id #" . $id : "username '" . $username . "'");
@@ -453,7 +453,7 @@ class Ftps extends ApiCommand implements ResourceEntity
"id" => $id,
"password" => $cryptPassword
], true, true);
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] updated ftp-account password for '" . $result['username'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated ftp-account password for '" . $result['username'] . "'");
}
// path update?
@@ -471,7 +471,7 @@ class Ftps extends ApiCommand implements ResourceEntity
"customerid" => $customer['customerid'],
"id" => $id
], true, true);
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] updated ftp-account homdir for '" . $result['username'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated ftp-account homdir for '" . $result['username'] . "'");
}
}
// it's the task for "new ftp" but that will
@@ -533,7 +533,7 @@ class Ftps extends ApiCommand implements ResourceEntity
while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) {
$result[] = $row;
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list ftp-users");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list ftp-users");
return $this->response([
'count' => count($result),
'list' => $result

View File

@@ -61,7 +61,7 @@ class HostingPlans extends ApiCommand implements ResourceEntity
public function listing()
{
if ($this->isAdmin()) {
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list hosting-plans");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list hosting-plans");
$query_fields = [];
$result_stmt = Database::prepare("
SELECT p.*, a.loginname as adminname
@@ -200,8 +200,8 @@ class HostingPlans extends ApiCommand implements ResourceEntity
$value_arr['logviewenabled'] = $this->getBoolParam('logviewenabled', true, 0);
// validation
$name = Validate::validate(trim($name), 'name', '', '', [], true);
$description = Validate::validate(str_replace("\r\n", "\n", $description), 'description', Validate::REGEX_DESC_TEXT);
$name = Validate::validate(trim($name), 'name', Validate::REGEX_DESC_TEXT, '', [], true);
$description = Validate::validate(str_replace("\r\n", "\n", $description), 'description', Validate::REGEX_CONF_TEXT);
if (Settings::Get('system.mail_quota_enabled') != '1') {
$value_arr['email_quota'] = -1;
@@ -227,7 +227,7 @@ class HostingPlans extends ApiCommand implements ResourceEntity
'valuearr' => json_encode($value_arr)
];
Database::pexecute($ins_stmt, $ins_data, true, true);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added hosting-plan '" . $name . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] added hosting-plan '" . $name . "'");
$result = $this->apiCall('HostingPlans.get', [
'planname' => $name
]);
@@ -264,7 +264,7 @@ class HostingPlans extends ApiCommand implements ResourceEntity
}
$result = Database::pexecute_first($result_stmt, $params, true, true);
if ($result) {
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] get hosting-plan '" . $result['name'] . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] get hosting-plan '" . $result['name'] . "'");
return $this->response($result);
}
$key = ($id > 0 ? "id #" . $id : "planname '" . $planname . "'");
@@ -382,8 +382,8 @@ class HostingPlans extends ApiCommand implements ResourceEntity
$value_arr['logviewenabled'] = $this->getBoolParam('logviewenabled', true, $result['logviewenabled']);
// validation
$name = Validate::validate(trim($name), 'name', '', '', [], true);
$description = Validate::validate(str_replace("\r\n", "\n", $description), 'description', Validate::REGEX_DESC_TEXT);
$name = Validate::validate(trim($name), 'name', Validate::REGEX_DESC_TEXT, '', [], true);
$description = Validate::validate(str_replace("\r\n", "\n", $description), 'description', Validate::REGEX_CONF_TEXT);
if (Settings::Get('system.mail_quota_enabled') != '1') {
$value_arr['email_quota'] = -1;
@@ -414,7 +414,7 @@ class HostingPlans extends ApiCommand implements ResourceEntity
'id' => $id
];
Database::pexecute($upd_stmt, $update_data, true, true);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] updated hosting-plan '" . $result['name'] . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] updated hosting-plan '" . $result['name'] . "'");
return $this->response($update_data);
}
throw new Exception("Not allowed to execute given command.", 403);

View File

@@ -65,7 +65,7 @@ class IpsAndPorts extends ApiCommand implements ResourceEntity
public function listing()
{
if ($this->isAdmin() && ($this->getUserDetail('change_serversettings') || !empty($this->getUserDetail('ip')))) {
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list ips and ports");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list ips and ports");
$ip_where = "";
$append_where = false;
if (!empty($this->getUserDetail('ip')) && $this->getUserDetail('ip') != -1) {
@@ -175,9 +175,9 @@ class IpsAndPorts extends ApiCommand implements ResourceEntity
$docroot = Validate::validate($this->getParam('docroot', true, ''), 'docroot', Validate::REGEX_DIR, '', [], true);
if ((int)Settings::Get('system.use_ssl') == 1) {
$ssl = !empty($this->getBoolParam('ssl', true, 0)) ? intval($this->getBoolParam('ssl', true, 0)) : 0;
$ssl_cert_file = Validate::validate($this->getParam('ssl_cert_file', $ssl, ''), 'ssl_cert_file', '', '', [], true);
$ssl_key_file = Validate::validate($this->getParam('ssl_key_file', $ssl, ''), 'ssl_key_file', '', '', [], true);
$ssl = (bool)$this->getBoolParam('ssl', true, 0);
$ssl_cert_file = Validate::validate($this->getParam('ssl_cert_file', !$ssl, ''), 'ssl_cert_file', '', '', [], true);
$ssl_key_file = Validate::validate($this->getParam('ssl_key_file', !$ssl, ''), 'ssl_key_file', '', '', [], true);
$ssl_ca_file = Validate::validate($this->getParam('ssl_ca_file', true, ''), 'ssl_ca_file', '', '', [], true);
$ssl_cert_chainfile = Validate::validate($this->getParam('ssl_cert_chainfile', true, ''), 'ssl_cert_chainfile', '', '', [], true);
$sslss = $this->getParam('ssl_specialsettings', true, '');
@@ -335,7 +335,7 @@ class IpsAndPorts extends ApiCommand implements ResourceEntity
'id' => $id
], true, true);
if ($result) {
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] get ip " . $result['ip'] . " " . $result['port']);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] get ip " . $result['ip'] . " " . $result['port']);
return $this->response($result);
}
throw new Exception("IP/port with id #" . $id . " could not be found", 404);
@@ -414,9 +414,9 @@ class IpsAndPorts extends ApiCommand implements ResourceEntity
$docroot = Validate::validate($this->getParam('docroot', true, $result['docroot']), 'docroot', Validate::REGEX_DIR, '', [], true);
if ((int)Settings::Get('system.use_ssl') == 1) {
$ssl = $this->getBoolParam('ssl', true, $result['ssl']);
$ssl_cert_file = Validate::validate($this->getParam('ssl_cert_file', $ssl, $result['ssl_cert_file']), 'ssl_cert_file', '', '', [], true);
$ssl_key_file = Validate::validate($this->getParam('ssl_key_file', $ssl, $result['ssl_key_file']), 'ssl_key_file', '', '', [], true);
$ssl = (bool)$this->getBoolParam('ssl', true, $result['ssl']);
$ssl_cert_file = Validate::validate($this->getParam('ssl_cert_file', !$ssl, $result['ssl_cert_file']), 'ssl_cert_file', '', '', [], true);
$ssl_key_file = Validate::validate($this->getParam('ssl_key_file', !$ssl, $result['ssl_key_file']), 'ssl_key_file', '', '', [], true);
$ssl_ca_file = Validate::validate($this->getParam('ssl_ca_file', true, $result['ssl_ca_file']), 'ssl_ca_file', '', '', [], true);
$ssl_cert_chainfile = Validate::validate($this->getParam('ssl_cert_chainfile', true, $result['ssl_cert_chainfile']), 'ssl_cert_chainfile', '', '', [], true);
$sslss = $this->getParam('ssl_specialsettings', true, $result['ssl_specialsettings']);

View File

@@ -26,14 +26,15 @@
namespace Froxlor\Api\Commands;
use Exception;
use PDO;
use PDOException;
use Froxlor\Froxlor;
use Froxlor\PhpHelper;
use Froxlor\Api\ApiCommand;
use Froxlor\Api\ResourceEntity;
use Froxlor\Database\Database;
use Froxlor\Froxlor;
use Froxlor\FroxlorLogger;
use Froxlor\PhpHelper;
use Froxlor\Validate\Validate;
use PDO;
use PDOException;
class MysqlServer extends ApiCommand implements ResourceEntity
{
@@ -73,8 +74,8 @@ class MysqlServer extends ApiCommand implements ResourceEntity
* optional, test connection with given credentials, default is true (yes)
*
* @access admin
* @throws Exception
* @return string json-encoded array
* @throws Exception
*/
public function add()
{
@@ -112,7 +113,7 @@ class MysqlServer extends ApiCommand implements ResourceEntity
);
if (!empty($mysql_ca)) {
$options[PDO::MYSQL_ATTR_SSL_CA] = $mysql_ca;
$options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool) $mysql_verifycert;
$options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool)$mysql_verifycert;
}
$dsn = "mysql:host=" . $mysql_host . ";port=" . $mysql_port . ";";
@@ -167,6 +168,8 @@ class MysqlServer extends ApiCommand implements ResourceEntity
$this->addDatabaseFromCustomerAllowedList($newdbserver);
}
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added new database server '" . $description . "' (" . $mysql_host . ")");
return $this->response(['dbserver' => $newdbserver]);
}
@@ -179,16 +182,16 @@ class MysqlServer extends ApiCommand implements ResourceEntity
* optional the number of the mysql server (either id or dbserver must be set)
*
* @access admin
* @throws Exception
* @return string json-encoded array
* @throws Exception
*/
public function delete()
{
$this->validateAccess();
$id = (int) $this->getParam('id', true, -1);
$id = (int)$this->getParam('id', true, -1);
$dn_optional = $id >= 0;
$dbserver = (int) $this->getParam('dbserver', $dn_optional, -1);
$dbserver = (int)$this->getParam('dbserver', $dn_optional, -1);
$dbserver = $id >= 0 ? $id : $dbserver;
if ($dbserver == 0) {
@@ -212,8 +215,12 @@ class MysqlServer extends ApiCommand implements ResourceEntity
// when removing, remove from list of allowed_mysqlservers from any customers
$this->removeDatabaseFromCustomerAllowedList($dbserver);
$description = $sql_root[$dbserver]['caption'] ?? "unknown";
$mysql_host = $sql_root[$dbserver]['host'] ?? "unknown";
unset($sql_root[$dbserver]);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] removed database server '" . $description . "' (" . $mysql_host . ")");
$this->generateNewUserData($sql, $sql_root);
return $this->response(['true']);
}
@@ -287,14 +294,14 @@ class MysqlServer extends ApiCommand implements ResourceEntity
* optional the number of the mysql server (either id or dbserver must be set)
*
* @access admin, customer
* @throws Exception
* @return string json-encoded array
* @throws Exception
*/
public function get()
{
$id = (int) $this->getParam('id', true, -1);
$id = (int)$this->getParam('id', true, -1);
$dn_optional = $id >= 0;
$dbserver = (int) $this->getParam('dbserver', $dn_optional, -1);
$dbserver = (int)$this->getParam('dbserver', $dn_optional, -1);
$dbserver = $id >= 0 ? $id : $dbserver;
$sql_root = [];
@@ -317,6 +324,7 @@ class MysqlServer extends ApiCommand implements ResourceEntity
unset($sql_root[$dbserver]['password']);
$sql_root[$dbserver]['id'] = $dbserver;
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] get database-server '" . $sql_root[$dbserver]['caption'] . "'");
return $this->response($sql_root[$dbserver]);
}
@@ -347,16 +355,16 @@ class MysqlServer extends ApiCommand implements ResourceEntity
* optional, test connection with given credentials, default is true (yes)
*
* @access admin
* @throws Exception
* @return string json-encoded array
* @throws Exception
*/
public function update()
{
$this->validateAccess();
$id = (int) $this->getParam('id', true, -1);
$id = (int)$this->getParam('id', true, -1);
$dn_optional = $id >= 0;
$dbserver = (int) $this->getParam('dbserver', $dn_optional, -1);
$dbserver = (int)$this->getParam('dbserver', $dn_optional, -1);
$dbserver = $id >= 0 ? $id : $dbserver;
$sql_root = [];
@@ -417,7 +425,7 @@ class MysqlServer extends ApiCommand implements ResourceEntity
);
if (!empty($mysql_ca)) {
$options[PDO::MYSQL_ATTR_SSL_CA] = $mysql_ca;
$options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool) $mysql_verifycert;
$options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool)$mysql_verifycert;
}
$dsn = "mysql:host=" . $mysql_host . ";port=" . $mysql_port . ";";
@@ -448,6 +456,8 @@ class MysqlServer extends ApiCommand implements ResourceEntity
$this->addDatabaseFromCustomerAllowedList($dbserver);
}
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] edited database server '" . $description . "' (" . $mysql_host . ")");
return $this->response(['true']);
}
@@ -472,7 +482,7 @@ class MysqlServer extends ApiCommand implements ResourceEntity
WHERE `dbserver` = :dbserver
");
$result = Database::pexecute_first($result_stmt, ['dbserver' => $dbserver], true, true);
return (int) $result['num_dbs'];
return (int)$result['num_dbs'];
} else {
$dbserver = $this->getParam('mysql_server');
$customer_ids = $this->getAllowedCustomerIds();

View File

@@ -199,7 +199,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
$this->mailer()->clearAddresses();
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] added mysql-database '" . $username . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added mysql-database '" . $username . "'");
$result = $this->apiCall('Mysqls.get', [
'dbname' => $username,
@@ -299,7 +299,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
$mbdata = $mbdata_stmt->fetch(PDO::FETCH_ASSOC);
Database::needRoot(false);
$result['size'] = $mbdata['MB'] ?? 0;
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get database '" . $result['databasename'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get database '" . $result['databasename'] . "'");
return $this->response($result);
}
$key = ($id > 0 ? "id #" . $id : "dbname '" . $dbname . "'");
@@ -388,7 +388,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
];
Database::pexecute($stmt, $params, true, true);
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] updated mysql-database '" . $result['databasename'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated mysql-database '" . $result['databasename'] . "'");
$result = $this->apiCall('Mysqls.get', [
'dbname' => $result['databasename']
]);

View File

@@ -67,7 +67,7 @@ class PhpSettings extends ApiCommand implements ResourceEntity
public function listing()
{
if ($this->isAdmin()) {
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list php-configs");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list php-configs");
$with_subdomains = $this->getBoolParam('with_subdomains', true, false);
$query_fields = [];
@@ -392,7 +392,7 @@ class PhpSettings extends ApiCommand implements ResourceEntity
$ins_data['id'] = Database::lastInsertId();
Cronjob::inserttask(TaskId::REBUILD_VHOST);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] php setting with description '" . $description . "' has been created by '" . $this->getUserDetail('loginname') . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] php setting with description '" . $description . "' has been created by '" . $this->getUserDetail('loginname') . "'");
$result = $this->apiCall('PhpSettings.get', [
'id' => $ins_data['id']
@@ -629,7 +629,7 @@ class PhpSettings extends ApiCommand implements ResourceEntity
Database::pexecute($upd_stmt, $upd_data, true, true);
Cronjob::inserttask(TaskId::REBUILD_VHOST);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] php setting with description '" . $description . "' has been updated by '" . $this->getUserDetail('loginname') . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] php setting with description '" . $description . "' has been updated by '" . $this->getUserDetail('loginname') . "'");
$result = $this->apiCall('PhpSettings.get', [
'id' => $id
@@ -686,7 +686,7 @@ class PhpSettings extends ApiCommand implements ResourceEntity
], true, true);
Cronjob::inserttask(TaskId::REBUILD_VHOST);
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] php setting '" . $result['description'] . "' has been deleted by '" . $this->getUserDetail('loginname') . "'");
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] php setting '" . $result['description'] . "' has been deleted by '" . $this->getUserDetail('loginname') . "'");
return $this->response($result);
}
throw new Exception("Not allowed to execute given command.", 403);

View File

@@ -486,7 +486,7 @@ class SubDomains extends ApiCommand implements ResourceEntity
$result['ipsandports'] = $this->getIpsForDomain($result['id']);
}
$result['domain_hascert'] = $this->getHasCertValueForDomain((int)$result['id'], (int)$result['parentdomainid']);
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] get subdomain '" . $result['domain'] . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get subdomain '" . $result['domain'] . "'");
return $this->response($result);
}
$key = ($id > 0 ? "id #" . $id : "domainname '" . $domainname . "'");
@@ -856,7 +856,7 @@ class SubDomains extends ApiCommand implements ResourceEntity
Cronjob::inserttask(TaskId::REBUILD_VHOST);
Cronjob::inserttask(TaskId::REBUILD_DNS);
$idna_convert = new IdnaWrapper();
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] edited domain '" . $idna_convert->decode($result['domain']) . "'");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] edited domain '" . $idna_convert->decode($result['domain']) . "'");
}
$result = $this->apiCall('SubDomains.get', [
'id' => $id

View File

@@ -92,7 +92,7 @@ class SysLog extends ApiCommand implements ResourceEntity
while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) {
$result[] = $row;
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list log-entries");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list log-entries");
return $this->response([
'count' => count($result),
'list' => $result

View File

@@ -166,7 +166,7 @@ class Traffic extends ApiCommand implements ResourceEntity
$row['mail'] *= 1024;
$result[] = $row;
}
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] list traffic");
$this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list traffic");
return $this->response([
'count' => count($result),
'list' => $result

View File

@@ -0,0 +1,178 @@
<?php
/**
* This file is part of the Froxlor project.
* Copyright (c) 2010 the Froxlor Team (see authors).
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you can also view it online at
* https://files.froxlor.org/misc/COPYING.txt
*
* @copyright the authors
* @author Froxlor team <team@froxlor.org>
* @license https://files.froxlor.org/misc/COPYING.txt GPLv2
*/
namespace Froxlor\Cli;
use Froxlor\Config\ConfigParser;
use Froxlor\FileDir;
use Froxlor\Froxlor;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
final class ConfigDiff extends CliCommand
{
protected function configure(): void
{
$this->setName('froxlor:config-diff')
->setDescription('Shows differences in config templates between OS versions')
->addArgument('from', InputArgument::OPTIONAL, 'OS version to compare against')
->addArgument('to', InputArgument::OPTIONAL, 'OS version to compare from')
->addOption('list', 'l', InputOption::VALUE_NONE, 'List all possible OS versions')
->addOption('diff-params', '', InputOption::VALUE_REQUIRED, 'Additional parameters for `diff`, e.g. --diff-params="--color=always"');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
require Froxlor::getInstallDir() . '/lib/functions.php';
$parsers = $versions = [];
foreach (glob(Froxlor::getInstallDir() . '/lib/configfiles/*.xml') as $config) {
$name = str_replace(".xml", "", strtolower(basename($config)));
$parser = new ConfigParser($config);
$versions[$name] = $parser->getCompleteDistroName();
$parsers[$name] = $parser;
}
asort($versions);
if ($input->getOption('list') === true) {
$output->writeln('The following OS version templates are available:');
foreach ($versions as $k => $v) {
$output->writeln(str_pad($k, 20) . $v);
}
return self::SUCCESS;
}
if (!$input->hasArgument('from') || !array_key_exists($input->getArgument('from'), $versions)) {
$output->writeln('<error>Missing or invalid "from" argument.</error>');
$output->writeln('Available versions: ' . implode(', ', array_keys($versions)));
return self::INVALID;
}
if (!$input->hasArgument('to') || !array_key_exists($input->getArgument('to'), $versions)) {
$output->writeln('<error>Missing or invalid "to" argument.</error>');
$output->writeln('Available versions: ' . implode(', ', array_keys($versions)));
return self::INVALID;
}
// Make sure diff is installed
$check_diff_installed = FileDir::safe_exec('which diff');
if (count($check_diff_installed) === 0) {
$output->writeln('<error>Unable to find "diff" installation on your system.</error>');
return self::INVALID;
}
$parser_from = $parsers[$input->getArgument('from')];
$parser_to = $parsers[$input->getArgument('to')];
$tmp_from = tempnam(sys_get_temp_dir(), 'froxlor_config_diff_from');
$tmp_to = tempnam(sys_get_temp_dir(), 'froxlor_config_diff_to');
$files = [];
$titles_by_key = [];
// Aggregate content for each config file
foreach ([[$parser_from, 'from'], [$parser_to, 'to']] as $todo) {
foreach ($todo[0]->getServices() as $service_type => $service) {
foreach ($service->getDaemons() as $daemon_name => $daemon) {
foreach ($daemon->getConfig() as $instruction) {
if ($instruction['type'] !== 'file') {
continue;
}
if (isset($instruction['subcommands'])) {
foreach ($instruction['subcommands'] as $subinstruction) {
if ($subinstruction['type'] !== 'file') {
continue;
}
$content = $subinstruction['content'];
}
} else {
$content = $instruction['content'];
}
if (!isset($content)) {
throw new \Exception("Cannot find content for {$instruction['name']}");
}
$key = "{$service_type}_{$daemon_name}_{$instruction['name']}";
$titles_by_key[$key] = "{$service->title} : {$daemon->title} : {$instruction['name']}";
if (!isset($files[$key])) {
$files[$key] = ['from' => '', 'to' => ''];
}
$files[$key][$todo[1]] = $this->filterContent($content);
}
}
}
}
ksort($files);
$diff_params = '';
if ($input->hasOption('diff-params') && trim($input->getOption('diff-params')) !== '') {
$diff_params = trim($input->getOption('diff-params'));
}
// Run diff on each file and output, if anything changed
foreach ($files as $file_key => $content) {
file_put_contents($tmp_from, $content['from']);
file_put_contents($tmp_to, $content['to']);
$diff_output = FileDir::safe_exec("{$check_diff_installed[0]} {$diff_params} {$tmp_from} {$tmp_to}");
if (count($diff_output) === 0) {
continue;
}
$output->writeln('<info># ' . $titles_by_key[$file_key] . '</info>');
$output->writeln(implode("\n", $diff_output) . "\n");
unset($diff_output);
}
// Remove tmp files again
unlink($tmp_from);
unlink($tmp_to);
return self::SUCCESS;
}
private function filterContent(string $content): string
{
$new_content = '';
foreach (explode("\n", $content) as $n) {
$n = trim($n);
if (!$n) {
continue;
}
if (str_starts_with($n, '#')) {
continue;
}
$new_content .= $n . "\n";
}
return $new_content;
}
}

View File

@@ -236,9 +236,12 @@ class AcmeSh extends FroxlorCron
return false;
}
private static function checkFsFilesAreNewer($domain, $cert_date = 0)
private static function checkFsFilesAreNewer($domain, $cert_date = 0): bool
{
$certificate_folder = self::getWorkingDirFromEnv(strtolower($domain));
$certificate_folder = self::getCertificateFolder(strtolower($domain));
if (empty($certificate_folder)) {
return false;
}
$ssl_file = FileDir::makeCorrectFile($certificate_folder . '/' . strtolower($domain) . '.cer');
if (is_dir($certificate_folder) && file_exists($ssl_file) && is_readable($ssl_file)) {
@@ -250,9 +253,13 @@ class AcmeSh extends FroxlorCron
return false;
}
public static function getWorkingDirFromEnv($domain = "", $forced_noecc = false)
public static function getWorkingDirFromEnv($domain = "", $forced_ecc = false): string
{
if (Settings::Get('system.leecc') > 0 && !$forced_noecc) {
// first try without _ecc either if it's enabled currently or not as
// it might have been at some point so there is a chance we have certificates
// with and without _ecc - the method getCertificateFolder() will check both
// possibilities
if ($forced_ecc) {
$domain .= "_ecc";
}
$env_file = FileDir::makeCorrectFile(dirname(self::getAcmeSh()) . '/acme.sh.env');
@@ -262,7 +269,7 @@ class AcmeSh extends FroxlorCron
cut -d'"' -f2
EOC;
exec('grep "LE_WORKING_DIR" ' . escapeshellarg($env_file) . ' | ' . $cut, $output);
if (is_array($output) && !empty($output) && isset($output[0]) && !empty($output[0])) {
if (is_array($output) && !empty($output) && !empty($output[0])) {
return FileDir::makeCorrectDir($output[0] . "/" . $domain);
}
}
@@ -635,35 +642,21 @@ EOC;
*/
private static function readCertificateToVar($domain, &$return, &$cronlog)
{
$certificate_folder = self::getWorkingDirFromEnv($domain);
$certificate_folder_noecc = null;
if (Settings::Get('system.leecc') > 0) {
$certificate_folder_noecc = self::getWorkingDirFromEnv($domain, true);
}
$certificate_folder = FileDir::makeCorrectDir($certificate_folder);
$certificate_folder = self::getCertificateFolder($domain);
if (is_dir($certificate_folder) || is_dir($certificate_folder_noecc)) {
foreach (
[
'crt' => $domain . '.cer',
'key' => $domain . '.key',
'chain' => 'ca.cer',
'fullchain' => 'fullchain.cer',
'csr' => $domain . '.csr'
] as $index => $sslfile
) {
if (!empty($certificate_folder)) {
$certificate_files = [
'crt' => $domain . '.cer',
'key' => $domain . '.key',
'chain' => 'ca.cer',
'fullchain' => 'fullchain.cer',
'csr' => $domain . '.csr'
];
foreach ($certificate_files as $index => $sslfile) {
$ssl_file = FileDir::makeCorrectFile($certificate_folder . '/' . $sslfile);
if (file_exists($ssl_file)) {
$return[$index] = file_get_contents($ssl_file);
} else {
if (!empty($certificate_folder_noecc)) {
$ssl_file_fb = FileDir::makeCorrectFile($certificate_folder_noecc . '/' . $sslfile);
if (file_exists($ssl_file_fb)) {
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "ECC certificates activated but found only non-ecc file");
$return[$index] = file_get_contents($ssl_file_fb);
continue;
}
}
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not find file '" . $sslfile . "' in '" . $certificate_folder . "'");
$return[$index] = null;
}
@@ -672,4 +665,18 @@ EOC;
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not find certificate-folder '" . $certificate_folder . "'");
}
}
private static function getCertificateFolder(string $domain): string
{
$certificate_folder = self::getWorkingDirFromEnv(strtolower($domain));
if (file_exists($certificate_folder)) {
return $certificate_folder;
}
$certificate_folder_ecc = self::getWorkingDirFromEnv($domain, true);
if (file_exists($certificate_folder_ecc)) {
return $certificate_folder_ecc;
}
FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not find certificate-folder for domain '" . $domain . "'");
return "";
}
}

View File

@@ -225,7 +225,7 @@ class Nginx extends HttpConfigBase
$this->nginx_data[$vhost_filename] .= "\t" . '}' . "\n";
// protect bin/
$this->nginx_data[$vhost_filename] .= "\t" . 'location ~ ' . rtrim($relpath, "/") . '/(bin|cache|logs|tests|vendor) {' . "\n";
$this->nginx_data[$vhost_filename] .= "\t" . 'location ~ ^' . rtrim($relpath, "/") . '/(bin|cache|logs|tests|vendor) {' . "\n";
$this->nginx_data[$vhost_filename] .= "\t" . ' deny all;' . "\n";
$this->nginx_data[$vhost_filename] .= "\t" . '}' . "\n";
}
@@ -883,13 +883,7 @@ class Nginx extends HttpConfigBase
// remove comments
$vhost = implode("\n", preg_replace('/^(\s+)?#(.*)$/', '', explode("\n", $vhost)));
// Break blocks into lines
$vhost = str_replace([
"{",
"}"
], [
" {\n",
"\n}"
], $vhost);
$vhost = preg_replace("/^(\s+)?location(.+)\{(.+)\}$/misU", "location $2 {\n $3 \n}", $vhost);
// Break into array items
$vhost = explode("\n", preg_replace('/[ \t]+/', ' ', trim(preg_replace('/\t+/', '', $vhost))));
// Remove empty lines
@@ -1171,7 +1165,6 @@ class Nginx extends HttpConfigBase
$phpopts .= "\t\tinclude " . Settings::Get('nginx.fastcgiparams') . ";\n";
$phpopts .= "\t\tfastcgi_param SCRIPT_FILENAME \$request_filename;\n";
$phpopts .= "\t\tfastcgi_param PATH_INFO \$fastcgi_path_info;\n";
$phpopts .= "\t\ttry_files \$fastcgi_script_name =404;\n";
$phpopts .= "\t\tfastcgi_pass " . Settings::Get('system.nginx_php_backend') . ";\n";
$phpopts .= "\t\tfastcgi_index index.php;\n";
if ($domain['ssl'] == '1' && $ssl_vhost) {

View File

@@ -25,10 +25,10 @@
namespace Froxlor;
use Froxlor\Database\Database;
use Froxlor\UI\Collection;
use Froxlor\Api\Commands\Customers;
use Froxlor\Api\Commands\SubDomains;
use Froxlor\Database\Database;
use Froxlor\UI\Collection;
/**
* Class to manage the current user / session
@@ -151,9 +151,13 @@ class CurrentUser
]);
$addition = $result['emaildomains'] != 0;
} elseif ($resource == 'subdomains') {
$parentDomainCollection = (new Collection(SubDomains::class, $_SESSION['userinfo'],
['sql_search' => ['d.parentdomainid' => 0]]));
$addition = $parentDomainCollection != 0;
if (Settings::IsInList('panel.customer_hide_options', 'domains')) {
$addition = false;
} else {
$parentDomainCollection = (new Collection(SubDomains::class, $_SESSION['userinfo'],
['sql_search' => ['d.parentdomainid' => 0]]));
$addition = $parentDomainCollection->count() != 0;
}
} elseif ($resource == 'domains') {
$customerCollection = (new Collection(Customers::class, $_SESSION['userinfo']));
$addition = $customerCollection != 0;

View File

@@ -55,6 +55,7 @@ class IpAddr
/**
* @return array
* @throws \Exception
*/
public static function getSslIpPortCombinations(): array
{
@@ -75,7 +76,7 @@ class IpAddr
$additional_conditions_params = [];
$additional_conditions_array = [];
if ($userinfo['ip'] != '-1') {
if (!empty($userinfo) && $userinfo['ip'] != '-1') {
$admin_ip_stmt = Database::prepare("
SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = IN (:ipid)
");

View File

@@ -31,7 +31,7 @@ final class Froxlor
{
// Main version variable
const VERSION = '2.0.17';
const VERSION = '2.0.24';
// Database version (YYYYMMDDC where C is a daily counter)
const DBVERSION = '202304260';

View File

@@ -42,7 +42,7 @@ class Install
public $phpVersion;
public $formfield;
public string $requiredVersion = '7.4.0';
public array $requiredExtensions = ['session', 'ctype', 'mysql', 'xml', 'filter', 'posix', 'mbstring', 'curl', 'gmp', 'json', 'gd'];
public array $requiredExtensions = ['session', 'ctype', 'xml', 'filter', 'posix', 'mbstring', 'curl', 'gmp', 'json', 'gd'];
public array $suggestedExtensions = ['bcmath', 'zip'];
public array $suggestions = [];
public array $criticals = [];

View File

@@ -448,7 +448,11 @@ class Core
$reload = "service php" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION . "-fpm restart";
$config_dir = "/etc/php/" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION . "/fpm/pool.d/";
// fcgid
$binary = "/usr/bin/php" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION . "-cgi";
if ($this->validatedData['distribution'] == 'bookworm') {
$binary = "/usr/bin/php-cgi" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;
} else {
$binary = "/usr/bin/php" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION . "-cgi";
}
}
$db_user->query("UPDATE `" . TABLE_PANEL_FPMDAEMONS . "` SET `reload_cmd` = '" . $reload . "', `config_dir` = '" . $config_dir . "' WHERE `id` ='1';");
$db_user->query("UPDATE `" . TABLE_PANEL_PHPCONFIGS . "` SET `binary` = '" . $binary . "';");

View File

@@ -449,7 +449,15 @@ class PhpHelper
'ssl_specialsettings',
'default_vhostconf_domain',
'ssl_default_vhostconf_domain',
'filecontent'
'filecontent',
'admin_password',
'password',
'new_customer_password',
'privileged_password',
'email_password',
'directory_password',
'ftp_password',
'mysql_password',
];
if (!empty($global)) {
$tmp = $global;

View File

@@ -87,16 +87,38 @@ class UI
return $isHttps && (strcasecmp('on', $isHttps) == 0 || strcasecmp('https', $isHttps) == 0);
}
/**
* Extract the cookie host from HTTP_HOST, stripping the port.
*/
public static function getCookieHost(): ?string
{
if (empty($_SERVER['HTTP_HOST']))
return null;
$colonPosition = strrpos($_SERVER['HTTP_HOST'], ':');
// There's no port in the host
if ($colonPosition === false)
return $_SERVER['HTTP_HOST'];
$closingSquareBracketPosition = strrpos($_SERVER['HTTP_HOST'], ']');
// The host is an IPv4 address or hostname with port
if ($closingSquareBracketPosition === false)
return substr($_SERVER['HTTP_HOST'], 0, $colonPosition);
// The host is an IPv6 address with port
return substr($_SERVER['HTTP_HOST'], 0, $closingSquareBracketPosition + 1);
}
/**
* send various security related headers
*/
public static function sendHeaders()
{
$cookie_host = empty($_SERVER['HTTP_HOST']) ? null : explode (':', $_SERVER['HTTP_HOST'])[0];
session_set_cookie_params([
'lifetime' => self::$install_mode ? 7200 : 600, // will be renewed based on settings in lib/init.php
'path' => '/',
'domain' => $cookie_host,
'domain' => self::getCookieHost(),
'secure' => self::requestIsHttps(),
'httponly' => true,
'samesite' => 'Strict'
@@ -268,7 +290,8 @@ class UI
];
}
public static function validateThemeTemplate(string $name, string $theme = "") {
public static function validateThemeTemplate(string $name, string $theme = "")
{
if (empty(trim($theme))) {
$theme = self::getTheme();
}

View File

@@ -52,16 +52,16 @@ class Check
];
$check_array = [
'system_mod_fcgid_enabled' => [
'other_post_field' => 'system_phpfpm_enabled',
'system_mod_fcgid' => [
'other_post_field' => 'phpfpm_enabled',
'other_enabled' => 'phpfpm.enabled',
'other_enabled_lng' => 'phpfpmstillenabled',
'deactivate' => [
'phpfpm.enabled_ownvhost' => 0
]
],
'system_phpfpm_enabled' => [
'other_post_field' => 'system_mod_fcgid_enabled',
'phpfpm_enabled' => [
'other_post_field' => 'system_mod_fcgid',
'other_enabled' => 'system.mod_fcgid',
'other_enabled_lng' => 'fcgidstillenabled',
'deactivate' => [

View File

@@ -260,7 +260,7 @@ class Validate
}
/**
* Returns if an emailaddress is in correct format or not
* Returns if an email-address is in correct format or not
*
* @param string $email The email address to check
*

View File

@@ -2600,7 +2600,6 @@ ServerName "<SERVERNAME> FTP Server"
ServerType standalone
DeferWelcome off
MultilineRFC2228 on
DefaultServer on
ShowSymlinks on
@@ -2939,7 +2938,6 @@ SQLNamedQuery get-quota-limit SELECT "ftp_users.username AS name, ftp_quotalimit
SQLNamedQuery get-quota-tally SELECT "name, quota_type, bytes_in_used,bytes_out_used, bytes_xfer_used, files_in_used, files_out_used,files_xfer_used FROM ftp_quotatallies WHERE name = '%{0}' AND quota_type = '%{1}'"
SQLNamedQuery update-quota-tally UPDATE "bytes_in_used = bytes_in_used + %{0}, bytes_out_used = bytes_out_used + %{1}, bytes_xfer_used = bytes_xfer_used + %{2}, files_in_used = files_in_used + %{3}, files_out_used= files_out_used + %{4}, files_xfer_used = files_xfer_used + %{5} WHERE name= '%{6}' AND quota_type = '%{7}'" ftp_quotatallies
SQLNamedQuery insert-quota-tally INSERT "%{0}, %{1}, %{2}, %{3}, %{4},%{5}, %{6}, %{7}" ftp_quotatallies
</IfModule>
]]>
</content>
@@ -2955,7 +2953,7 @@ TLSRSACertificateFile /etc/ssl/certs/proftpd.crt
TLSRSACertificateKeyFile /etc/ssl/private/proftpd.key
TLSECCertificateFile /etc/ssl/certs/proftpd_ec.crt
TLSECCertificateKeyFile /etc/ssl/private/proftpd_ec.key
TLSOptions NoCertRequest NoSessionReuseRequired
TLSOptions NoSessionReuseRequired
TLSVerifyClient off
# Are clients required to use FTP over TLS when talking to this server?
@@ -3317,7 +3315,7 @@ aliases: files
<command><![CDATA[mkdir -p {{settings.system.mod_fcgid_configdir}}]]></command>
<command><![CDATA[mkdir -p {{settings.system.mod_fcgid_tmpdir}}]]></command>
<command><![CDATA[chmod 1777 {{settings.system.mod_fcgid_tmpdir}}]]></command>
<command><![CDATA[a2dismod php8.1]]></command>
<command><![CDATA[a2dismod php8.2]]></command>
</commands>
<!-- instead of just restarting apache, we let the cronjob do all the
dirty work -->
@@ -3350,7 +3348,7 @@ aliases: files
</visibility>
<visibility mode="true">{{settings.phpfpm.enabled_ownvhost}}
</visibility>
<command><![CDATA[a2dismod php8.1]]></command>
<command><![CDATA[a2dismod php8.2]]></command>
</commands>
<!-- instead of just restarting apache, we let the cronjob do all the
dirty work -->

View File

@@ -133,7 +133,7 @@ return [
'customers' => [
'label' => lng('admin.customers'),
'type' => 'textul',
'value' => $result['customers'],
'value' => empty($result['customers']) ? '0' : $result['customers'],
'maxlength' => 9,
'mandatory' => true
],
@@ -146,7 +146,7 @@ return [
'domains' => [
'label' => lng('admin.domains'),
'type' => 'textul',
'value' => $result['domains'],
'value' => empty($result['domains']) ? '0' : $result['domains'],
'maxlength' => 9,
'mandatory' => true
],
@@ -159,49 +159,49 @@ return [
'diskspace' => [
'label' => lng('customer.diskspace') . ' (' . lng('customer.mib') . ')',
'type' => 'textul',
'value' => $result['diskspace'],
'value' => empty($result['diskspace']) ? '0' : $result['diskspace'],
'maxlength' => 6,
'mandatory' => true
],
'traffic' => [
'label' => lng('customer.traffic') . ' (' . lng('customer.gib') . ')',
'type' => 'textul',
'value' => $result['traffic'],
'value' => empty($result['traffic']) ? '0' : $result['traffic'],
'maxlength' => 4,
'mandatory' => true
],
'subdomains' => [
'label' => lng('customer.subdomains'),
'type' => 'textul',
'value' => $result['subdomains'],
'value' => empty($result['subdomains']) ? '0' : $result['subdomains'],
'maxlength' => 9,
'mandatory' => true
],
'emails' => [
'label' => lng('customer.emails'),
'type' => 'textul',
'value' => $result['emails'],
'value' => empty($result['emails']) ? '0' : $result['emails'],
'maxlength' => 9,
'mandatory' => true
],
'email_accounts' => [
'label' => lng('customer.accounts'),
'type' => 'textul',
'value' => $result['email_accounts'],
'value' => empty($result['email_accounts']) ? '0' : $result['email_accounts'],
'maxlength' => 9,
'mandatory' => true
],
'email_forwarders' => [
'label' => lng('customer.forwarders'),
'type' => 'textul',
'value' => $result['email_forwarders'],
'value' => empty($result['email_forwarders']) ? '0' : $result['email_forwarders'],
'maxlength' => 9,
'mandatory' => true
],
'email_quota' => [
'label' => lng('customer.email_quota') . ' (' . lng('customer.mib') . ')',
'type' => 'textul',
'value' => $result['email_quota'],
'value' => empty($result['email_quota']) ? '0' : $result['email_quota'],
'maxlength' => 9,
'visible' => Settings::Get('system.mail_quota_enabled') == '1',
'mandatory' => true
@@ -209,13 +209,13 @@ return [
'ftps' => [
'label' => lng('customer.ftps'),
'type' => 'textul',
'value' => $result['ftps'],
'value' => empty($result['ftps']) ? '0' : $result['ftps'],
'maxlength' => 9
],
'mysqls' => [
'label' => lng('customer.mysqls'),
'type' => 'textul',
'value' => $result['mysqls'],
'value' => empty($result['mysqls']) ? '0' : $result['mysqls'],
'maxlength' => 9,
'mandatory' => true
]

View File

@@ -43,13 +43,16 @@ return [
'email_password' => [
'label' => lng('login.password'),
'type' => 'password',
'autocomplete' => 'off'
],
'email_password_suggestion' => [
'label' => lng('customer.generated_pwd'),
'type' => 'text',
'visible' => (Settings::Get('panel.password_regex') == ''),
'value' => Crypt::generatePassword()
'autocomplete' => 'off',
'next_to' => [
'email_password_suggestion' => [
'next_to_prefix' => lng('customer.generated_pwd') . ':',
'type' => 'text',
'visible' => (Settings::Get('panel.password_regex') == ''),
'value' => Crypt::generatePassword(),
'readonly' => true
]
]
]
]
]

View File

@@ -46,7 +46,7 @@ return [
'autocomplete' => 'off',
'mandatory' => true,
'next_to' => [
'admin_password_suggestion' => [
'email_password_suggestion' => [
'next_to_prefix' => lng('customer.generated_pwd') . ':',
'type' => 'text',
'visible' => (Settings::Get('panel.password_regex') == ''),

View File

@@ -181,8 +181,10 @@ if (@file_exists('templates/' . $theme . '/config.json')) {
}
// check for existence of variant in theme
if (is_array($_themeoptions) && (!array_key_exists('variants', $_themeoptions) || !array_key_exists($themevariant,
$_themeoptions['variants']))) {
if (is_array($_themeoptions) && (!array_key_exists('variants', $_themeoptions) || !array_key_exists(
$themevariant,
$_themeoptions['variants']
))) {
$themevariant = "default";
}
@@ -216,12 +218,11 @@ UI::twig()->addGlobal('header_logo', $header_logo);
if (!CurrentUser::hasSession() && AREA != 'login') {
unset($_SESSION['userinfo']);
CurrentUser::setData();
session_destroy();
$params = [
"script" => basename($_SERVER["SCRIPT_NAME"]),
"qrystr" => $_SERVER["QUERY_STRING"]
$_SESSION = [
"lastscript" => basename($_SERVER["SCRIPT_NAME"]),
"lastqrystr" => $_SERVER["QUERY_STRING"]
];
Response::redirectTo('index.php', $params);
Response::redirectTo('index.php');
exit();
}
@@ -331,11 +332,10 @@ if (CurrentUser::hasSession()) {
}
}
// update cookie lifetime
$cookie_host = empty($_SERVER['HTTP_HOST']) ? null : explode (':', $_SERVER['HTTP_HOST'])[0];
$cookie_params = [
'expires' => time() + Settings::Get('session.sessiontimeout'),
'path' => '/',
'domain' => $cookie_host,
'domain' => UI::getCookieHost(),
'secure' => UI::requestIsHttps(),
'httponly' => true,
'samesite' => 'Strict'

View File

@@ -38,7 +38,7 @@ return [
'url' => 'customer_email.php?page=emails',
'label' => lng('menue.email.emails'),
'required_resources' => 'emails',
'add_shortlink' => CurrentUser::canAddResource('emails') ? 'customer_email.php?page=emails&action=add' : null,
'add_shortlink' => !CurrentUser::isAdmin() && CurrentUser::canAddResource('emails') ? 'customer_email.php?page=email_domain&action=add' : null,
],
[
'url' => Settings::Get('panel.webmail_url'),
@@ -60,7 +60,7 @@ return [
'url' => 'customer_mysql.php?page=mysqls',
'label' => lng('menue.mysql.databases'),
'required_resources' => 'mysqls',
'add_shortlink' => CurrentUser::canAddResource('mysqls')? 'customer_mysql.php?page=mysqls&action=add' : null,
'add_shortlink' => !CurrentUser::isAdmin() && CurrentUser::canAddResource('mysqls')? 'customer_mysql.php?page=mysqls&action=add' : null,
],
[
'url' => Settings::Get('panel.phpmyadmin_url'),
@@ -81,7 +81,7 @@ return [
[
'url' => 'customer_domains.php?page=domains',
'label' => lng('menue.domains.settings'),
'add_shortlink' => CurrentUser::canAddResource('subdomains') ? 'customer_domains.php?page=domains&action=add' : null,
'add_shortlink' => !CurrentUser::isAdmin() && CurrentUser::canAddResource('subdomains') ? 'customer_domains.php?page=domains&action=add' : null,
],
[
'url' => 'customer_domains.php?page=sslcertificates',
@@ -98,7 +98,7 @@ return [
[
'url' => 'customer_ftp.php?page=accounts',
'label' => lng('menue.ftp.accounts'),
'add_shortlink' => CurrentUser::canAddResource('ftps') ? 'customer_ftp.php?page=accounts&action=add' : null,
'add_shortlink' => !CurrentUser::isAdmin() && CurrentUser::canAddResource('ftps') ? 'customer_ftp.php?page=accounts&action=add' : null,
],
[
'url' => Settings::Get('panel.webftp_url'),

2406
lng/ca.lng.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -926,6 +926,7 @@ return [
'domaincannotbeedited' => 'Keine Berechtigung, um die Domain %s zu bearbeiten',
'invalidcronjobintervalvalue' => 'Cronjob Intervall muss einer der folgenden Werte sein: %s',
'phpgdextensionnotavailable' => 'Die PHP GD Extension ist nicht verfügbar. Bild-Daten können nicht validiert werden.',
'2fa_wrongcode' => 'Der angegebene Code ist nicht korrekt',
],
'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.',

View File

@@ -34,6 +34,7 @@ return [
'pt' => 'Portuguese',
'se' => 'Swedish',
'es' => 'Spanish',
'ca' => 'Catalan',
],
'2fa' => [
'2fa' => '2FA options',
@@ -995,6 +996,7 @@ return [
'domaincannotbeedited' => 'You are not permitted to edit the domain %s',
'invalidcronjobintervalvalue' => 'Cronjob interval must be one of: %s',
'phpgdextensionnotavailable' => 'The PHP GD extension is not available. Unable to validate image-data',
'2fa_wrongcode' => 'The code entered is not valid',
],
'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.',
@@ -1193,7 +1195,7 @@ Yours sincerely, your administrator',
'database_edit' => 'Edit database',
'size' => 'Size',
'privileged_user' => 'Privileged database user',
'privileged_passwd' => 'Password for priviliged user',
'privileged_passwd' => 'Password for privileged user',
'unprivileged_passwd' => 'Password for unprivileged user',
'mysql_ssl_ca_file' => 'SSL server certificate',
'mysql_ssl_verify_server_certificate' => 'Verify SSL server certificate'
@@ -1262,7 +1264,7 @@ Yours sincerely, your administrator',
'reset' => 'Discard changes',
'pathDescription' => 'If the directory doesn\'t exist, it will be created automatically.',
'pathDescriptionEx' => '<br /><br /><span class="text-danger">Please note:</span> The path <code>/</code> is not allowed due to administrative settings, it will automatically be set to <code>/chosen.subdomain.tld/</code> if not set to another directory.',
'pathDescriptionSubdomain' => 'If the directory doesn\'t exist, it will be created automatically.<br /><br />If you want a redirect to another domain than this entry has to start with http:// or https://.<br /><br />If the URL ends with / it is considered a folder, if not, it is treated as file.',
'pathDescriptionSubdomain' => 'If the directory doesn\'t exist, it will be created automatically.<br /><br />If you want a redirect to another domain then this entry has to start with http:// or https://.<br /><br />If the URL ends with / it is considered a folder, if not, it is treated as file.',
'back' => 'Back',
'reseller' => 'reseller',
'admin' => 'admin',

10455
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
<?php
chmod('/app//bin/froxlor-cli', 0755);
// re-create cron.d configuration file
exec('/app//bin/froxlor-cli froxlor:cron -r 99');
exit;

View File

@@ -10,6 +10,13 @@
<div class="card-body">
<h5 class="card-title">{{ pagetitle }}</h5>
{% if message is not empty %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">{{ lng('error.error') }}</h4>
<p>{{ message|raw }}</p>
</div>
{% endif %}
<div class="mb-3">
<label for="2fa_code" class="col-form-label">{{ lng('login.2facode') }}</label>
<input class="form-control" type="text" name="2fa_code" id="2fa_code" value="" autocomplete="off" autofocus required/>

View File

@@ -3,7 +3,7 @@
{% block content %}
<div class="container">
<div class="row justify-content-center">
<form class="col-12 max-w-420 d-flex flex-column" method="post" enctype="application/x-www-form-urlencoded">
<form action="{{ formaction }}" class="col-12 max-w-420 d-flex flex-column" method="post" enctype="application/x-www-form-urlencoded">
<img class="align-self-center my-5" src="{{ header_logo_login }}" alt="Froxlor Server Management Panel"/>
<div class="card shadow">
@@ -38,8 +38,6 @@
</div>
<div class="card-body d-grid gap-2">
<input type="hidden" name="action" value="{{ action }}"/>
<input type="hidden" name="send" value="send"/>
<button class="btn btn-primary rounded-top-0" type="submit" name="doremind">{{ lng('login.remind') }}</button>
</div>

View File

@@ -39,9 +39,6 @@
</div>
<div class="card-body d-grid gap-2">
<input type="hidden" name="script" value="{{ lastscript }}"/>
<input type="hidden" name="qrystr" value="{{ lastqrystr|raw }}"/>
<input type="hidden" name="send" value="send"/>
<button class="btn btn-primary rounded-top-0" type="submit" name="dologin">{{ lng('login.login') }}</button>
</div>

View File

@@ -30,8 +30,6 @@
</div>
<div class="card-body d-grid gap-2">
<input type="hidden" name="action" value="resetpwd"/>
<input type="hidden" name="send" value="send"/>
<button class="btn btn-primary rounded-top-0" type="submit" name="doremind">{{ lng('login.remind') }}</button>
</div>

View File

@@ -0,0 +1,192 @@
:root,[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
/* --bs-gray-dark:#343a40; */
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #1872a2;
--bs-secondary: #6c757d;
--bs-success: #059669;
--bs-info: #0e5380;
--bs-warning: #fbbf24;
--bs-danger: #be123c;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 180,170,160;
--bs-secondary-rgb: 218,212,208;
--bs-success-rgb: 5,150,105;
--bs-info-rgb: 14,83,128;
--bs-warning-rgb: 251,191,36;
--bs-danger-rgb: 190,18,60;
--bs-light-rgb: 248,249,250;
--bs-dark-rgb: 218,212,208;
--bs-primary-text-emphasis: #0a2e41;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #023c2a;
--bs-info-text-emphasis: #062133;
--bs-warning-text-emphasis: #644c0e;
--bs-danger-text-emphasis: #4c0718;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #d1e3ec;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #cdeae1;
--bs-info-bg-subtle: #cfdde6;
--bs-warning-bg-subtle: #fef2d3;
--bs-danger-bg-subtle: #f2d0d8;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #a3c7da;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #9bd5c3;
--bs-info-border-subtle: #9fbacc;
--bs-warning-border-subtle: #fde5a7;
--bs-danger-border-subtle: #e5a0b1;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255,255,255;
--bs-black-rgb: 0,0,0;
--bs-font-sans-serif: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
--bs-font-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
--bs-gradient: linear-gradient(180deg,hsla(0,0%,100%,.15),hsla(0,0%,100%,0));
--bs-root-font-size: 16px;
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #343a40;
--bs-body-color-rgb: 52,58,64;
--bs-body-bg: #f8f9fa;
--bs-body-bg-rgb: 248,249,250;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0,0,0;
--bs-secondary-color: rgba(52,58,64,.75);
--bs-secondary-color-rgb: 52,58,64;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233,236,239;
--bs-tertiary-color: rgba(52,58,64,.5);
--bs-tertiary-color-rgb: 52,58,64;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248,249,250;
--bs-heading-color: inherit;
--bs-link-color: #1872a2;
--bs-link-color-rgb: 24,114,162;
--bs-link-decoration: underline;
--bs-link-hover-color: #135b82;
--bs-link-hover-color-rgb: 19,91,130;
--bs-code-color: #d63384;
--bs-highlight-color: #343a40;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0,0,0,.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0,0,0,.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0,0,0,.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0,0,0,.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(24,114,162,.25);
--bs-form-valid-color: #059669;
--bs-form-valid-border-color: #059669;
--bs-form-invalid-color: #be123c;
--bs-form-invalid-border-color: #be123c
}
.navbar .navbar-brand {
background: rgb(180,170,160);
flex-shrink: 0;
margin-right: 0;
width: 256px;
}
.sidebar>.nav>.nav-item>.nav-link:not(.collapsed) {
background: rgb(180,170,160);
border-left: 3px solid #1872a2;
padding-left: calc(1rem - 3px);
}
.text-light {
--bs-text-opacity: 1;
color: rgba(var(--bs-gray-900),var(--bs-text-opacity))!important;
}
.sidebar>.nav>.nav-item>.collapse, .sidebar>.nav>.nav-item>.collapsing {
background: rgb(160,140,120);
color: rgba(var(--bs-light),var(--bs-text-opacity))!important;
}
img.header-logo {
width: 80%;
height: 80%;
}
.btn-primary {
--bs-btn-color: #fff;
--bs-btn-bg: rgb(180,170,160);
--bs-btn-border-color: rgb(160,140,120);
--bs-btn-hover-color: #000;
--bs-btn-hover-bg: #fff;
--bs-btn-hover-border-color: rgb(160,140,120);
--bs-btn-focus-shadow-rgb: 59,135,176;
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #135b82;
--bs-btn-active-border-color: #12567a;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,.125);
--bs-btn-disabled-color: #fff;
--bs-btn-disabled-bg: #1872a2;
--bs-btn-disabled-border-color: #1872a2;
}
.btn-outline-primary {
--bs-btn-color: #000;
--bs-btn-border-color: rgb(160,140,120);
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: rgb(180,170,160);
--bs-btn-hover-border-color: rgb(160,140,120);
--bs-btn-focus-shadow-rgb: 24,114,162;
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #1872a2;
--bs-btn-active-border-color: #1872a2;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,.125);
--bs-btn-disabled-color: #1872a2;
--bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: #1872a2;
--bs-gradient: none;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,45 @@
/*!
* Bootstrap v5.3.2 (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
/*!
* @kurkle/color v0.2.1
* https://github.com/kurkle/color#readme
* (c) 2022 Jukka Kurkela
* Released under the MIT License
*/
/*!
* Chart.js v3.9.1
* https://www.chartjs.org
* (c) 2022 Chart.js Contributors
* Released under the MIT License
*/
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
/*!
* jQuery JavaScript Library v3.7.1
* https://jquery.com/
*
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2023-08-28T13:37Z
*/
/*!
* jQuery Validation Plugin v1.20.0
*
* https://jqueryvalidation.org/
*
* Copyright (c) 2023 Jörn Zaefferer
* Released under the MIT license
*/

View File

@@ -0,0 +1,13 @@
{
"/js/main.js": "/js/main.js?id=29451cc889431e973473738b93e6a626",
"/css/dark.css": "/css/dark.css?id=f965da97d900604705c3762bb9cd645a",
"/css/main.css": "/css/main.css?id=cbda89c88526530a66fe1e0d46a63a22",
"/webfonts/fa-brands-400.ttf": "/webfonts/fa-brands-400.ttf?id=69e5d8e4e818f05fd882cceb758d1eba",
"/webfonts/fa-brands-400.woff2": "/webfonts/fa-brands-400.woff2?id=189b85e9c72c6f75e464c3f58a6707cf",
"/webfonts/fa-regular-400.ttf": "/webfonts/fa-regular-400.ttf?id=ed4c23399d1013809882e90bfe396d1b",
"/webfonts/fa-regular-400.woff2": "/webfonts/fa-regular-400.woff2?id=be75b1958ae0da55e1eed562d9b7713d",
"/webfonts/fa-solid-900.ttf": "/webfonts/fa-solid-900.ttf?id=dfdc7801582dd0d20ea75faa3b96c296",
"/webfonts/fa-solid-900.woff2": "/webfonts/fa-solid-900.woff2?id=a0feb384c3c6071947a49708f2b0bc85",
"/webfonts/fa-v4compatibility.ttf": "/webfonts/fa-v4compatibility.ttf?id=e24ec0b8661f7fa333b29444df39e399",
"/webfonts/fa-v4compatibility.woff2": "/webfonts/fa-v4compatibility.woff2?id=e11465c0eff0549edd4e8ea6bbcf242f"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow, noarchive"/>
<meta name="googlebot" content="nosnippet"/>
<link rel="icon" type="image/x-icon" href="{{ basehref|default('') }}templates/Froxlor/assets/img/icon.png">
{% if csrf_token %}<meta name="csrf-token" content="{{ csrf_token }}" />{% endif %}
<!-- CSS -->
{% if theme_css is empty %}
<link href="{{ basehref|default('') }}{{ mix('templates/Froxlor/assets/css/main.css') }}" rel="stylesheet" type="text/css" />
{% else %}
{{ theme_css|raw }}
{% endif %}
{% block custom_css %}{% endblock %}
<!-- Scripts -->
{% if theme_js is empty %}
<script type="text/javascript" src="{{ basehref|default('') }}{{ mix('templates/Froxlor/assets/js/main.js') }}"></script>
{% else %}
{{ theme_js|raw }}
{% endif %}
{% block custom_js %}{% endblock %}
<title>Froxlor{% if page_title %} | {{ page_title }}{% endif %}</title>
</head>
<body class="min-vh-100 d-flex flex-column">
{% block navigation %}{% endblock %}
{% block body %}
<div class="container-fluid">
{% block content %}{% endblock %}
{{ include('Froxlor/footer.html.twig') }}
</div>
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,35 @@
{
"variants": {
"default": {
"img": {
"login": "logo.svg",
"ui": "logo_white.png"
},
"css": [
"main.css",
"custom.css"
],
"js": [
"main.js",
"apikey.js"
],
"description": "Default"
},
"dark": {
"img": {
"login": "logo.svg",
"ui": "logo_white.png"
},
"css": [
"dark.css",
"custom.css"
],
"js": [
"main.js",
"apikey.js"
],
"description": "Darkmode"
}
},
"author": "Maketank"
}

View File

@@ -0,0 +1,23 @@
<footer class="text-center mb-3">
<span>
<img src="{{ basehref|default("") }}templates/Froxlor/assets/img/logo_grey.png" alt="Froxlor"/>
{% if install_mode is not defined %}
{% if (get_setting('admin.show_version_login') == '1'
and area == 'login') or (area != 'login'
and get_setting('admin.show_version_footer') == '1') %}
{{ call_static('\\Froxlor\\Froxlor', 'getFullVersion') }}
{% endif %}
{% endif %}
&copy; 2009-{{ "now"|date("Y") }} by <a href="https://www.froxlor.org/" rel="external" target="_blank">the Froxlor Team</a><br>
{% if install_mode is not defined %}
{% if (get_setting('panel.imprint_url') != '') %}<a href="{{ get_setting('panel.imprint_url') }}" target="_blank" class="footer-link">{{ lng('imprint') }}</a>{% endif %}
{% if (get_setting('panel.terms_url') != '') %}<a href="{{ get_setting('panel.terms_url') }}" target="_blank" class="footer-link">{{ lng('terms') }}</a>{% endif %}
{% if (get_setting('panel.privacy_url') != '') %}<a href="{{ get_setting('panel.privacy_url') }}" target="_blank" class="footer-link">{{ lng('privacy') }}</a>{% endif %}
{% endif %}
</span>
{% if lng('translator') %}
<br/>
<small class="mt-3">{{ lng('panel.translator') }}: {{ lng('translator') }}</small>
{% endif %}
</footer>

View File

@@ -0,0 +1,56 @@
{% macro form(form_data, formaction, title = "", hiddenid = "", nosubmit = false, idprefix = "") %}
{% import "Froxlor/form/formfields.html.twig" as formfields %}
<form action="{{ formaction|default("") }}" {% if form_data.id is defined %}id="{{ form_data.id }}"{% endif %} method="post" enctype="multipart/form-data" class="form">
{% for sid,section in form_data.sections %}
{% if section.visible is not defined or (section.visible is defined and section.visible == true) %}
<div class="card mb-3" id="{{ idprefix }}{{ sid }}">
{% if section.title is not empty %}
<div class="card-header">
{% if section.image is not empty %}
<i class="{{ section.image }}"></i>
{% endif %}
{{ section.title }}
</div>
{% endif %}
<div class="formfields">
{% for id,field in section.fields %}
{{ formfields.fieldrow(id, field) }}
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
{% if nosubmit == false %}
<!-- submit buttons -->
<div>
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"/>
{% if hiddenid is not empty %}
<input type="hidden" name="id" value="{{ hiddenid }}"/>
{% endif %}
<input type="hidden" name="page" value="{{ page }}"/>
<input type="hidden" name="action" value="{{ action }}"/>
<input type="hidden" name="send" value="send"/>
<div class="col-12 text-center mb-2 d-grid gap-2 d-md-block">
{% if form_data.buttons is defined and form_data.buttons is iterable %}
{% for btn in form_data.buttons %}
<button type="{{ btn.type|default("submit") }}" class="btn btn-lg {{ btn.class|default(" btn-primary") }}">{{ btn.label }}</button>
{% endfor %}
{% else %}
<button type="reset" class="btn btn-lg btn-outline-secondary me-md-3">{{ lng('panel.reset') }}</button>
<button type="submit" class="btn btn-lg btn-primary">{{ lng('panel.save') }}</button>
{% endif %}
</div>
</div>
{% endif %}
<span class="text-danger">*</span> {{ lng('panel.mandatoryfield') }}
</form>
{# add translation for custom validations #}
{% 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

@@ -0,0 +1,250 @@
{% macro fieldrow(id, field, norow = false, nohide = false, em = false) %}
{% 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 %}
<mark>
{% endif %}
{{ field.label.title|raw }}
{% if field.mandatory is defined and field.mandatory %}
<span class="text-danger">*</span>
{% endif %}
{% if em %}
</mark>
{% endif %}
{% if field.label.description is defined and field.label.description is not empty %}<br><small>{{ field.label.description|raw }}</small>
{% endif %}
{% if field.requires_reconf is defined and field.requires_reconf is not empty %}
<div class="bg-info bg-opacity-25 rounded p-2 mt-2 d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-exclamation me-2"></i><p class="mb-0">{{ lng('serversettings.requires_reconfiguration', [field.requires_reconf|join(', ')])|raw }}</p>
</div>
{% endif %}
</label>
{% else %}
<label for="{{ id }}" class="col-sm-6 col-form-label pe-3">
{% if em %}
<mark>
{% endif %}
{{ field.label|raw }}
{% if field.mandatory is defined and field.mandatory %}
<span class="text-danger">*</span>
{% endif %}
{% if em %}
</mark>
{% endif %}
{% if field.desc is defined and field.desc is not empty %}<br><small>{{ field.desc|raw }}</small>
{% endif %}
{% if field.requires_reconf is defined and field.requires_reconf is not empty %}
<div class="bg-info bg-opacity-25 rounded p-2 mt-2 d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-exclamation me-2"></i><p class="mb-0">{{ lng('serversettings.requires_reconfiguration', [field.requires_reconf|join(', ')])|raw }}</p>
</div>
{% endif %}
</label>
{% endif %}
<div class="col-sm-6">
{% endif %}
{% if field.type == 'text' or field.type == 'password' or field.type == 'number' or field.type == 'file' or field.type == 'email' or field.type == 'url' or field.type == 'hidden' or field.type == 'date' or field.type == 'datetime-local' %}
{{ _self.input(id, field) }}
{% elseif field.type == 'textul' %}
{{ _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' %}
{{ _self.textarea(id, field) }}
{% elseif field.type == 'label' %}
{{ _self.plain(id, field) }}
{% elseif field.type == 'link' %}
{{ _self.link(id, field) }}
{% elseif field.type == 'itemlist' %}
{{ _self.itemlist(id, field) }}
{% elseif field.type == 'infotext' %}
{{ _self.infotext(id, field) }}
{% elseif field.type == 'image' %}
{{ _self.image(id, field) }}
{% else %}
<div class="alert alert-warning" role="alert">Unknown field-type
{{ field.type }}</div>
{% endif %}
{% if field.note is defined and field.note is not empty %}
<small class="text-info">{{ field.note|raw }}</small>
{% endif %}
{% if norow == false and (field.type != 'hidden' or (field.type == 'hidden' and field.display is defined and field.display is not empty)) %}
</div>
</div>
{% endif %}
{% endif %}
{% endmacro %}
{# installation specific format #}
{% macro field(id, field, norow = true, nohide = false, em = false) %}
{% if field.type == 'checkbox' %}
<div class="form-check form-switch mb-3">
<input type="hidden" value="0" name="{{ id }}"/>
<input type="checkbox" {% if (field.visible is defined and field.visible == false) or (field.disabled is defined and field.disabled == true) %} disabled {% endif %} value="{{ field.value }}" id="{{ id }}" name="{{ id }}" class="form-check-input {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.checked is defined and field.checked == 1 %} checked="checked" {% endif %}>
<label for="{{ id }}" class="form-check-label">{{ field.label|raw }}</label>
</div>
{% elseif field.type == 'hidden' %}
{{ _self.fieldrow(id, field, norow, nohide, em) }}
{% else %}
<div class="form-floating mb-3">
{{ _self.fieldrow(id, field, norow, nohide, em) }}
<label for="{{ id }}" class="form-label">{{ field.label|raw }}</label>
</div>
{% endif %}
{% endmacro %}
{% macro bool(id, field) %}
{% if field.is_array is defined and field.is_array == 1 and field.values is not empty %}
{% for subfield in field.values %}
<div class="form-check form-switch">
<input type="checkbox" {% if (field.visible is defined and field.visible == false) or (field.disabled is defined and field.disabled == true) %} disabled {% endif %} value="{{ subfield.value }}" name="{{ id }}[]" class="form-check-input" {% if field.value is defined and subfield.value in field.value %} checked="checked" {% endif %} {% if field.mandatory is defined and field.mandatory %} required {% endif %}>
<label class="form-check-label">
{{ subfield.label|raw }}
</label>
</div>
{% endfor %}
{% else %}
<div class="form-check form-switch">
<input type="hidden" value="0" name="{{ id }}"/>
<input type="checkbox" {% if (field.visible is defined and field.visible == false) or (field.disabled is defined and field.disabled == true) %} disabled {% endif %} value="{{ field.value }}" id="{{ id }}" name="{{ id }}" class="form-check-input {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.checked is defined and field.checked == 1 %} checked="checked" {% endif %}>
</div>
{% 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">
{% endif %}
<span {% if field.classes is defined %} class="{{ field.classes }}" {% endif %}>{{ field.value|raw }}</span>
{% if field.next_to is defined %}
{% for nid, nfield in field.next_to %}
{% if nfield.next_to_prefix is defined %}
<span class="input-group-text">{{ nfield.next_to_prefix }}</span>
{% endif %}
{{ _self.fieldrow(nid, nfield, true) }}
{% endfor %}
</div>
{% endif %}
{% endmacro %}
{% macro plain(id, field) %}
<input type="text" readonly class="form-control-plaintext" id="{{ id }}" name="{{ id }}" value="{{ field.value|raw }}">
{% if field.next_to is defined %}
{% for nid, nfield in field.next_to %}
{% if nfield.next_to_prefix is defined %}
<span class="input-group-text">{{ nfield.next_to_prefix }}</span>
{% endif %}
{{ _self.fieldrow(nid, nfield, true) }}
{% endfor %}
{% endif %}
{% endmacro %}
{% macro input(id, field) %}
{% 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 %} {% 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 %}
{% if field.next_to is defined %}
{% for nid, nfield in field.next_to %}
{% if nfield.next_to_prefix is defined %}
<span class="input-group-text">{{ nfield.next_to_prefix }}</span>
{% endif %}
{{ _self.fieldrow(nid, nfield, true) }}
{% endfor %}
</div>
{% endif %}
{% endmacro %}
{% macro image(id, field) %}
{% if field.value is not empty %}
<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">
{{ lng('panel.image_field_delete') }}
</label>
</div>
{% endif %}
{% set field = field|merge({'type':'file'}) %}
{{ _self.input(id, field) }}
{% endmacro %}
{% macro input_ul(id, field) %}
{% set max = "" %}
{% if field.maxlength is defined %}
{% for i in 1..field.maxlength %}
{% set max = max ~ "9" %}
{% endfor %}
{% endif %}
<div class="input-group">
<input type="number" min="0" {% if max is not empty %} max="{{ max }}" {% endif %} id="{{ id }}" name="{{ id }}" value="{% if field.value >= 0 %}{{ field.value }}{% endif %}" class="form-control {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.readonly is defined and field.readonly %} readonly {% endif %} {% if field.autocomplete is defined %} autocomplete="{{ field.autocomplete }}" {% endif %} {% if field.placeholder is defined %} placeholder="{{ field.placeholder }}" {% endif %}/>
<div class="input-group-text">
<input class="form-check-input mt-0" type="checkbox" name="{{ id }}_ul" value="1" {% if field.value == -1 %} checked="checked" {% endif %}>
</div>
</div>
{% endmacro %}
{% macro select(id, field) %}
{% if field.next_to is defined %}
<div class="input-group">
{% endif %}
<select {% if field.visible is defined and field.visible == false %} disabled {% endif %} class="form-select {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" name="{{ id }}{% if field.select_mode is defined and field.select_mode == 'multiple' %}[]{% endif %}" id="{{ id }}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.select_mode is defined and field.select_mode == 'multiple' %} multiple="multiple" {% endif %}{% if field.readonly is defined and field.readonly %} readonly {% endif %}>
{% for val,txt in field.select_var %}
<option value="{{ val }}" {% if field.selected is defined and ((field.selected is not iterable and field.selected == val) or (field.selected is iterable and val in field.selected|keys)) %} selected="selected" {% endif %}>{{ txt|raw }}</option>
{% endfor %}
</select>
{% if field.next_to is defined %}
{% for nid, nfield in field.next_to %}
{% if nfield.next_to_prefix is defined %}
<span class="input-group-text">{{ nfield.next_to_prefix }}</span>
{% endif %}
{{ _self.fieldrow(nid, nfield, true) }}
{% endfor %}
</div>
{% endif %}
{% endmacro %}
{% macro textarea(id, field) %}
<textarea {% if field.visible is defined and field.visible == false %} disabled {% endif %} rows="{{ field.rows|default('12') }}" cols="{{ field.cols|default('60') }}" id="{{ id }}" name="{{ id }}" 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.placeholder is defined %} placeholder="{{ field.placeholder }}" {% endif %} {% if field.style is defined %} style="{{ field.style }}" {% endif %}>{{ field.value|raw }}</textarea>
{% endmacro %}
{% macro link(id, field) %}
<a href="{{ field.href|raw }}" class="{{ field.classes }}">{{ field.label|raw }}</a>
{% endmacro %}
{% macro itemlist(id, field) %}
{% if field.values is not empty %}
{% for value in field.values %}
<p>{{ value.item|raw }}
{% if value.href is defined and value.href is not empty %}
{{ _self.link(id, value) }}
{% endif %}
</p>
{% endfor %}
{% endif %}
{% if field.next_to is defined %}
{% for nid, nfield in field.next_to %}
{{ _self.fieldrow(nid, nfield, true) }}
{% endfor %}
{% endif %}
{% endmacro %}

View File

View File

@@ -0,0 +1,37 @@
{% extends "Froxlor/userarea.html.twig" %}
{% block content %}
<form action="{{ action|default("") }}" method="post" enctype="application/x-www-form-urlencoded" class="form">
<div class="alert alert-warning" role="alert">
<h4 class="alert-heading">{{ lng('panel.security_question') }}</h4>
<p>{{ question|raw }}</p>
{% if with_checkbox is defined and with_checkbox is iterable %}
{% if with_checkbox.show %}
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="delete_userfiles" name="delete_userfiles" value="1">
<label class="form-check-label" for="delete_userfiles">{{ with_checkbox.chk_text|raw }}</label>
</div>
{% else %}
<input type="hidden" name="delete_userfiles" value="0"/>
{% endif %}
{% endif %}
<p>
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"/>
<input type="hidden" name="send" value="send"/>
{% for id,field in url_params %}
<input type="hidden" name="{{ id }}" value="{{ field }}"/>
{% endfor %}
<button class="btn btn-danger" type="submit" name="submitbutton">{{ lng('panel.yes') }}</button>&nbsp;
{% if back_link is defined and back_link is iterable and back_link|length > 0 %}
<a href="{{ linker(back_link) }}" class="btn btn-secondary">{{ lng('panel.no') }}</a>
{% else %}
<a href="javascript:history.back(-1)" class="btn btn-secondary">{{ lng('panel.no') }}</a>
{% endif %}
</p>
</div>
</form>
{% endblock %}

View File

View File

@@ -0,0 +1,89 @@
<!-- language select -->
<form action="{{ pagecontent.form.formaction }}" method="get">
<div class="row mb-3">
<label for="language" class="col-sm-4 col-form-label">{{ lng('install.language') }}</label>
<div class="col-sm-8">
<select class="form-select" id="language" name="language">
{% for lngfile,lngname in pagecontent.form.languages %}
<option value="{{ lngfile }}" {% if lngfile == pagecontent.form.activelang %} selected="selected" {% endif %}>{{ lngname }}</option>
{% endfor %}
</select>
</div>
</div>
<aside class="text-end">
<input type="hidden" name="check" value="1"/>
<button class="btn btn-sm btn-primary" type="submit" name="chooselang">{{ lng('install.lngbtn_go') }}</button>
</aside>
</form>
<!-- main install form -->
<div class="alert alert-primary mt-md-3" role="alert">{{ lng('install.welcometext')|raw }}</div>
{% if pagecontent.form.result is not empty %}
<div class="alert alert-warning" role="alert">
{% for emsg in pagecontent.form.result %}
<p>{{ emsg }}</p>
{% endfor %}
</div>
{% endif %}
<form action="{{ pagecontent.form.formaction }}" method="post">
{% for fdata in pagecontent.form.data %}
<fieldset>
<legend>{{ fdata.title }}</legend>
{% for field in fdata.fields %}
{% if field is iterable %}
{% if field.type is defined %}
{% if field.type == 'text' or field.type == 'password' %}
<div class="row mb-3">
<label for="{{ field.id }}" class="col-sm-4 col-form-label">{{ field.label|raw }}</label>
<div class="col-sm-8">
<input type="{{ field.type }}" class="form-control {% if field.style == 'red' %}is-invalid{% endif %}" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.value }}" {% if field.required %} required {% endif %}/>
</div>
</div>
{% elseif field.type == 'select' %}
<div class="row mb-3">
<label for="{{ field.id }}" class="col-sm-4 col-form-label">{{ field.label|raw }}</label>
<div class="col-sm-8">
<select class="form-select {% if field.style == 'red' %}is-invalid{% endif %}" id="{{ field.id }}" name="{{ field.name }}" {% if field.required %} required {% endif %}>
{% for opts in field.options %}
<option value="{{ opts.value }}" {% if opts.selected %} selected="selected" {% endif %}>{{ opts.label }}</option>
{% endfor %}
</select>
</div>
</div>
{% elseif field.type == 'checkbox' %}
<div class="row mb-3">
<label for="{{ field.id }}" class="col-sm-4 col-form-label">{{ field.label|raw }}</label>
<div class="col-sm-8">
<div class="form-check form-switch">
<input class="form-check-input {% if field.style == 'red' %}is-invalid{% endif %}" type="checkbox" value="{{ field.value }}" id="{{ field.id }}" name="{{ field.name }}" {% if field.checked %} checked="checked" {% endif %}>
</div>
</div>
</div>
{% endif %}
{% else %}
<div class="row mb-3">
<label class="col-sm-4 col-form-label">{{ field.label|raw }}</label>
<div class="col-sm-8">
{% for radios in field.fields %}
<div class="form-check">
<input class="form-check-input {% if field.style == 'red' %}is-invalid{% endif %}" type="radio" name="{{ radios.name }}" id="{{ radios.id }}" value="{{ radios.value }}" {% if radios.checked %}checked="checked"{% endif %}>
<label class="form-check-label" for="{{ radios.id }}">
{{ radios.label }}
</label>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endif %}
{% endfor %}
</fieldset>
{% endfor %}
<aside class="text-end mt-3">
<input type="hidden" name="check" value="1"/>
<input type="hidden" name="language" value="{{ pagecontent.form.activelang }}"/>
<input type="hidden" name="installstep" value="1"/>
<button class="btn btn-lg btn-success" type="submit" name="submitbutton">
{{ lng('click_here_to_continue') }} &raquo;
</button>
</aside>
</form>

View File

View File

@@ -0,0 +1,126 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container max-w-lg flex align-content-center mt-5">
<img src="{{ basehref|default('') }}templates/Froxlor/assets/img/logo.png" class="mb-5" alt="{{ lng('install.slogan') }}"/>
{% if error is not null %}
<div class="alert alert-danger mb-4">{{ error }}</div>
{% endif %}
<div class="row text-center gx-0">
<div class="col p-3{{ setup.step == 0 ? ' bg-white shadow rounded-top' : '' }}">
<i class="far fa-circle{{ setup.step > 0 ? '-check' : '' }}"></i>
{% if setup.step > 0 %}<a href="?step=0" class="text-decoration-none">{{ lng('install.preflight') }}</a>{% else %}{{ lng('install.preflight') }}{% endif %}
</div>
<div class="col p-3{{ setup.step == 1 ? ' bg-white shadow rounded-top' : '' }}">
<i class="far fa-circle{{ setup.step > 1 ? '-check' : '' }}"></i>
{% if setup.step > 1 %}<a href="?step=1" class="text-decoration-none">{{ lng('install.database.top') }}</a>{% else %}{{ lng('install.database.top') }}{% endif %}
</div>
<div class="col p-3{{ setup.step == 2 ? ' bg-white shadow rounded-top' : '' }}">
<i class="far fa-circle{{ setup.step > 2 ? '-check' : '' }}"></i>
{% if setup.step > 2 %}<a href="?step=2" class="text-decoration-none">{{ lng('install.admin.top') }}</a>{% else %}{{ lng('install.admin.top') }}{% endif %}
</div>
<div class="col p-3{{ setup.step == 3 ? ' bg-white shadow rounded-top' : '' }}">
<i class="far fa-circle{{ setup.step > 3 ? '-check' : '' }}"></i>
{% if setup.step > 3 %}<a href="?step=3" class="text-decoration-none">{{ lng('install.system.top') }}</a>{% else %}{{ lng('install.system.top') }}{% endif %}
</div>
<div class="col p-3{{ setup.step == 4 ? ' bg-white shadow rounded-top' : '' }}">
<i class="far fa-circle{{ setup.step > 4 ? '-check' : '' }}"></i>
{% if setup.step > 4 %}<a href="?step=4" class="text-decoration-none">{{ lng('install.install.top') }}</a>{% else %}{{ lng('install.install.top') }}{% endif %}
</div>
</div>
<div class="card border-0 shadow">
<div class="card-body p-5">
<form method="post" action="?step={{ setup.step }}">
{% if setup.step > 0 %}
<div class="d-block d-lg-flex justify-content-between align-items-center mb-3">
<h4 class="mb-3 mb-lg-0">{{ section.title }}</h4>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="switchInstallMode" {% if extended is defined and extended %}checked{% endif %}>
<label class="form-check-label" for="switchInstallMode">{% if extended is defined and extended %}{{ lng('install.switchmode_basic') }}{% else %}{{ lng('install.switchmode_advanced') }}{% endif %}</label>
</div>
</div>
<p class="lead">{{ section.description|raw }}</p>
<hr />
{% import "Froxlor/form/formfields.html.twig" as formfields %}
{% for id, field in section.fields %}
{% if field.advanced is defined and field.advanced == true and extended == false %}
{# hide advanced fields #}
{% set field = field|merge({'type': 'hidden'}) %}
{% endif %}
{{ formfields.field(id, field) }}
{% endfor %}
<div class="d-flex {% if setup.step < setup.max_steps %}justify-content-between{% else %}justify-content-end{% endif %} mt-4">
{% if setup.step < setup.max_steps %}
<a href="?step={{ setup.step - 1 }}" class="btn btn-secondary">&laquo; {{ lng('panel.back') }}</a>
<button type="submit" name="submit" class="btn btn-primary">{{ lng('panel.next') }} &raquo;</button>
{% else %}
<span id="submitAuto"><i class="fas fa-spinner fa-pulse"></i> {{ lng('install.install.waitforconfig') }}</span>
<button id="submitManual" type="submit" name="submit" class="btn btn-success d-none">{{ lng('install.install.top') }} &raquo;</button>
{% endif %}
</div>
{% else %}
<h4 class="mb-3">{{ lng('install.dependency_check.title') }}</h4>
<p class="lead">{{ lng('install.dependency_check.description') }}</p>
<p class="lead {{ preflight.criticals ? 'text-danger' : preflight.suggestions ? 'text-warning' : 'text-success'}}">
<i class="{{ preflight.criticals ? 'fa-solid fa-triangle-exclamation' : preflight.suggestions ? 'fa-solid fa-circle-info' : 'far fa-circle-check' }}"></i>
{{ preflight.text }}
</p>
{% if preflight.criticals %}
<p class="text-muted">{{ lng('install.critical_error') }}</p>
<ul>
{% for ctype, critical in preflight.criticals %}
{% if ctype == 'wrong_ownership' %}
<li>{{ lng('install.errors.' ~ ctype, [critical.user, critical.group]) }}</li>
{% elseif ctype == 'missing_extensions' %}
<li>{{ lng('install.errors.' ~ ctype) }}<ul>
{% for missext in critical %}
<li>{{ missext }}</li>
{% endfor %}
</ul>
</li>
{% else %}
<li>{{ critical|raw }}</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
{% if preflight.suggestions %}
<p class="text-muted">{{ lng('install.suggestions') }}</p>
<ul>
{% for ctype, suggestion in preflight.suggestions %}
{% if ctype == 'missing_extensions' %}
<li>{{ lng('install.errors.suggestedextensions') }}<ul>
{% for missext in suggestion %}
<li>{{ missext }}</li>
{% endfor %}
</ul>
</li>
{% else %}
<li>{{ suggestion|raw }}</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
<div class="d-flex justify-content-end mt-4">
{% if preflight.criticals %}
<a href="" class="btn btn-secondary"><i class="fa-solid fa-arrow-rotate-left"></i> {{ lng('install.check_again') }}</a>
{% else %}
<a href="?step=1" class="btn btn-primary">{{ lng('install.start_installation') }}</a>
{% endif %}
</div>
{% endif %}
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,48 @@
{% extends "Froxlor/userarea.html.twig" %}
{% block heading %}
<div>
<h5 class="mb-1">
<i class="fa-solid fa-download me-1"></i>
{{ lng('update.update') }}
</h5>
<span class="text-muted">{{ lng('update.description') }}</span>
</div>
{% endblock %}
{% block content %}
<div class="card table-responsive">
<div class="card-body">
<table class="table table-borderless align-middle mb-0 px-3">
<tbody>
{% for check in checks %}
<tr class="{% if check.result == 1 %}table-danger{% elseif check.result == 2 %}table-warning{% endif %}">
<td class="w-75" scope="row">{{ check.title }}</td>
<td class="col-auto text-end{% if check.result == 0 %} text-success{% endif %}">
<span class="d-none d-md-inline">{{ check.result_txt }}</span>
{% if check.result == 0 %}&nbsp;<i class="fa-solid fa-check-circle" {% elseif check.result == 2 %}<span class="d-md-none">&nbsp;???</span>{% elseif check.result == 1 %}<span class="d-md-none">&nbsp;!!!</span>
{% endif %}
</td>
</tr>
{% if check.result_desc is not empty %}
<tr>
<td colspan="2">
<span>{{ check.result_desc|raw }}</span>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<div class="row pt-md-3">
<div class="col-12 text-end mt-4 mt-md-0">
<a class="btn btn-lg btn-block btn-primary" href="admin_index.php">
{{ lng('success.clickheretocontinue') }}
&raquo;
</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<form action="index.php" class="col-12 max-w-420 d-flex flex-column" method="post" enctype="application/x-www-form-urlencoded">
<img class="align-self-center my-5" src="{{ header_logo_login }}" alt="Froxlor Server Management Panel"/>
<div class="card shadow">
<div class="card-body">
<h5 class="card-title">{{ pagetitle }}</h5>
{% if message is not empty %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">{{ lng('error.error') }}</h4>
<p>{{ message|raw }}</p>
</div>
{% endif %}
<div class="mb-3">
<label for="2fa_code" class="col-form-label">{{ lng('login.2facode') }}</label>
<input class="form-control" type="text" name="2fa_code" id="2fa_code" value="" autocomplete="off" autofocus required/>
</div>
</div>
<div class="card-body d-grid gap-2">
<input type="hidden" name="action" value="2fa_verify"/>
<input type="hidden" name="send" value="send"/>
<button class="btn btn-primary rounded-top-0" type="submit" name="2faverify">{{ lng('2fa.2fa_verify') }}</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,53 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<form action="{{ formaction }}" class="col-12 max-w-420 d-flex flex-column" method="post" enctype="application/x-www-form-urlencoded">
<img class="align-self-center my-5" src="{{ header_logo_login }}" alt="Froxlor Server Management Panel"/>
<div class="card shadow">
<div class="card-body">
<h5 class="card-title">{{ pagetitle }}</h5>
{% if upd_in_progress %}
<div class="alert alert-warning" role="alert">
{{ lng('update.updateinprogress_onlyadmincanlogin')|raw }}
</div>
{% elseif successmsg is not empty %}
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">{{ lng('success.success') }}</h4>
<p>{{ successmsg|raw }}</p>
</div>
{% elseif message is not empty %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">{{ lng('error.error') }}</h4>
<p>{{ message|raw }}</p>
</div>
{% endif %}
<div class="mb-3">
<label for="loginname" class="col-form-label">{{ lng('login.username') }}</label>
<input class="form-control" type="text" name="loginname" id="loginname" value="" required autofocus/>
</div>
<div class="mb-3">
<label for="loginemail" class="col-form-label">{{ lng('login.email') }}</label>
<input class="form-control" type="email" name="loginemail" id="loginemail" value="" required/>
</div>
</div>
<div class="card-body d-grid gap-2">
<button class="btn btn-primary rounded-top-0" type="submit" name="doremind">{{ lng('login.remind') }}</button>
</div>
<div class="card-footer">
<a class="card-link text-muted" href="index.php">
<i class="fa-solid fa-angles-left"></i>
{{ lng('login.backtologin') }}</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

View File

@@ -0,0 +1,54 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<form class="col-12 max-w-420 d-flex flex-column" method="post" enctype="application/x-www-form-urlencoded">
<img class="align-self-center my-5" src="{{ header_logo_login }}" alt="Froxlor Server Management Panel"/>
<div class="card shadow">
<div class="card-body">
<h5 class="card-title">{{ pagetitle }}</h5>
<p>{{ lng('login.welcomemsg') }}</p>
{% if upd_in_progress %}
<div class="alert alert-warning" role="alert">
{{ lng('update.updateinprogress_onlyadmincanlogin')|raw }}
</div>
{% elseif successmsg is not empty %}
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">{{ lng('success.success') }}</h4>
<p>{{ successmsg|raw }}</p>
</div>
{% elseif message is not empty %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">{{ lng('error.error') }}</h4>
<p>{{ message|raw }}</p>
</div>
{% endif %}
<div class="mb-3">
<label for="loginname" class="col-form-label">{{ lng('login.username') }}</label>
<input class="form-control" type="text" name="loginname" id="loginname" value="" required autofocus/>
</div>
<div class="mb-3">
<label for="password" class="col-form-label">{{ lng('login.password') }}</label>
<input class="form-control" type="password" name="password" id="password" value="" required/>
</div>
</div>
<div class="card-body d-grid gap-2">
<button class="btn btn-primary rounded-top-0" type="submit" name="dologin">{{ lng('login.login') }}</button>
</div>
{% if get_setting('panel.allow_preset') == '1' %}
<div class="card-footer">
<a class="card-link text-muted" href="index.php?action=forgotpwd">{{ lng('login.forgotpwd') }}</a>
</div>
{% endif %}
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<form action="{{ formaction }}" class="col-12 max-w-420 d-flex flex-column" method="post" enctype="application/x-www-form-urlencoded">
<img class="align-self-center my-5" src="{{ header_logo_login }}" alt="Froxlor Server Management Panel"/>
<div class="card shadow">
<div class="card-body">
<h5 class="card-title">{{ pagetitle }}</h5>
<p>{{ lng('login.presend') }}</p>
{% if message is not empty %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">{{ lng('error.error') }}</h4>
<p>{{ message|raw }}</p>
</div>
{% endif %}
<div class="mb-3">
<label for="new_password" class="col-form-label">{{ lng('changepassword.new_password') }}</label>
<input class="form-control" type="password" name="new_password" id="new_password" value="" required autofocus/>
</div>
<div class="mb-3">
<label for="new_password_confirm" class="col-form-label">{{ lng('changepassword.new_password_confirm') }}</label>
<input class="form-control" type="password" name="new_password_confirm" id="new_password_confirm" value="" required/>
</div>
</div>
<div class="card-body d-grid gap-2">
<button class="btn btn-primary rounded-top-0" type="submit" name="doremind">{{ lng('login.remind') }}</button>
</div>
<div class="card-footer">
<a class="card-link text-muted" href="index.php">
<i class="fa-solid fa-angles-left"></i>
{{ lng('login.backtologin') }}</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

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