false, 'error' => 'Method not allowed.']); } } function zs_request_host(): string { $host = $_SERVER['HTTP_HOST'] ?? ''; if (!is_string($host) || $host === '') { return ''; } return strtolower((string) preg_replace('/:\d+$/', '', $host)); } function zs_require_csrf(): void { $host = zs_request_host(); $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (is_string($origin) && $origin !== '') { $originHost = parse_url($origin, PHP_URL_HOST); if (!is_string($originHost) || strtolower($originHost) !== $host) { zs_json_response(403, [ 'ok' => false, 'error' => 'Security check failed. Please refresh the page and try again.', ]); } } $cookie = $_COOKIE['zs_csrf'] ?? ''; $posted = $_POST['csrf_token'] ?? ''; if ( !is_string($cookie) || !is_string($posted) || $cookie === '' || $posted === '' || !hash_equals($cookie, $posted) ) { zs_json_response(403, [ 'ok' => false, 'error' => 'Security check failed. Please refresh the page and try again.', ]); } } function zs_rate_limit(string $bucket): void { $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; if (!is_string($ip) || $ip === '') { $ip = '0.0.0.0'; } $dir = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'zs_form_rl'; if (!is_dir($dir) && !@mkdir($dir, 0700, true) && !is_dir($dir)) { return; } $file = $dir . DIRECTORY_SEPARATOR . hash('sha256', $bucket . '|' . $ip); $fh = @fopen($file, 'c+'); if ($fh === false) { return; } flock($fh, LOCK_EX); $raw = stream_get_contents($fh); $hits = []; if (is_string($raw) && $raw !== '') { $decoded = json_decode($raw, true); if (is_array($decoded)) { $hits = $decoded; } } $now = time(); $windowStart = $now - ZS_RL_WINDOW; $fresh = []; foreach ($hits as $stamp) { if (is_int($stamp) && $stamp > $windowStart) { $fresh[] = $stamp; } } if (count($fresh) >= ZS_RL_MAX) { flock($fh, LOCK_UN); fclose($fh); zs_json_response(429, [ 'ok' => false, 'error' => 'Too many submissions. Please wait a few minutes and try again.', ]); } $fresh[] = $now; rewind($fh); ftruncate($fh, 0); fwrite($fh, json_encode($fresh)); fflush($fh); flock($fh, LOCK_UN); fclose($fh); } function zs_field(string $key, int $max = 2000): string { $value = $_POST[$key] ?? ''; if (!is_string($value)) { return ''; } $value = trim(strip_tags($value)); if (mb_strlen($value) > $max) { $value = mb_substr($value, 0, $max); } return $value; } function zs_is_honeypot_filled(): bool { $hp = $_POST['website'] ?? $_POST['company_url'] ?? ''; return is_string($hp) && trim($hp) !== ''; } function zs_valid_email(string $email): bool { return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); } function zs_send_mail(string $to, string $subject, string $textBody, string $replyTo = '', ?array $attachment = null): bool { $boundary = 'zs_' . bin2hex(random_bytes(12)); $fromHeader = sprintf('%s <%s>', ZS_FROM_NAME, ZS_FROM_EMAIL); $headers = [ 'MIME-Version: 1.0', 'From: ' . $fromHeader, 'Date: ' . gmdate('D, d M Y H:i:s') . ' +0000', 'X-Mailer: ZettaiSystemsForms/1.0', 'X-Priority: 3', ]; if ($replyTo !== '' && zs_valid_email($replyTo)) { $headers[] = 'Reply-To: ' . $replyTo; } if ($attachment === null) { $headers[] = 'Content-Type: text/plain; charset=UTF-8'; $body = $textBody; } else { $headers[] = 'Content-Type: multipart/mixed; boundary="' . $boundary . '"'; $body = "--{$boundary}\r\n"; $body .= "Content-Type: text/plain; charset=UTF-8\r\n"; $body .= "Content-Transfer-Encoding: 8bit\r\n\r\n"; $body .= $textBody . "\r\n\r\n"; $body .= "--{$boundary}\r\n"; $body .= 'Content-Type: ' . $attachment['mime'] . '; name="' . $attachment['name'] . "\"\r\n"; $body .= "Content-Transfer-Encoding: base64\r\n"; $body .= 'Content-Disposition: attachment; filename="' . $attachment['name'] . "\"\r\n\r\n"; $body .= chunk_split(base64_encode($attachment['data'])) . "\r\n"; $body .= "--{$boundary}--"; } $encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?='; $headerString = implode("\r\n", $headers); $envelope = '-f' . ZS_FROM_EMAIL; return @mail($to, $encodedSubject, $body, $headerString, $envelope); } function zs_resume_attachment(): ?array { if (!isset($_FILES['resume']) || !is_array($_FILES['resume'])) { return null; } $file = $_FILES['resume']; if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) { return null; } if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) { zs_json_response(400, ['ok' => false, 'error' => 'Resume upload failed. Please try again.']); } if (($file['size'] ?? 0) <= 0 || $file['size'] > ZS_MAX_RESUME_BYTES) { zs_json_response(400, ['ok' => false, 'error' => 'Resume must be a PDF or Word file under 5 MB.']); } $original = (string) ($file['name'] ?? 'resume'); $ext = strtolower(pathinfo($original, PATHINFO_EXTENSION)); $allowed = [ 'pdf' => 'application/pdf', 'doc' => 'application/msword', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', ]; if (!isset($allowed[$ext])) { zs_json_response(400, ['ok' => false, 'error' => 'Resume must be a PDF or Word document (.pdf, .doc, .docx).']); } $tmp = (string) ($file['tmp_name'] ?? ''); if ($tmp === '' || !is_uploaded_file($tmp)) { zs_json_response(400, ['ok' => false, 'error' => 'Invalid resume upload.']); } if (class_exists('finfo')) { $detected = (new finfo(FILEINFO_MIME_TYPE))->file($tmp) ?: ''; $okMime = [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip', // some hosts report .docx as zip ]; if ($detected !== '' && !in_array($detected, $okMime, true)) { zs_json_response(400, ['ok' => false, 'error' => 'Resume must be a PDF or Word document (.pdf, .doc, .docx).']); } } $data = file_get_contents($tmp); if ($data === false) { zs_json_response(500, ['ok' => false, 'error' => 'Could not read the uploaded resume.']); } $safeName = preg_replace('/[^A-Za-z0-9._-]/', '_', $original) ?: ('resume.' . $ext); return [ 'name' => $safeName, 'mime' => $allowed[$ext], 'data' => $data, ]; }