Merge remote-tracking branch 'origin/main' into v2.2

This commit is contained in:
Michael Kaufmann
2025-04-24 10:02:37 +02:00
20 changed files with 114 additions and 79 deletions

22
composer.lock generated
View File

@@ -3066,16 +3066,16 @@
},
{
"name": "phpstan/phpstan",
"version": "1.12.20",
"version": "1.12.24",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "3240b1972042c7f73cf1045e879ea5bd5f761bb7"
"reference": "338b92068f58d9f8035b76aed6cf2b9e5624c025"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/3240b1972042c7f73cf1045e879ea5bd5f761bb7",
"reference": "3240b1972042c7f73cf1045e879ea5bd5f761bb7",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/338b92068f58d9f8035b76aed6cf2b9e5624c025",
"reference": "338b92068f58d9f8035b76aed6cf2b9e5624c025",
"shasum": ""
},
"require": {
@@ -3120,7 +3120,7 @@
"type": "github"
}
],
"time": "2025-03-05T13:37:43+00:00"
"time": "2025-04-16T13:01:53+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -4571,16 +4571,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.11.3",
"version": "3.12.2",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
"reference": "ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10"
"reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10",
"reference": "ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10",
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa",
"reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa",
"shasum": ""
},
"require": {
@@ -4647,11 +4647,11 @@
"type": "open_collective"
},
{
"url": "https://thanks.dev/phpcsstandards",
"url": "https://thanks.dev/u/gh/phpcsstandards",
"type": "thanks_dev"
}
],
"time": "2025-01-23T17:04:15+00:00"
"time": "2025-04-13T04:10:18+00:00"
},
{
"name": "symfony/config",

View File

@@ -226,7 +226,7 @@ if (Froxlor::isDatabaseVersion('202411200')) {
$current_allowed_mysqlserver = !empty($customer['allowed_mysqlserver']) ? json_decode($customer['allowed_mysqlserver'], true) : [];
foreach ($current_allowed_mysqlserver as $dbserver) {
// require privileged access for target db-server
Database::needRoot(true, $dbserver, true);
Database::needRoot(true, $dbserver, false);
// get DbManager
$dbm = new DbManager(FroxlorLogger::getInstanceOf());
foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) {
@@ -238,7 +238,7 @@ if (Froxlor::isDatabaseVersion('202411200')) {
}
}
$dbm->getManager()->flushPrivileges();
Database::needRoot(false);
Database::needRoot();
}
}
Update::lastStepStatus(0);

View File

