Files
vojta 4520137240 Nepoužívat mb_strlen() pro validaci uživatelského jména
Jméno je hned za tím validované regexem jen na ASCII znaky, takže
mb_strlen() byl zbytečný a navíc vyžadoval rozšíření mbstring, které
na produkčním serveru chybělo a shazovalo registraci s 500.
2026-08-05 12:41:33 +02:00

104 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
function client_ip(): string
{
// Za reverzní proxy (Nginx Proxy Manager, Cloudflare, ...) je
// REMOTE_ADDR vždy IP proxy, ne návštěvníka — rate limiting by tak
// omylem sdílel jeden limit pro úplně všechny. TRUST_PROXY_HEADERS=true
// v .env zapíná čtení X-Forwarded-For; zapínej ho JEN pokud je tenhle
// web dostupný z internetu výhradně přes tu proxy (Apache port 80 by
// neměl být veřejně dosažitelný přímo), jinak si klient IP jednoduše
// vymyslí a rate limiting úplně obejde.
if (env('TRUST_PROXY_HEADERS', 'false') === 'true') {
$forwardedFor = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
if ($forwardedFor !== '') {
$first = trim(explode(',', $forwardedFor)[0]);
if (filter_var($first, FILTER_VALIDATE_IP)) {
return $first;
}
}
}
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0';
}
function csrf_token(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrf_verify(?string $token): bool
{
return is_string($token)
&& !empty($_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $token);
}
/**
* Honeypot: skryté pole, které lidský uživatel nikdy nevyplní, ale
* jednoduší boti ano.
*/
function honeypot_triggered(): bool
{
return !empty($_POST['website']);
}
/**
* @return string[] seznam chyb (prázdné pole = validní)
*/
function validate_username(string $username, array $rules): array
{
$errors = [];
// Uživatelské jméno je o pár řádků níž omezené regexem na čisté ASCII
// (A-Z, 0-9), takže obyčejné strlen() stačí a nepřidává závislost na
// rozšíření mbstring.
$len = strlen($username);
if ($len < $rules['username_min'] || $len > $rules['username_max']) {
$errors[] = sprintf(
'Uživatelské jméno musí mít %d až %d znaků.',
$rules['username_min'],
$rules['username_max']
);
}
if (!preg_match('/^[A-Za-z0-9]+$/', $username)) {
$errors[] = 'Uživatelské jméno smí obsahovat pouze písmena A-Z a číslice (bez diakritiky a speciálních znaků).';
}
return $errors;
}
function validate_password(string $password, array $rules): array
{
$errors = [];
$len = strlen($password);
if ($len < $rules['password_min'] || $len > $rules['password_max']) {
$errors[] = sprintf(
'Heslo musí mít %d až %d znaků.',
$rules['password_min'],
$rules['password_max']
);
}
if (!preg_match('/^[\x21-\x7E]+$/', $password)) {
$errors[] = 'Heslo smí obsahovat pouze běžné znaky ASCII (bez mezer a diakritiky) — herní klient WoW jiné znaky nepodporuje.';
}
return $errors;
}
function validate_email(string $email): array
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return ['Zadej platnou e-mailovou adresu.'];
}
return [];
}