]> danix's work - danix.xyz-2.git/commitdiff
added contact form backend
authorDanilo M. <redacted>
Wed, 15 Apr 2026 13:58:26 +0000 (15:58 +0200)
committerDanilo M. <redacted>
Wed, 15 Apr 2026 13:58:26 +0000 (15:58 +0200)
static/api/composer.json [new file with mode: 0644]
static/api/composer.lock [new file with mode: 0644]
static/api/contact.php [new file with mode: 0644]
static/contact.php [deleted file]

diff --git a/static/api/composer.json b/static/api/composer.json
new file mode 100644 (file)
index 0000000..078861f
--- /dev/null
@@ -0,0 +1,5 @@
+{
+    "require": {
+        "phpmailer/phpmailer": "^7.0"
+    }
+}
diff --git a/static/api/composer.lock b/static/api/composer.lock
new file mode 100644 (file)
index 0000000..447fe18
--- /dev/null
@@ -0,0 +1,101 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "826f515f5ef16946d3e3ee3e3205b25e",
+    "packages": [
+        {
+            "name": "phpmailer/phpmailer",
+            "version": "v7.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPMailer/PHPMailer.git",
+                "reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
+                "reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-filter": "*",
+                "ext-hash": "*",
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+                "doctrine/annotations": "^1.2.6 || ^1.13.3",
+                "php-parallel-lint/php-console-highlighter": "^1.0.0",
+                "php-parallel-lint/php-parallel-lint": "^1.3.2",
+                "phpcompatibility/php-compatibility": "^10.0.0@dev",
+                "squizlabs/php_codesniffer": "^3.13.5",
+                "yoast/phpunit-polyfills": "^1.0.4"
+            },
+            "suggest": {
+                "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
+                "directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
+                "ext-imap": "Needed to support advanced email address parsing according to RFC822",
+                "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+                "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+                "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
+                "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+                "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+                "psr/log": "For optional PSR-3 debug logging",
+                "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
+                "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PHPMailer\\PHPMailer\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1-only"
+            ],
+            "authors": [
+                {
+                    "name": "Marcus Bointon",
+                    "email": "phpmailer@synchromedia.co.uk"
+                },
+                {
+                    "name": "Jim Jagielski",
+                    "email": "jimjag@gmail.com"
+                },
+                {
+                    "name": "Andy Prevost",
+                    "email": "codeworxtech@users.sourceforge.net"
+                },
+                {
+                    "name": "Brent R. Matzelle"
+                }
+            ],
+            "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+            "support": {
+                "issues": "https://github.com/PHPMailer/PHPMailer/issues",
+                "source": "https://github.com/PHPMailer/PHPMailer/tree/v7.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Synchro",
+                    "type": "github"
+                }
+            ],
+            "time": "2026-01-09T18:02:33+00:00"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": {},
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {},
+    "platform-dev": {},
+    "plugin-api-version": "2.6.0"
+}
diff --git a/static/api/contact.php b/static/api/contact.php
new file mode 100644 (file)
index 0000000..4689b99
--- /dev/null
@@ -0,0 +1,180 @@
+<?php
+/**
+ * Contact form handler for danix.me
+ *
+ * Spam protection:
+ *   1. Honeypot field  — bots fill it, humans don't see it
+ *   2. Timing check    — reject submissions faster than MIN_ELAPSED seconds
+ *   3. IP rate limit   — one submission per IP per RATE_WINDOW seconds
+ */
+
+ob_start();
+header('Content-Type: application/json; charset=utf-8');
+header('X-Content-Type-Options: nosniff');
+header('X-Frame-Options: DENY');
+header('Referrer-Policy: strict-origin-when-cross-origin');
+header('Cache-Control: no-store');
+
+// ── Configuration ─────────────────────────────────────────────────────────────
+// Credentials are read from server environment variables — never hardcoded here.
+// Set them in your nginx fastcgi_param, Apache SetEnv, or hosting panel.
+const SENDER_DOMAIN  = 'danix.xyz';         // used in From name and subject prefix
+const ALLOWED_ORIGIN = 'https://danix.me';  // CSRF: only accept from this origin
+const MIN_ELAPSED    = 4;                   // seconds before submission is accepted
+const RATE_WINDOW    = 300;                 // seconds between allowed submissions per IP
+const RATE_DIR       = '/var/lib/danixme/cf_rate'; // owned by www-data:www-data, mode 0700
+
+// $cfg = [
+//     'recipient'   => getenv('MAIL_RECIPIENT')  ?: '',
+//     'smtp_host'   => getenv('MAIL_SMTP_HOST')  ?: '',
+//     'smtp_port'   => (int)(getenv('MAIL_SMTP_PORT') ?: 587),
+//     'smtp_secure' => getenv('MAIL_SMTP_SECURE') ?: 'tls',
+//     'smtp_user'   => getenv('MAIL_SMTP_USER')  ?: '',
+//     'smtp_pass'   => getenv('MAIL_SMTP_PASS')  ?: '',
+// ];
+
+$cfg = require dirname(__DIR__, 2) . '/mail-config.php';
+
+if ($cfg['recipient'] === '' || $cfg['smtp_host'] === '' || $cfg['smtp_user'] === '') {
+    out(500, 'Mail not configured');
+}
+// ──────────────────────────────────────────────────────────────────────────────
+
+// Only accept POST
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+    out(405, 'Method not allowed');
+}
+
+// ── CSRF: verify request originates from our own domain ───────────────────────
+$origin  = $_SERVER['HTTP_ORIGIN']  ?? '';
+$referer = $_SERVER['HTTP_REFERER'] ?? '';
+
+if ($origin !== '') {
+    if ($origin !== ALLOWED_ORIGIN) {
+        out(403, 'Forbidden');
+    }
+} elseif (strpos($referer, ALLOWED_ORIGIN . '/') !== 0) {
+    out(403, 'Forbidden');
+}
+// ─────────────────────────────────────────────────────────────────────────────
+
+// ── Honeypot ──────────────────────────────────────────────────────────────────
+// If the hidden "website" field is filled, it's a bot — silently pretend success.
+if (!empty($_POST['website'])) {
+    out(200, null, true);
+}
+
+// ── Timing check ──────────────────────────────────────────────────────────────
+$loadedAt = (int) filter_input(INPUT_POST, '_t', FILTER_SANITIZE_NUMBER_INT);
+$elapsed  = time() - $loadedAt;
+
+if ($loadedAt === 0 || $elapsed < MIN_ELAPSED || $elapsed > 7200) {
+    out(400, 'Invalid submission');
+}
+
+// ── Rate limiting ─────────────────────────────────────────────────────────────
+$ip      = preg_replace('/[^a-f0-9:.]/', '', $_SERVER['REMOTE_ADDR'] ?? '');
+$lockDir = rtrim(RATE_DIR, '/');
+
+if (!is_dir($lockDir)) {
+    @mkdir($lockDir, 0700, true);
+}
+
+$lockFile = $lockDir . '/' . md5($ip) . '.lock';
+
+// Atomic check+write using exclusive lock — prevents TOCTOU race condition
+$fp = fopen($lockFile, 'c+');
+if ($fp === false || !flock($fp, LOCK_EX)) {
+    out(500, 'Server error');
+}
+$lastTime = (int) fread($fp, 20);
+if ($lastTime > 0 && (time() - $lastTime) < RATE_WINDOW) {
+    flock($fp, LOCK_UN);
+    fclose($fp);
+    out(429, 'Too many requests');
+}
+
+// ── Sanitize & validate ───────────────────────────────────────────────────────
+$name    = clean(INPUT_POST, 'name',    100);
+$email   = trim(filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL) ?? '');
+$subject = clean(INPUT_POST, 'subject', 200);
+$message = clean(INPUT_POST, 'message', 5000);
+
+if ($name === '' || $email === '' || $message === '') {
+    flock($fp, LOCK_UN); fclose($fp);
+    out(400, 'Missing required fields');
+}
+
+if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+    flock($fp, LOCK_UN); fclose($fp);
+    out(400, 'Invalid email address');
+}
+
+// ── Build and send the email via PHPMailer ────────────────────────────────────
+require __DIR__ . '/vendor/autoload.php';
+
+use PHPMailer\PHPMailer\PHPMailer;
+use PHPMailer\PHPMailer\SMTP;
+use PHPMailer\PHPMailer\Exception as MailerException;
+
+$subjectLine = '[contact from danix.me] - ' . ($subject !== '' ? $subject : 'New message from ' . $name);
+
+$body = implode("\n", [
+    'Name:    ' . $name,
+    'Email:   ' . $email,
+    'Subject: ' . ($subject ?: '—'),
+    '',
+    str_repeat('─', 48),
+    '',
+    $message,
+]);
+
+$mail = new PHPMailer(true);
+
+try {
+    $mail->isSMTP();
+    $mail->Host       = $cfg['smtp_host'];
+    $mail->SMTPAuth   = true;
+    $mail->Username   = $cfg['smtp_user'];
+    $mail->Password   = $cfg['smtp_pass'];
+    $mail->SMTPSecure = $cfg['smtp_secure'] === 'ssl' ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS;
+    $mail->Port       = $cfg['smtp_port'];
+    $mail->CharSet    = 'UTF-8';
+
+    $mail->setFrom('danix@' . SENDER_DOMAIN, SENDER_DOMAIN);
+    $mail->addReplyTo($email, $name);
+    $mail->addAddress($cfg['recipient']);
+
+    $mail->Subject = $subjectLine;
+    $mail->Body    = $body;
+
+    $mail->send();
+
+    rewind($fp);
+    ftruncate($fp, 0);
+    fwrite($fp, (string) time());
+    flock($fp, LOCK_UN);
+    fclose($fp);
+    out(200, null, true);
+} catch (MailerException $e) {
+    flock($fp, LOCK_UN);
+    fclose($fp);
+    out(500, 'Failed to send email');
+}
+
+// ── Helpers ───────────────────────────────────────────────────────────────────
+
+function clean(int $type, string $key, int $maxLen): string
+{
+    $val = filter_input($type, $key, FILTER_UNSAFE_RAW) ?? '';
+    $val = strip_tags(trim((string) $val));
+    return mb_substr($val, 0, $maxLen);
+}
+
+function out(int $code, ?string $error, bool $success = false): void
+{
+    ob_end_clean();
+    http_response_code($code);
+    echo json_encode(['success' => $success, 'error' => $error]);
+    exit;
+}
diff --git a/static/contact.php b/static/contact.php
deleted file mode 100644 (file)
index ddd3fe1..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-// Placeholder contact form endpoint
-// This file should be implemented by the site owner
-// to handle form submissions and send emails
-
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
-    // TODO: Implement contact form handler
-    http_response_code(501);
-    echo json_encode(['error' => 'Contact form handler not implemented']);
-}
-?>