@@ -44,7 +44,7 @@ abstract class ApiParameter
*
* @throws Exception
*/
public function __construct(array $params = null)
public function __construct(?array $params = null)
{
if (!is_null($params)) {
$params = $this->trimArray($params);
@@ -91,7 +91,7 @@ abstract class ApiParameter
* @return mixed
* @throws Exception
*/
protected function getUlParam(string $param = null, string $ul_field = null, bool $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);
@@ -116,7 +116,7 @@ abstract class ApiParameter
* @return mixed
* @throws Exception
*/
protected function getParam(string $param = null, bool $optional = false, $default = '')
protected function getParam(?string $param = null, bool $optional = false, $default = '')
{
// does it exist?
if (!isset($this->cmd_params[$param])) {
@@ -183,7 +183,7 @@ abstract class ApiParameter
*
* @return string
*/
protected function getBoolParam(string $param = null, bool $optional = false, $default = false)
protected function getBoolParam(?string $param = null, bool $optional = false, $default = false)
{
$_default = '0';
if ($default) {

View File

@@ -777,7 +777,7 @@ class Customers extends ApiCommand implements ResourceEntity
if ($mysqls != 0) {
foreach ($allowed_mysqlserver as $dbserver) {
// require privileged access for target db-server
Database::needRoot(true, $dbserver, true);
Database::needRoot(true, $dbserver, false);
// get DbManager
$dbm = new DbManager($this->logger());
// give permission to the user on every access-host we have

View File

@@ -133,7 +133,7 @@ class Database
* if set to false, the error will be logged, but we go on
* @throws Exception
*/
private static function showerror(Exception $error, bool $showerror = true, bool $json_response = false, PDOStatement $stmt = null)
private static function showerror(Exception $error, bool $showerror = true, bool $json_response = false, ?PDOStatement $stmt = null)
{
global $userinfo, $theme, $linker;
@@ -377,6 +377,14 @@ class Database
self::$link = null;
}
/**
* get the currently used database-server (relevant for root-connection)
*/
public static function getServer()
{
return self::$dbserver;
}
/**
* enable the temporary access to sql-access data
* note: if you want root-sqldata you need to

View File

@@ -176,7 +176,7 @@ class DbManager
*/
public function createDatabase(string $loginname = null, string $password = null, int $dbserver = 0, int $last_accnumber = 0, string $global_user = "")
{
Database::needRoot(true, $dbserver, true);
Database::needRoot(true, $dbserver, false);
// check whether we shall create a random username
if (strtoupper(Settings::Get('customer.mysqlprefix')) == 'RANDOM') {
@@ -211,7 +211,7 @@ class DbManager
}
$this->getManager()->flushPrivileges();
Database::needRoot(false);
Database::needRoot();
$this->log->logAction(FroxlorLogger::USR_ACTION, LOG_INFO, "created database '" . $username . "'");

View File

@@ -115,12 +115,9 @@ class DbManagerMySQL
$grants = "SELECT, INSERT, UPDATE, DELETE, DROP, INDEX, ALTER";
}
$stmt = Database::prepare("
GRANT " . $grants . " ON `" . $username . ($grant_access_prefix ? '%' : '') . "`.* TO :username@:host
GRANT " . $grants . " ON `" . $username . ($grant_access_prefix ? '%' : '') . "`.* TO `" . $username . "`@`" . $access_host . "`
");
Database::pexecute($stmt, [
"username" => $username,
"host" => $access_host
]);
Database::pexecute($stmt);
if ($grant_access_prefix) {
$this->grantCreateToCustomerDbs($username, $access_host);
@@ -327,19 +324,22 @@ class DbManagerMySQL
*/
private function grantCreateToCustomerDbs(string $username, string $access_host)
{
// remember what (possible remote) db-server we're on
$currentDbServer = Database::getServer();
// use "unprivileged" connection
Database::needRoot();
$cus_stmt = Database::prepare("SELECT customerid FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE loginname = :username");
$cust = Database::pexecute_first($cus_stmt, ['username' => $username]);
if ($cust) {
$sel_stmt = Database::prepare("SELECT databasename FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :cid");
Database::pexecute($sel_stmt, ['cid' => $cust['customerid']]);
$sel_stmt = Database::prepare("SELECT databasename FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :cid AND `dbserver` = :dbserver");
Database::pexecute($sel_stmt, ['cid' => $cust['customerid'], 'dbserver' => $currentDbServer]);
// reset to root-connection for used dbserver
Database::needRoot(true, $currentDbServer, false);
while ($dbdata = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) {
$stmt = Database::prepare("
GRANT ALL ON `" . $dbdata['databasename'] . "`.* TO :username@:host
GRANT ALL ON `" . $dbdata['databasename'] . "`.* TO `" . $username . "`@`" . $access_host . "`
");
Database::pexecute($stmt, [
"username" => $username,
"host" => $access_host
]);
Database::pexecute($stmt);
}
}
}
@@ -355,12 +355,12 @@ class DbManagerMySQL
*/
public function grantCreateToDb(string $username, string $database, string $access_host)
{
$stmt = Database::prepare("
GRANT ALL ON `" . $database . "`.* TO :username@:host
");
Database::pexecute($stmt, [
"username" => $username,
"host" => $access_host
]);
// only grant permission if the user exists
if ($this->userExistsOnHost($username, $access_host)) {
$stmt = Database::prepare("
GRANT ALL ON `" . $database . "`.* TO `" . $username . "`@`" . $access_host . "`
");
Database::pexecute($stmt);
}
}
}

View File

@@ -164,7 +164,7 @@ class FroxlorLogger
* @param int $type
* @param ?string $text
*/
public function logAction($action = FroxlorLogger::USR_ACTION, int $type = LOG_NOTICE, string $text = null)
public function logAction($action = FroxlorLogger::USR_ACTION, int $type = LOG_NOTICE, ?string $text = null)
{
// not logging normal stuff if not set to "paranoid" logging
if (!self::$crondebug_flag && Settings::Get('logger.severity') == '1' && $type > LOG_NOTICE) {

View File

@@ -199,7 +199,7 @@ class PhpHelper
* @param string|null $nameserver set additional resolver nameserver to use (e.g. 1.1.1.1)
* @return bool|array
*/
public static function gethostbynamel6(string $host, bool $try_a = true, string $nameserver = null)
public static function gethostbynamel6(string $host, bool $try_a = true, ?string $nameserver = null)
{
$ips = [];
@@ -442,7 +442,7 @@ class PhpHelper
* @param bool $asReturn
* @return string
*/
public static function parseArrayToPhpFile(array $array, string $comment = null, bool $asReturn = false): string
public static function parseArrayToPhpFile(array $array, ?string $comment = null, bool $asReturn = false): string
{
$str = sprintf("<?php\n// %s\n\n", $comment ?? 'autogenerated froxlor file');
@@ -464,7 +464,7 @@ class PhpHelper
* @param int $depth
* @return string
*/
public static function parseArrayToString(array $array, string $key = null, int $depth = 1): string
public static function parseArrayToString(array $array, ?string $key = null, int $depth = 1): string
{
$str = '';
if (!is_null($key)) {

View File

@@ -162,7 +162,7 @@ class Pagination
*
* @return Pagination
*/
public function addSearch(string $searchtext = null, string $field = null, string $operator = null): Pagination
public function addSearch(?string $searchtext = null, string $field = null, string $operator = null): Pagination
{
if (!isset($this->data['sql_search'])) {
$this->data['sql_search'] = [];

View File

@@ -28,7 +28,6 @@ namespace Froxlor\Validate;
use Exception;
use Froxlor\Database\Database;
use Froxlor\FroxlorLogger;
use Froxlor\Idna\IdnaWrapper;
use Froxlor\System\IPTools;
use Froxlor\UI\Response;
@@ -63,10 +62,11 @@ class Validate
string $str,
string $fieldname,
string $pattern = '',
$lng = '',
$emptydefault = [],
bool $throw_exception = false
) {
$lng = '',
$emptydefault = [],
bool $throw_exception = false
)
{
if (!is_array($emptydefault)) {
$emptydefault_array = [
$emptydefault
@@ -122,14 +122,15 @@ class Validate
*/
public static function validate_ip2(
string $ip,
bool $return_bool = false,
bool $return_bool = false,
string $lng = 'invalidip',
bool $allow_localhost = false,
bool $allow_priv = false,
bool $allow_cidr = false,
bool $cidr_as_netmask = false,
bool $throw_exception = false
) {
bool $allow_localhost = false,
bool $allow_priv = false,
bool $allow_cidr = false,
bool $cidr_as_netmask = false,
bool $throw_exception = false
)
{
$cidr = "";
if ($allow_cidr) {
$org_ip = $ip;
@@ -200,20 +201,34 @@ class Validate
$url = 'http://' . $url;
}
// needs converting
try {
$idna_convert = new IdnaWrapper();
$url = $idna_convert->encode($url);
} catch (Exception $e) {
// Parse parts
$parts = parse_url($url);
if ($parts === false || !isset($parts['scheme'], $parts['host'])) {
return false;
}
if ($allow_private_ip) {
$pattern = '%^(?:(?:https?):\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$%iuS';
} else {
$pattern = '%^(?:(?:https?):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$%iuS';
// Check allowed schemes
if (!in_array(strtolower($parts['scheme']), ['http', 'https'], true)) {
return false;
}
if (preg_match($pattern, $url)) {
// Check if host is valid domain or valid IP (v4 or v6)
$host = $parts['host'];
if (substr($host, 0, 1) == '[' && substr($host, -1) == ']') {
$host = substr($host, 1, -1);
}
$opts = FILTER_FLAG_IPV4 | FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE;
$opts6 = FILTER_FLAG_IPV6 | FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE;
if ($allow_private_ip) {
$opts = FILTER_FLAG_IPV4 | FILTER_FLAG_NO_RES_RANGE;
$opts6 = FILTER_FLAG_IPV6 | FILTER_FLAG_NO_RES_RANGE;
}
if (filter_var($host, FILTER_VALIDATE_IP, $opts)) {
return true;
} elseif (substr($parts['host'], 0, 1) == '[' && substr($parts['host'], -1) == ']' && filter_var($host, FILTER_VALIDATE_IP, $opts6)) {
return true;
} elseif (!preg_match('/^([0-9]{1,3}\.)+[0-9]{1,3}$/', $host) && self::validateDomain($host) !== false) {
return true;
}
@@ -342,7 +357,8 @@ class Validate
* @return bool
* @throws Exception
*/
public static function validateBase64Image(string $base64string) {
public static function validateBase64Image(string $base64string)
{
if (!extension_loaded('gd')) {
Response::standardError('phpgdextensionnotavailable', null, true);

View File

@@ -2599,6 +2599,7 @@ try_fallback = true;
allow_username_mismatch = true;
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
selector_map = "/etc/rspamd/dkim_selectors.map";
use_esld = false;
]]>
</content>
</file>

View File

@@ -4168,6 +4168,7 @@ try_fallback = true;
allow_username_mismatch = true;
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
selector_map = "/etc/rspamd/dkim_selectors.map";
use_esld = false;
]]>
</content>
</file>

View File

@@ -3391,6 +3391,7 @@ try_fallback = true;
allow_username_mismatch = true;
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
selector_map = "/etc/rspamd/dkim_selectors.map";
use_esld = false;
]]>
</content>
</file>

View File

@@ -3381,6 +3381,7 @@ try_fallback = true;
allow_username_mismatch = true;
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
selector_map = "/etc/rspamd/dkim_selectors.map";
use_esld = false;
]]>
</content>
</file>

View File

@@ -2054,6 +2054,7 @@ try_fallback = true;
allow_username_mismatch = true;
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
selector_map = "/etc/rspamd/dkim_selectors.map";
use_esld = false;
]]>
</content>
</file>

16
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"@fortawesome/fontawesome-free": "^6.7.2",
"@popperjs/core": "^2.11.8",
"@vitejs/plugin-vue": "^5.2.1",
"axios": "^1.8.1",
"axios": "^1.8.2",
"bootstrap": "^5.3.3",
"chart.js": "^4.4.8",
"jquery": "^3.7.1",
@@ -19,7 +19,7 @@
"postcss": "^8.5.3",
"resolve-url-loader": "^5.0.0",
"sass": "^1.85.1",
"vite": "^6.2.0",
"vite": "^6.2.6",
"vue": "^3.5.13"
},
"engines": {
@@ -1264,9 +1264,9 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2107,9 +2107,9 @@
}
},
"node_modules/vite": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
"version": "6.2.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -9,7 +9,7 @@
"@fortawesome/fontawesome-free": "^6.7.2",
"@popperjs/core": "^2.11.8",
"@vitejs/plugin-vue": "^5.2.1",
"axios": "^1.8.1",
"axios": "^1.8.2",
"bootstrap": "^5.3.3",
"chart.js": "^4.4.8",
"jquery": "^3.7.1",
@@ -19,7 +19,7 @@
"postcss": "^8.5.3",
"resolve-url-loader": "^5.0.0",
"sass": "^1.85.1",
"vite": "^6.2.0",
"vite": "^6.2.6",
"vue": "^3.5.13"
},
"engines": {

View File

@@ -19,7 +19,7 @@
<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/>
<input class="form-control" type="text" name="2fa_code" id="2fa_code" value="" autocomplete="one-time-code" autofocus required/>
</div>
{% if remember_me %}

View File

@@ -142,6 +142,12 @@ class ValidateTest extends TestCase
$this->assertFalse($result);
$result = Validate::validateUrl("172.16.0.1:8080", true);
$this->assertTrue($result);
$result = Validate::validateUrl("https://xn--frxlr-kuac.de/", true);
$this->assertTrue($result);
$result = Validate::validateUrl("https://2a10:ec2::193:107:51:5/test");
$this->assertFalse($result);
$result = Validate::validateUrl("https://[2a10:ec2::193:107:51:5]");
$this->assertTrue($result);
}
public function testValidateDomain()