Replace v1 with v2 codebase

Full rewrite: swap out the v1 framework (src/, controllers/, views/,
twig/, sass/, skeleton/) for the working v2 codebase from phpproject
(App/, novaconium/, public/).
This commit is contained in:
code
2026-07-14 01:58:25 +00:00
parent d9c4b64d9c
commit fbd4e92e9f
460 changed files with 30107 additions and 4661 deletions
+54
View File
@@ -0,0 +1,54 @@
<?php
use App\Response;
use Lib\Csrf;
use Lib\FormValidator;
use Lib\Input;
use Lib\Mailer;
use Lib\SpamGuard;
$errors = [];
$old = ['name' => '', 'email' => '', 'message' => ''];
$spamGuard = new SpamGuard();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!Csrf::verify(Input::post('csrf_token'))) {
return Response::redirect('/contact?error=security');
}
$old = [
'name' => Input::post('name', ''),
'email' => Input::post('email', ''),
'message' => Input::post('message', ''),
];
$validator = (new FormValidator())
->required($old['name'], 'name', 'Name is required.')
->email($old['email'], 'email', 'A valid email is required.')
->required($old['message'], 'message', 'Message is required.');
if ($validator->passes()) {
// $spamGuard checks the honeypot field + submission timing (see
// Lib\SpamGuard and index.twig's hidden fields) — a bot gets the
// exact same redirect a human would either way, with nothing in
// the response revealing which check it tripped, or that a check
// exists at all. Only Mailer::send() is skipped for spam.
if (!$spamGuard->isSpam(Input::post())) {
(new Mailer())->send($old['name'], $old['email'], $old['message']);
}
return Response::redirect('/contact?sent=1');
}
$errors = $validator->errors();
}
return [
'errors' => $errors,
'old' => $old,
'sent' => Input::get('sent') !== null,
'securityError' => Input::get('error') === 'security',
'renderedAt' => $spamGuard->renderedAt(),
'csrfField' => Csrf::fieldName(),
'csrfToken' => Csrf::token(),
];
+63
View File
@@ -0,0 +1,63 @@
{% extends layout %}
{% block title %}Contact{% endblock %}
{% block description %}Get in touch — send a message using the contact form.{% endblock %}
{# Every block below is optional — the layout already supplies a sensible
default for each (see /admin/docs/seo). Shown here uncommented as a
reference for every override a page can make; delete what you don't
need. #}
{% block robots %}index, follow{% endblock %}
{% block canonical %}{{ request_path|default('/') }}{% endblock %}
{% block og_type %}website{% endblock %}
{% block og_title %}{{ block('title') }}{% endblock %}
{% block og_description %}{{ block('description') }}{% endblock %}
{% block og_url %}{{ block('canonical') }}{% endblock %}
{% block twitter_card %}summary{% endblock %}
{% block twitter_title %}{{ block('title') }}{% endblock %}
{% block twitter_description %}{{ block('description') }}{% endblock %}
{% import '_layout/icons.twig' as icons %}
{% block content %}
<article>
<h1 class="icon-heading">{{ icons.email() }}Contact</h1>
<p>This form is handled entirely by <code>App/pages/contact/index.php</code>, a sidecar demonstrating the classic POST/redirect/GET pattern: it validates <code>$_POST</code>, calls <code>Lib\Mailer::send()</code> on success, and redirects to <code>/contact?sent=1</code> — so refreshing the page after submitting never resubmits the form. Because this page has a sidecar, it's never served from the static cache; see <a class="icon-link" href="/admin/docs/sidecars">{{ icons.book() }}Sidecars</a> and <a class="icon-link" href="/admin/docs/libraries">{{ icons.book() }}Libraries</a> for the full contract. It's also a working example of self-hosted spam prevention (honeypot field + submission-timing check, no external CAPTCHA service) — see <a class="icon-link" href="/admin/docs/sidecars">{{ icons.book() }}Sidecars</a>' "Spam prevention" section.</p>
{% if sent %}
<p><strong>Thanks — your message has been sent.</strong></p>
{% endif %}
{% if securityError %}
<p><strong>Your session expired before submitting — please try again.</strong></p>
{% endif %}
<form method="post" action="/contact">
<input type="hidden" name="{{ csrfField }}" value="{{ csrfToken }}">
<div class="hp-field" aria-hidden="true">
<label for="website">Leave this field blank</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
<input type="hidden" name="rendered_at" value="{{ renderedAt }}">
<p>
<label for="name">Name</label><br>
<input type="text" id="name" name="name" value="{{ old.name }}">
{% if errors.name %}<br><small>{{ errors.name }}</small>{% endif %}
</p>
<p>
<label for="email">Email</label><br>
<input type="text" id="email" name="email" value="{{ old.email }}">
{% if errors.email %}<br><small>{{ errors.email }}</small>{% endif %}
</p>
<p>
<label for="message">Message</label><br>
<textarea id="message" name="message" rows="5">{{ old.message }}</textarea>
{% if errors.message %}<br><small>{{ errors.message }}</small>{% endif %}
</p>
<button type="submit">Send</button>
</form>
</article>
{% endblock %}