104 lines
3.8 KiB
PHP
104 lines
3.8 KiB
PHP
<?php
|
|
// Contact form endpoint for quorlasocial.com.
|
|
// Astro copies this file as-is to dist/api/contact.php; HestiaCP runs it with PHP and sends through the
|
|
// server's own mail system (exim), so the message is DKIM-signed for quorlasocial.com. No passwords in code.
|
|
|
|
declare(strict_types=1);
|
|
|
|
const TO = 'support@quorlasocial.com';
|
|
const FROM = 'support@quorlasocial.com';
|
|
const ALLOWED_ORIGINS = ['https://quorlasocial.com', 'https://www.quorlasocial.com'];
|
|
const MAX_PER_HOUR = 5; // per visitor IP
|
|
const MIN_SECONDS_ON_PAGE = 3; // bots submit instantly
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-store');
|
|
|
|
function reply(int $status, array $body): void {
|
|
http_response_code($status);
|
|
echo json_encode($body);
|
|
exit;
|
|
}
|
|
|
|
function field(string $key, int $max): string {
|
|
$v = trim((string)($_POST[$key] ?? ''));
|
|
$v = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $v) ?? '';
|
|
return mb_substr($v, 0, $max);
|
|
}
|
|
|
|
function oneLine(string $v): string {
|
|
return trim(preg_replace('/[\r\n]+/', ' ', $v) ?? '');
|
|
}
|
|
|
|
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
|
reply(405, ['ok' => false, 'error' => 'Use the form on quorlasocial.com/contact.']);
|
|
}
|
|
|
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
if ($origin !== '' && !in_array($origin, ALLOWED_ORIGINS, true)) {
|
|
reply(403, ['ok' => false, 'error' => 'Not allowed.']);
|
|
}
|
|
|
|
// Honeypot and timing: pretend success so bots don't learn anything.
|
|
if (field('website_fax', 200) !== '') {
|
|
reply(200, ['ok' => true]);
|
|
}
|
|
$started = (int)($_POST['t'] ?? 0);
|
|
if ($started > 0 && (time() - intdiv($started, 1000)) < MIN_SECONDS_ON_PAGE) {
|
|
reply(200, ['ok' => true]);
|
|
}
|
|
|
|
// Rate limit per IP (small file in the system temp folder).
|
|
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
|
$rlFile = sys_get_temp_dir() . '/quorla-contact-' . hash('sha256', $ip);
|
|
$now = time();
|
|
$hits = [];
|
|
if (is_file($rlFile)) {
|
|
$hits = array_filter(array_map('intval', explode(',', (string)file_get_contents($rlFile))), fn($t) => $t > $now - 3600);
|
|
}
|
|
if (count($hits) >= MAX_PER_HOUR) {
|
|
reply(429, ['ok' => false, 'error' => 'Too many requests. Please email support@quorlasocial.com instead.']);
|
|
}
|
|
|
|
$name = oneLine(field('name', 100));
|
|
$agency = oneLine(field('agency', 150));
|
|
$email = oneLine(field('email', 200));
|
|
$clients = oneLine(field('clients', 40));
|
|
$deployment = oneLine(field('deployment', 40));
|
|
$message = field('message', 5000);
|
|
|
|
if ($name === '' || $agency === '' || $clients === '') {
|
|
reply(400, ['ok' => false, 'error' => 'Please fill in your name, agency and number of clients.']);
|
|
}
|
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
reply(400, ['ok' => false, 'error' => 'Please enter a valid work email.']);
|
|
}
|
|
|
|
$subject = 'Quorla access request: ' . $agency . ' (' . $name . ')';
|
|
$body = "New access request from quorlasocial.com/contact\n\n"
|
|
. "Name: $name\n"
|
|
. "Agency: $agency\n"
|
|
. "Work email: $email\n"
|
|
. "Client accounts: $clients\n"
|
|
. "Preferred deployment: " . ($deployment !== '' ? $deployment : 'not given') . "\n\n"
|
|
. "Notes:\n" . ($message !== '' ? $message : '(none)') . "\n\n"
|
|
. "---\nSent " . gmdate('Y-m-d H:i') . " UTC from IP $ip. Reply to this email to answer them directly.\n";
|
|
|
|
$headers = [
|
|
'From: Quorla website <' . FROM . '>',
|
|
'Reply-To: ' . $name . ' <' . $email . '>',
|
|
'MIME-Version: 1.0',
|
|
'Content-Type: text/plain; charset=UTF-8',
|
|
'Content-Transfer-Encoding: 8bit',
|
|
'X-Mailer: quorlasocial.com contact form',
|
|
];
|
|
|
|
$sent = mail(TO, '=?UTF-8?B?' . base64_encode($subject) . '?=', $body, implode("\r\n", $headers), '-f' . FROM);
|
|
if (!$sent) {
|
|
reply(500, ['ok' => false, 'error' => 'We could not send your request. Please email support@quorlasocial.com.']);
|
|
}
|
|
|
|
$hits[] = $now;
|
|
@file_put_contents($rlFile, implode(',', $hits), LOCK_EX);
|
|
reply(200, ['ok' => true]);
|