4520137240
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.
104 lines
3.1 KiB
PHP
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 [];
|
|
}
|