175 lines
9.0 KiB
PHP
175 lines
9.0 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace FDMTech;
|
|
|
|
final class Application
|
|
{
|
|
private ?CmsRepository $repository = null;
|
|
private string $nonce;
|
|
|
|
public function __construct(private Config $config)
|
|
{
|
|
$this->nonce = base64_encode(random_bytes(24));
|
|
}
|
|
|
|
private function repository(): CmsRepository
|
|
{
|
|
return $this->repository ??= new CmsRepository(Database::connect($this->config));
|
|
}
|
|
|
|
public function handle(array $server, array $post, array &$session): Response
|
|
{
|
|
try {
|
|
return $this->dispatch($server, $post, $session);
|
|
} catch (\Throwable $error) {
|
|
// Never log inquiry bodies, credentials, SQL, or raw exception messages.
|
|
$reference = bin2hex(random_bytes(6));
|
|
error_log('FDMTech request failure ' . $reference . ' [' . $error::class . ']');
|
|
$data = $this->baseData($session, false);
|
|
$data['formStatus'] = 'unavailable';
|
|
try {
|
|
return $this->render($data, 503, '<h1>Serwis jest chwilowo niedostępny</h1><p>Nie możemy teraz obsłużyć żądania. Spróbuj ponownie później.</p><p>Numer błędu: ' . $reference . '</p>');
|
|
} catch (\Throwable) {
|
|
return new Response(503, '<!doctype html><html lang="pl"><meta charset="utf-8"><title>Serwis niedostępny</title><h1>Serwis jest chwilowo niedostępny</h1></html>', $this->headers() + ['Retry-After' => '60']);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function dispatch(array $server, array $post, array &$session): Response
|
|
{
|
|
$method = $server['REQUEST_METHOD'] ?? 'GET';
|
|
$route = Router::route($server['REQUEST_URI'] ?? '/');
|
|
$demo = $this->config->bool('APP_DEMO');
|
|
$data = $this->baseData($session, $demo);
|
|
if (isset($route['error'])) {
|
|
return $this->render($data, 404, '<h1>Nie znaleziono strony</h1><p><a href="/">Wróć na stronę główną</a>.</p>');
|
|
}
|
|
if (isset($route['redirect'])) {
|
|
return new Response(308, '', $this->headers() + ['Location' => $route['redirect']]);
|
|
}
|
|
$slug = $route['slug'];
|
|
if (!in_array($method, $slug === 'inquiries' ? ['POST'] : ['GET', 'HEAD'], true)) {
|
|
$response = $this->render($data, 405, '<h1>Niedozwolona metoda żądania</h1>');
|
|
$response->headers['Allow'] = $slug === 'inquiries' ? 'POST' : 'GET, HEAD';
|
|
return $response;
|
|
}
|
|
if ($slug === 'inquiries') {
|
|
return $this->inquiry($server, $post, $session, $data);
|
|
}
|
|
$data['site'] = $demo ? Settings::defaults() : $this->repository()->settings();
|
|
if ($slug === 'kontakt') {
|
|
$data['page']['title'] = 'Kontakt';
|
|
$data['page']['slug'] = 'kontakt';
|
|
if (isset($session['inquiry_saved'])) {
|
|
$data['formStatus'] = 'saved';
|
|
$data['formNotice'] = 'Dziękujemy. Twoje zapytanie zostało zapisane i czeka na obsługę przez zespół FDMTech.';
|
|
unset($session['inquiry_saved']);
|
|
}
|
|
return $this->render($data, 200, View::form($data), '/kontakt');
|
|
}
|
|
$page = $demo ? $this->demoPage($slug) : $this->repository()->page($slug);
|
|
if ($page === null) {
|
|
return $this->render($data, 404, '<h1>Nie znaleziono strony</h1><p>Ta strona nie jest dostępna.</p>');
|
|
}
|
|
$data['page'] = $page;
|
|
return $this->render($data, 200, null, $route['path']);
|
|
}
|
|
|
|
private function inquiry(array $server, array $post, array &$session, array $data): Response
|
|
{
|
|
$data['page']['title'] = 'Kontakt';
|
|
$data['page']['slug'] = 'kontakt';
|
|
$fail = function (int $status, string $message, string $state = 'invalid') use (&$data): Response {
|
|
$data['formErrors']['form'] = $message;
|
|
$data['formStatus'] = $state;
|
|
return $this->render($data, $status, View::form($data), '/kontakt');
|
|
};
|
|
if ($data['isDemo']) {
|
|
return $fail(503, 'Tryb demonstracyjny nie zapisuje ani nie wysyła zgłoszeń.', 'unavailable');
|
|
}
|
|
if ((int) ($server['CONTENT_LENGTH'] ?? 0) > 65536 || strlen(http_build_query($post)) > 65536) {
|
|
return $fail(413, 'Formularz przekracza dopuszczalny rozmiar. Skróć wiadomość.');
|
|
}
|
|
if (strtolower(trim(explode(';', $server['CONTENT_TYPE'] ?? '')[0])) !== 'application/x-www-form-urlencoded') {
|
|
return $fail(415, 'Nieobsługiwany format formularza. Użyj formularza na stronie.');
|
|
}
|
|
if (!(new RateLimiter($this->config))->consume($server['REMOTE_ADDR'] ?? 'unknown')) {
|
|
$response = $fail(429, 'Zbyt wiele prób. Odczekaj kilka minut i spróbuj ponownie.', 'limited');
|
|
$response->headers['Retry-After'] = (string) $this->config->number('INQUIRY_RATE_WINDOW', 900);
|
|
return $response;
|
|
}
|
|
if (!is_string($post['_csrf'] ?? null) || !hash_equals($data['csrfToken'], $post['_csrf'])) {
|
|
return $fail(403, 'Sesja formularza wygasła. Otwórz formularz ponownie i spróbuj jeszcze raz.');
|
|
}
|
|
if (($post['website'] ?? '') !== '') {
|
|
return $fail(422, 'Nie udało się zweryfikować formularza. Spróbuj ponownie.');
|
|
}
|
|
[$data['formValues'], $data['formErrors']] = Inquiry::validate($post);
|
|
if ($data['formErrors']) {
|
|
$data['formStatus'] = 'invalid';
|
|
return $this->render($data, 422, View::form($data), '/kontakt');
|
|
}
|
|
try {
|
|
$repository = $this->repository();
|
|
$data['site'] = $repository->settings();
|
|
$repository->insertInquiry($data['formValues']);
|
|
} catch (\Throwable $error) {
|
|
error_log('FDMTech inquiry storage failure [' . $error::class . ']');
|
|
return $fail(503, 'Nie udało się zapisać zapytania. Spróbuj ponownie później.', 'unavailable');
|
|
}
|
|
$session['inquiry_saved'] = true;
|
|
$session['csrf'] = bin2hex(random_bytes(32));
|
|
$session['csrf_at'] = time();
|
|
return new Response(303, '', $this->headers() + ['Location' => '/kontakt']);
|
|
}
|
|
|
|
private function baseData(array &$session, bool $demo): array
|
|
{
|
|
if (!isset($session['csrf'], $session['csrf_at']) || $session['csrf_at'] < time() - 7200) {
|
|
$session['csrf'] = bin2hex(random_bytes(32));
|
|
$session['csrf_at'] = time();
|
|
}
|
|
return ['site' => Settings::defaults(), 'page' => ['slug' => '', 'title' => 'FDMTech', 'seo_title' => '',
|
|
'seo_description' => '', 'published_html' => '', 'published_css' => '', 'published_at' => null],
|
|
'isDemo' => $demo, 'csrfToken' => $session['csrf'], 'formAction' => '/inquiries',
|
|
'formValues' => [], 'formErrors' => [], 'formNotice' => null, 'formStatus' => 'idle', 'smtpStatus' => 'disabled', 'nonce' => $this->nonce];
|
|
}
|
|
|
|
private function render(array $data, int $status, ?string $html = null, string $path = '/'): Response
|
|
{
|
|
$data['statusCode'] = $status;
|
|
$data['meta'] = ['title' => $status >= 400 ? ($status . ' — ' . $data['site']['name'])
|
|
: (($data['page']['seo_title'] ?: $data['page']['title']) . ' — ' . $data['site']['name']),
|
|
'description' => $data['page']['seo_description'] ?? '',
|
|
'canonical' => $status === 200 ? $this->config->canonical($path) : '',
|
|
'robots' => $data['isDemo'] || $status !== 200 || $path === '/kontakt' ? 'noindex, nofollow' : 'index, follow'];
|
|
$headers = $this->headers();
|
|
if ($data['meta']['robots'] === 'noindex, nofollow') {
|
|
$headers['X-Robots-Tag'] = 'noindex, nofollow';
|
|
}
|
|
if ($status === 503) {
|
|
$headers['Retry-After'] = '60';
|
|
}
|
|
return new Response($status, (new View($this->config->root))->render($data, $html), $headers);
|
|
}
|
|
|
|
private function headers(): array
|
|
{
|
|
return ['Content-Type' => 'text/html; charset=UTF-8', 'Cache-Control' => 'no-store',
|
|
'Content-Security-Policy' => "default-src 'none'; script-src 'self' 'nonce-{$this->nonce}'; style-src 'self' 'nonce-{$this->nonce}'; img-src 'self' https:; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'; object-src 'none'",
|
|
'X-Content-Type-Options' => 'nosniff', 'Referrer-Policy' => 'strict-origin-when-cross-origin',
|
|
'X-Frame-Options' => 'DENY', 'Permissions-Policy' => 'camera=(), microphone=(), geolocation=()'];
|
|
}
|
|
|
|
private function demoPage(string $slug): ?array
|
|
{
|
|
if ($slug !== 'home') {
|
|
return null;
|
|
}
|
|
return ['slug' => 'home', 'title' => 'Technologia dla Twojej firmy', 'seo_title' => 'FDMTech — demonstracja',
|
|
'seo_description' => 'Wersja demonstracyjna serwisu FDMTech.', 'published_at' => null, 'published_css' => '',
|
|
'published_html' => '<h1>Technologia, która wspiera Twoją firmę</h1><p>Automatyzacja, integracje i rozwiązania informatyczne.</p><p><a href="/kontakt">Porozmawiajmy o projekcie</a></p>'];
|
|
}
|
|
}
|