$success, 'errors' => array_values($errors)],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
exit;
}
function value(string $key): string
{
$raw = $_POST[$key] ?? '';
return is_string($raw) ? trim($raw) : '';
}
function cleanLine(string $text): string
{
return trim((string) preg_replace('/[\r\n]+/u', ' ', $text));
}
function escapeHtml(string $text): string
{
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function textLength(string $text): int
{
return function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text);
}
function normalizeFiles(array $files): array
{
if (!isset($files['name']) || !is_array($files['name'])) {
return [];
}
$result = [];
foreach ($files['name'] as $index => $name) {
$error = $files['error'][$index] ?? UPLOAD_ERR_NO_FILE;
if ($error === UPLOAD_ERR_NO_FILE) {
continue;
}
$result[] = [
'name' => (string) $name,
'type' => (string) ($files['type'][$index] ?? ''),
'tmp_name' => (string) ($files['tmp_name'][$index] ?? ''),
'error' => (int) $error,
'size' => (int) ($files['size'][$index] ?? 0),
];
}
return $result;
}
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'GET' && isset($_GET['regulamin'])) {
$pdf = base64_decode(EMBEDDED_REGULATIONS_PDF, true);
if ($pdf === false) {
http_response_code(500);
exit('Nie udało się otworzyć regulaminu.');
}
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="regulamin-uslugi-projektowej-lubar.pdf"');
header('Content-Length: ' . strlen($pdf));
header('X-Content-Type-Options: nosniff');
echo $pdf;
exit;
}
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
// Prosty honeypot przeciw automatom.
if (value('website') !== '') {
respond(200, true);
}
$errors = [];
$submittedAt = value('submitted_at');
if ($submittedAt !== '' && ctype_digit($submittedAt)) {
$elapsedMs = (int) round(microtime(true) * 1000) - (int) $submittedAt;
if ($elapsedMs >= 0 && $elapsedMs < 1500) {
$errors[] = 'Formularz został wysłany zbyt szybko. Odczekaj chwilę i spróbuj ponownie.';
}
}
$fullName = value('full_name');
$address = value('address');
$phone = value('phone');
$email = value('email');
$area = value('area');
$variant = value('variant');
$projectNotes = value('project_notes');
$photoConsent = value('photo_consent');
$regulationsAcceptance = value('regulations_acceptance');
$privacyAcceptance = value('privacy_acceptance');
if ($fullName === '') $errors[] = 'Uzupełnij imię i nazwisko.';
if ($address === '') $errors[] = 'Uzupełnij adres lub lokalizację inwestycji.';
if ($phone === '') $errors[] = 'Uzupełnij numer telefonu.';
if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) $errors[] = 'Podaj poprawny adres e-mail.';
if (textLength($fullName) > 120) $errors[] = 'Imię i nazwisko jest zbyt długie.';
if (textLength($address) > 240) $errors[] = 'Adres jest zbyt długi.';
if (textLength($phone) > 30) $errors[] = 'Numer telefonu jest zbyt długi.';
if (textLength($email) > 160) $errors[] = 'Adres e-mail jest zbyt długi.';
if (textLength($projectNotes) > 4000) $errors[] = 'Treść uwag jest zbyt długa.';
if ($area !== '' && (!ctype_digit($area) || (int) $area < 1 || (int) $area > 99999)) {
$errors[] = 'Powierzchnia musi być liczbą od 1 do 99999 m².';
}
$allowedVariants = [
'e-Projekt Koncepcja 2D - projekt za złotówkę',
'Koncepcja Plus',
'Koncepcja Premium 3D - PROMOCJA',
];
if (!in_array($variant, $allowedVariants, true)) {
$errors[] = 'Wybierz wariant wykonania usługi.';
}
$allowedConsents = ['Wyrażam zgodę', 'Nie wyrażam zgody'];
if (!in_array($photoConsent, $allowedConsents, true)) {
$errors[] = 'Wskaż decyzję dotyczącą wykorzystania fotografii lub filmów.';
}
if ($regulationsAcceptance !== '1') $errors[] = 'Zaakceptuj regulamin usługi.';
if ($privacyAcceptance !== '1') $errors[] = 'Potwierdź zapoznanie się z Polityką prywatności.';
$files = normalizeFiles($_FILES['attachments'] ?? []);
if (count($files) > MAX_FILES) {
$errors[] = 'Można przesłać maksymalnie ' . MAX_FILES . ' plików.';
}
$allowedTypes = [
'pdf' => ['application/pdf'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'webp' => ['image/webp'],
'doc' => ['application/msword', 'application/CDFV2', 'application/x-ole-storage'],
'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip'],
'xls' => ['application/vnd.ms-excel', 'application/CDFV2', 'application/x-ole-storage'],
'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/zip'],
'dwg' => ['image/vnd.dwg', 'application/acad', 'application/x-acad', 'application/octet-stream'],
'dxf' => ['image/vnd.dxf', 'application/dxf', 'application/x-dxf', 'application/octet-stream', 'text/plain'],
];
$totalSize = 0;
$preparedFiles = [];
if (!class_exists('finfo')) {
$errors[] = 'Na serwerze nie jest włączone rozszerzenie PHP fileinfo potrzebne do obsługi załączników.';
} else {
$finfo = new finfo(FILEINFO_MIME_TYPE);
foreach ($files as $file) {
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = 'Nie udało się przesłać pliku „' . cleanLine($file['name']) . '”. Kod błędu: ' . $file['error'] . '.';
continue;
}
if ($file['size'] <= 0 || $file['size'] > MAX_FILE_SIZE) {
$errors[] = 'Plik „' . cleanLine($file['name']) . '” jest pusty albo przekracza limit 8 MB.';
continue;
}
$totalSize += $file['size'];
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$detectedType = $finfo->file($file['tmp_name']) ?: 'application/octet-stream';
if (!array_key_exists($extension, $allowedTypes) || !in_array($detectedType, $allowedTypes[$extension], true)) {
$errors[] = 'Niedozwolony typ pliku „' . cleanLine($file['name']) . '”.';
continue;
}
if (!is_uploaded_file($file['tmp_name']) || !is_readable($file['tmp_name'])) {
$errors[] = 'Nie można odczytać pliku „' . cleanLine($file['name']) . '”.';
continue;
}
$safeName = preg_replace('/[^\pL\pN._() -]+/u', '_', basename($file['name']));
$safeName = trim((string) $safeName, '. ');
if ($safeName === '') $safeName = 'zalacznik.' . $extension;
$preparedFiles[] = [
'name' => $safeName,
'type' => $detectedType,
'tmp_name' => $file['tmp_name'],
];
}
}
if ($totalSize > MAX_TOTAL_SIZE) {
$errors[] = 'Łączny rozmiar załączników przekracza limit 20 MB.';
}
if ($errors !== []) {
respond(422, false, $errors);
}
$timestamp = new DateTimeImmutable('now', new DateTimeZone('Europe/Warsaw'));
$subjectText = 'Nowe zgłoszenie projektu: ' . $fullName . ' - ' . $variant;
$encodedSubject = '=?UTF-8?B?' . base64_encode($subjectText) . '?=';
$ip = cleanLine((string) ($_SERVER['REMOTE_ADDR'] ?? 'brak danych'));
$userAgent = cleanLine((string) ($_SERVER['HTTP_USER_AGENT'] ?? 'brak danych'));
$rows = [
'Data zgłoszenia' => $timestamp->format('d.m.Y H:i'),
'Imię i nazwisko' => $fullName,
'Telefon' => $phone,
'E-mail' => $email,
'Adres / lokalizacja' => $address,
'Powierzchnia' => $area !== '' ? $area . ' m²' : 'nie podano',
'Wariant usługi' => $variant,
'Zgoda na fotografie/filmy' => $photoConsent,
'Akceptacja regulaminu' => 'Tak',
'Polityka prywatności' => 'Potwierdzona',
'Adres IP' => $ip,
];
$htmlRows = '';
foreach ($rows as $label => $content) {
$htmlRows .= '
'
. '| ' . escapeHtml($label) . ' | '
. '' . nl2br(escapeHtml($content)) . ' | '
. '
';
}
$htmlBody = ''
. '
'
. '
LUBAR
Nowe zgłoszenie usługi projektowej
'
. '
'
. '
'
. '
Założenia projektu / uwagi
'
. '
' . ($projectNotes !== '' ? nl2br(escapeHtml($projectNotes)) : 'Nie podano.') . '
'
. '
Przeglądarka: ' . escapeHtml($userAgent) . '
'
. '
';
$boundaryMixed = 'mixed_' . bin2hex(random_bytes(16));
$boundaryAlt = 'alt_' . bin2hex(random_bytes(16));
$eol = "\r\n";
$headers = [
'MIME-Version: 1.0',
'From: ' . SENDER_NAME . ' <' . SENDER_EMAIL . '>',
'Reply-To: ' . cleanLine($fullName) . ' <' . $email . '>',
'Content-Type: multipart/mixed; boundary="' . $boundaryMixed . '"',
'X-Mailer: PHP/' . PHP_VERSION,
];
$message = '--' . $boundaryMixed . $eol;
$message .= 'Content-Type: multipart/alternative; boundary="' . $boundaryAlt . '"' . $eol . $eol;
$plainBody = "NOWE ZGŁOSZENIE USŁUGI PROJEKTOWEJ\n\n";
foreach ($rows as $label => $content) {
$plainBody .= $label . ': ' . $content . "\n";
}
$plainBody .= "\nZałożenia projektu / uwagi:\n" . ($projectNotes !== '' ? $projectNotes : 'Nie podano.') . "\n";
$message .= '--' . $boundaryAlt . $eol;
$message .= 'Content-Type: text/plain; charset=UTF-8' . $eol;
$message .= 'Content-Transfer-Encoding: base64' . $eol . $eol;
$message .= chunk_split(base64_encode($plainBody)) . $eol;
$message .= '--' . $boundaryAlt . $eol;
$message .= 'Content-Type: text/html; charset=UTF-8' . $eol;
$message .= 'Content-Transfer-Encoding: base64' . $eol . $eol;
$message .= chunk_split(base64_encode($htmlBody)) . $eol;
$message .= '--' . $boundaryAlt . '--' . $eol;
foreach ($preparedFiles as $file) {
$content = file_get_contents($file['tmp_name']);
if ($content === false) {
respond(500, false, ['Nie udało się odczytać jednego z załączników.']);
}
$encodedFilename = rawurlencode($file['name']);
$message .= '--' . $boundaryMixed . $eol;
$message .= 'Content-Type: ' . $file['type'] . '; name="attachment"' . $eol;
$message .= 'Content-Transfer-Encoding: base64' . $eol;
$message .= "Content-Disposition: attachment; filename*=UTF-8''" . $encodedFilename . $eol . $eol;
$message .= chunk_split(base64_encode($content)) . $eol;
}
$message .= '--' . $boundaryMixed . '--' . $eol;
if (!function_exists('mail')) {
respond(500, false, ['Na serwerze nie jest dostępna funkcja mail(). Skonfiguruj wysyłkę poczty w PHP.']);
}
$sent = mail(
RECIPIENT,
$encodedSubject,
$message,
implode($eol, $headers)
);
if (!$sent) {
respond(500, false, ['Serwer nie przyjął wiadomości do wysyłki. Sprawdź konfigurację poczty PHP.']);
}
respond(200, true);
}
?>
Formularz usługi projektowej | LUBAR