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, '
Nie możemy teraz obsłużyć żądania. Spróbuj ponownie później.
Numer błędu: ' . $reference . '
'); } catch (\Throwable) { return new Response(503, 'Ta strona nie jest dostępna.
'); } $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' => 'Automatyzacja, integracje i rozwiązania informatyczne.
']; } }