Zettai_Systems_Website/api/helpers.php
2026-09-18 14:52:33 +05:30

249 lines
7.6 KiB
PHP

<?php
/**
* Shared mail helpers for Zettai Systems forms.
*/
declare(strict_types=1);
const ZS_CONTACT_TO = 'solutions@zettaisystems.com';
const ZS_CAREERS_TO = 'careers@zettaisystems.com';
const ZS_FROM_NAME = 'Zettai Systems Website';
const ZS_FROM_EMAIL = 'noreply@zettaisystems.com';
const ZS_MAX_RESUME_BYTES = 5 * 1024 * 1024; // 5 MB
const ZS_RL_MAX = 5;
const ZS_RL_WINDOW = 900; // 15 minutes
function zs_json_response(int $status, array $payload): void
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function zs_require_post(): void
{
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
zs_json_response(405, ['ok' => 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,
];
}