<?php
/**
 * ORCHARD ORDER FORM HANDLER
 * =========================================================================
 *
 * THE ONLY LINE YOU HAVE TO CHANGE IS $TO, A FEW LINES BELOW.
 * Put your own email address there, upload the folder, and the form works.
 *
 * Everything else already works out of the box:
 *
 *   - checks every field and says exactly what is wrong
 *   - blocks the three commonest kinds of form spam
 *   - saves every order to a CSV file before it tries to email, so an email
 *     problem can never lose you an order
 *   - emails you the order, and emails the customer a copy
 *   - answers with JSON for the page's JavaScript, or a normal thank-you page
 *     for anyone with JavaScript switched off
 *
 * REQUIREMENTS
 *   PHP 7.4 or newer. Nothing to install. No libraries, no Composer.
 *
 * WHERE THE ORDERS ARE SAVED
 *   storage/orders-YYYY-MM.csv, one file per month, next to this script.
 *   Open it in Excel or Google Sheets. The folder is protected by its own
 *   .htaccess so nobody can read it over the web. If your host does not use
 *   Apache (nginx, for example), move the folder above your web root and
 *   change $LOG_DIR below, or check the address is not reachable.
 *
 * IF THE EMAIL DOES NOT ARRIVE
 *   PHP's built-in mail() is not always reliable on shared hosting, and mail
 *   sent this way sometimes lands in spam. Fill in the $SMTP block below with
 *   the details from your email provider and set 'enabled' to true. That
 *   sends the message through your real mailbox, which arrives far more
 *   reliably. Nothing else needs to change.
 */

// =========================================================================
//  SETTINGS LIVE IN fillup.php, NOT HERE.
//
//  The values below are only fallbacks, used if fillup.php is missing or
//  incomplete, so a bad upload shows a working page rather than a blank
//  one. Editing them here does nothing as long as fillup.php exists.
// =========================================================================

$TO                  = 'hello@7uptheme.com';
$TO_NAME             = 'Orchard';
$FROM                = '';
$SUBJECT_TO_YOU      = 'New box order: {box} for {name}';
$INTRO_TO_YOU        = 'A new order came in from the website.';
$FOOT_TO_YOU         = 'Reply to this email and it goes straight to the customer.';
$SEND_COPY           = true;
$SUBJECT_TO_CUSTOMER = 'We have your order';
$MESSAGE_TO_CUSTOMER = "Hello {name},\n\nThank you for your order. Here is what we have:\n\n  Box     : {box}\n  Deliver : {address}\n  Phone   : {phone}\n\nWe will reply with the total and the first delivery date, usually\nwithin one working day. Nothing is charged until the box arrives.\n\nIf anything above is wrong, just reply to this email.";
$MSG_THANKS          = 'Thank you. Your order is with us and we will reply by email, usually within one working day.';
$MSG_FAILED          = 'Sorry, something went wrong at our end and the order did not save. Please email us at {email} instead.';
$MSG_TOO_MANY        = 'That is several orders in a short time. Please wait a little while, or email us instead.';
$MAX_PER_HOUR        = 5;
$REDIRECT_AFTER      = '';
$LOG_DIR             = __DIR__ . '/storage';
$SMTP                = ['enabled' => false, 'host' => '', 'port' => 587, 'secure' => 'tls', 'user' => '', 'pass' => ''];

if (is_file(__DIR__ . '/fillup.php')) {
    require __DIR__ . '/fillup.php';
}

// =========================================================================
//  END OF SETTINGS. You do not need to read past here.
// =========================================================================

/** Answer in JSON when the page's JavaScript asked, HTML when a browser did. */
$wants_json = (
    (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'fetch')
    || (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false)
);

/** Somebody typed the address in. Send them back to the form. */
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
    header('Location: index.html#order', true, 303);
    exit;
}

// -------------------------------------------------------------------------
//  Small helpers
// -------------------------------------------------------------------------

/**
 * Flatten a value to one safe line.
 *
 * Line breaks are removed because an email header that contains one lets an
 * attacker add headers of their own, which is how a contact form becomes a
 * spam relay. This is the single most important line in the file.
 */
function one_line($value): string
{
    $value = is_string($value) ? $value : '';
    $value = str_replace(["\r", "\n", "%0a", "%0d", "%0A", "%0D"], ' ', $value);
    return trim(preg_replace('/\s+/u', ' ', $value));
}

/** Tidy a multi-line value: keep the line breaks, drop the carriage returns. */
function many_lines($value): string
{
    $value = is_string($value) ? $value : '';
    return trim(str_replace("\r\n", "\n", str_replace("\r", "\n", $value)));
}

/** Cut a string to a length without splitting a multi-byte character. */
function clip(string $value, int $max): string
{
    return function_exists('mb_substr') ? mb_substr($value, 0, $max, 'UTF-8') : substr($value, 0, $max);
}

/** Length in characters, not bytes, so accented names are not punished. */
function width(string $value): int
{
    return function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
}

/** Encode a header value that may contain non-English characters. */
function header_text(string $value): string
{
    return preg_match('/[^\x20-\x7E]/', $value)
        ? '=?UTF-8?B?' . base64_encode($value) . '?='
        : $value;
}

/**
 * The name part of a From or Reply-To header, made safe.
 *
 * A name is under the visitor's control, so it can contain a colon, a comma,
 * an @ or a quote mark, all of which mean something in an address header. It
 * is encoded or quoted so it can only ever be read as a name.
 */
function display_name(string $value): string
{
    if ($value === '') {
        return '';
    }
    if (preg_match('/[^\x20-\x7E]/', $value)) {
        return '=?UTF-8?B?' . base64_encode($value) . '?=';
    }
    if (preg_match('/[():;<>@,.\[\]\\"]/', $value)) {
        return '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"';
    }
    return $value;
}

/**
 * Swap the {tokens} in a piece of wording from fillup.php for real values.
 * Anything not listed is left exactly as written, so a typo shows up as
 * itself rather than vanishing.
 */
function fill(string $text, array $with): string
{
    foreach ($with as $key => $value) {
        $text = str_replace('{' . $key . '}', $value, $text);
    }
    return $text;
}

/** The visitor's address, as well as it can be known behind a proxy. */
function visitor_ip(): string
{
    foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $key) {
        if (!empty($_SERVER[$key])) {
            $ip = trim(explode(',', (string) $_SERVER[$key])[0]);
            if (filter_var($ip, FILTER_VALIDATE_IP)) {
                return $ip;
            }
        }
    }
    return '0.0.0.0';
}

/** Make the storage folder, and keep it off the web. */
function storage_ready(string $dir): bool
{
    if (!is_dir($dir) && !@mkdir($dir, 0750, true) && !is_dir($dir)) {
        return false;
    }
    if (!is_file($dir . '/.htaccess')) {
        // Both spellings, so it works on Apache 2.2 and 2.4 alike.
        @file_put_contents(
            $dir . '/.htaccess',
            "<IfModule mod_authz_core.c>\n  Require all denied\n</IfModule>\n"
            . "<IfModule !mod_authz_core.c>\n  Order allow,deny\n  Deny from all\n</IfModule>\n"
        );
    }
    if (!is_file($dir . '/index.html')) {
        @file_put_contents($dir . '/index.html', '');
    }
    return is_writable($dir);
}

/** Append one row to a CSV, writing the header line the first time. */
function csv_append(string $file, array $head, array $row): bool
{
    $fresh = !is_file($file);
    $fh = @fopen($file, 'ab');
    if (!$fh) {
        return false;
    }
    if (flock($fh, LOCK_EX)) {
        if ($fresh) {
            fwrite($fh, "\xEF\xBB\xBF");   // so Excel opens UTF-8 correctly
            fputcsv($fh, $head);
        }
        fputcsv($fh, $row);
        fflush($fh);
        flock($fh, LOCK_UN);
    }
    fclose($fh);
    @chmod($file, 0640);
    return true;
}

/**
 * Allow only so many submissions an hour from one address.
 *
 * Addresses are stored hashed, so the file holds no personal data, and old
 * entries are dropped every time it is read.
 */
function within_rate_limit(string $dir, string $ip, int $max): bool
{
    if ($max <= 0) {
        return true;
    }
    $file = $dir . '/rate.json';
    $key  = substr(hash('sha256', $ip . '|orchard'), 0, 16);
    $now  = time();
    $ok   = true;

    $fh = @fopen($file, 'c+b');
    if (!$fh) {
        return true;              // cannot track it, so do not block anyone
    }
    if (flock($fh, LOCK_EX)) {
        $raw  = stream_get_contents($fh);
        $data = json_decode((string) $raw, true);
        $data = is_array($data) ? $data : [];

        foreach ($data as $k => $stamps) {
            $stamps = array_values(array_filter((array) $stamps, static fn($t) => $now - (int) $t < 3600));
            if ($stamps) {
                $data[$k] = $stamps;
            } else {
                unset($data[$k]);
            }
        }

        $mine = $data[$key] ?? [];
        if (count($mine) >= $max) {
            $ok = false;
        } else {
            $mine[] = $now;
            $data[$key] = $mine;
        }

        ftruncate($fh, 0);
        rewind($fh);
        fwrite($fh, json_encode($data));
        fflush($fh);
        flock($fh, LOCK_UN);
    }
    fclose($fh);
    @chmod($file, 0640);
    return $ok;
}

// -------------------------------------------------------------------------
//  Reading the form
// -------------------------------------------------------------------------

$in = [
    'name'    => one_line($_POST['name']    ?? ''),
    'email'   => one_line($_POST['email']   ?? ''),
    'phone'   => one_line($_POST['phone']   ?? ''),
    'box'     => one_line($_POST['box']     ?? ''),
    'address' => one_line($_POST['address'] ?? ''),
    'message' => many_lines($_POST['message'] ?? ''),
];
$trap    = one_line($_POST['website'] ?? '');
$started = (int) ($_POST['ts'] ?? 0);
$ip      = visitor_ip();
$ready   = storage_ready($LOG_DIR);

// -------------------------------------------------------------------------
//  Spam checks
//
//  A caught submission is answered with the ordinary thank-you. Telling a bot
//  it was caught only teaches whoever wrote it what to change. The details go
//  to storage/trapped-*.csv instead, so a genuine person caught by mistake is
//  never lost, and you can see whether the traps are behaving.
// -------------------------------------------------------------------------

$caught = '';
if ($trap !== '') {
    $caught = 'honeypot';
} elseif ($started > 0 && (microtime(true) * 1000 - $started) < 2500) {
    $caught = 'too fast';
}

if ($caught !== '') {
    if ($ready) {
        csv_append(
            $LOG_DIR . '/trapped-' . date('Y-m') . '.csv',
            ['when', 'why', 'ip', 'name', 'email', 'phone', 'box', 'address', 'message', 'trap field'],
            [date('c'), $caught, $ip, $in['name'], $in['email'], $in['phone'], $in['box'], $in['address'], $in['message'], $trap]
        );
    }
    finish(true, $MSG_THANKS, [], $wants_json, $REDIRECT_AFTER);
}

if ($ready && !within_rate_limit($LOG_DIR, $ip, (int) $MAX_PER_HOUR)) {
    finish(false, $MSG_TOO_MANY, [], $wants_json, '');
}

// -------------------------------------------------------------------------
//  Checking the fields
// -------------------------------------------------------------------------

$errors = [];

if ($in['name'] === '') {
    $errors['name'] = 'Please tell us your name.';
} elseif (width($in['name']) < 2) {
    $errors['name'] = 'That name looks too short.';
} elseif (width($in['name']) > 80) {
    $errors['name'] = 'Please keep the name under 80 characters.';
}

if ($in['email'] === '') {
    $errors['email'] = 'Please give us an email address, so we can reply.';
} elseif (!filter_var($in['email'], FILTER_VALIDATE_EMAIL) || width($in['email']) > 190) {
    $errors['email'] = 'That email address does not look right. Please check it.';
}

if ($in['phone'] === '') {
    $errors['phone'] = 'Please give us a phone number, so we can reach you about the delivery.';
} elseif (!preg_match('/^[0-9 ()+.\-]{6,25}$/', $in['phone'])) {
    $errors['phone'] = 'Use digits, spaces and + only, for example +1 555 123 4567.';
}

if ($in['address'] === '') {
    $errors['address'] = 'Please give us the delivery address.';
} elseif (width($in['address']) > 200) {
    $errors['address'] = 'Please keep the address under 200 characters.';
}

if (width($in['box']) > 80) {
    $errors['box'] = 'Please choose one of the boxes.';
}

if (width($in['message']) > 2000) {
    $errors['message'] = 'Please keep this under 2000 characters.';
}

if ($errors) {
    finish(false, 'Please check the highlighted fields.', $errors, $wants_json, '');
}

$in['name']    = clip($in['name'], 80);
$in['address'] = clip($in['address'], 200);
$in['box']     = $in['box'] === '' ? 'Not stated' : clip($in['box'], 80);
$in['message'] = clip($in['message'], 2000);

// -------------------------------------------------------------------------
//  Save it first, send it second
//
//  The CSV is written before any email is attempted. An email can fail for a
//  dozen reasons outside your control; a written file cannot. This way the
//  worst an email problem costs you is a notification, never the order.
// -------------------------------------------------------------------------

$saved = false;
if ($ready) {
    $saved = csv_append(
        $LOG_DIR . '/orders-' . date('Y-m') . '.csv',
        ['when', 'name', 'email', 'phone', 'box', 'address', 'message', 'ip'],
        [date('c'), $in['name'], $in['email'], $in['phone'], $in['box'], $in['address'], $in['message'], $ip]
    );
}

// -------------------------------------------------------------------------
//  Building the email
// -------------------------------------------------------------------------

$host = one_line($_SERVER['HTTP_HOST'] ?? 'localhost');
$host = preg_replace('/[^A-Za-z0-9.\-:]/', '', $host);
$host = preg_replace('/:\d+$/', '', $host);
$host = preg_replace('/^www\./i', '', $host);

$from = $FROM !== '' ? $FROM : 'noreply@' . ($host !== '' ? $host : 'localhost');
if (!filter_var($from, FILTER_VALIDATE_EMAIL)) {
    $from = 'noreply@localhost';
}

// Everything the wording in fillup.php is allowed to refer to.
$tokens = [
    'name'    => $in['name'],
    'email'   => $in['email'],
    'phone'   => $in['phone'] !== '' ? $in['phone'] : 'not given',
    'box'     => $in['box'],
    'address' => $in['address'],
    'message' => $in['message'] !== '' ? $in['message'] : '(none)',
    'site'    => $host !== '' ? $host : 'this site',
    'date'    => date('D j F Y, H:i'),
];

$subject = fill($SUBJECT_TO_YOU, $tokens);

$lines = [
    fill($INTRO_TO_YOU, $tokens),
    '',
    'Name     : ' . $tokens['name'],
    'Email    : ' . $tokens['email'],
    'Phone    : ' . $tokens['phone'],
    'Box      : ' . $tokens['box'],
    'Address  : ' . $tokens['address'],
    '',
    'Notes    :',
    $tokens['message'],
    '',
    str_repeat('-', 58),
    'Sent ' . $tokens['date'] . ' from ' . $tokens['site'],
    fill($FOOT_TO_YOU, $tokens),
];
$body = implode("\n", $lines);

$sent = send_mail(
    $SMTP,
    $from,
    $TO_NAME,
    $TO,
    $subject,
    $body,
    $in['email'],
    $in['name'],
    $why
);

// The customer's copy. It is not worth failing the order over, so its result
// is recorded but never shown to them.
if ($SEND_COPY) {
    $copy = fill($MESSAGE_TO_CUSTOMER, $tokens);
    send_mail($SMTP, $from, $TO_NAME, $in['email'], fill($SUBJECT_TO_CUSTOMER, $tokens), $copy, $TO, $TO_NAME, $ignore);
}

// -------------------------------------------------------------------------
//  Telling the visitor what happened
// -------------------------------------------------------------------------

if (!$sent && !$saved) {
    // Nothing worked. Never tell somebody their order arrived when no copy of
    // it exists anywhere. Give them another way to reach you instead.
    finish(false, fill($MSG_FAILED, ['email' => $TO]), [], $wants_json, '');
}

if (!$sent) {
    @error_log('[orchard] order saved but email failed: ' . (string) $why);
    if ($ready) {
        @file_put_contents(
            $LOG_DIR . '/mail-failures.log',
            date('c') . "\t" . $in['email'] . "\t" . (string) $why . "\n",
            FILE_APPEND | LOCK_EX
        );
    }
}

finish(true, $MSG_THANKS, [], $wants_json, $REDIRECT_AFTER);

// =========================================================================
//  Sending
// =========================================================================

/**
 * Send one message, through SMTP if it is switched on, otherwise mail().
 * Returns true when the server accepted it. $why is filled in when it did not.
 */
function send_mail(array $smtp, string $from, string $fromName, string $to, string $subject, string $body, string $replyTo, string $replyName, &$why = null): bool
{
    $why = '';
    $body = str_replace("\r\n", "\n", $body);
    $body = str_replace("\n", "\r\n", $body);

    // A Message-ID and a Date. Some spam filters mark down mail that has
    // neither, and not every host adds them for you.
    $domain = substr(strrchr($from, '@') ?: '@localhost', 1);
    try {
        $unique = bin2hex(random_bytes(12));
    } catch (Throwable $e) {
        $unique = md5(uniqid('', true));
    }

    $headers = [
        'Date'                      => date('r'),
        'Message-ID'                => '<' . $unique . '@' . $domain . '>',
        'From'                      => trim(display_name($fromName) . ' <' . $from . '>'),
        'Reply-To'                  => trim(display_name($replyName) . ' <' . $replyTo . '>'),
        'MIME-Version'              => '1.0',
        'Content-Type'              => 'text/plain; charset=UTF-8',
        'Content-Transfer-Encoding' => 'base64',
        'X-Mailer'                  => 'Orchard form',
    ];
    $encoded = chunk_split(base64_encode($body), 76, "\r\n");

    if (!empty($smtp['enabled'])) {
        return smtp_deliver($smtp, $from, $to, header_text($subject), $headers, $encoded, $why);
    }

    $flat = [];
    foreach ($headers as $k => $v) {
        $flat[] = $k . ': ' . $v;
    }
    $ok = @mail($to, header_text($subject), $encoded, implode("\r\n", $flat), '-f' . $from);
    if (!$ok) {
        $why = 'PHP mail() refused the message. The host may have mail disabled, or the From address may not be on this domain.';
    }
    return (bool) $ok;
}

/**
 * A small SMTP client. Enough for one plain-text message, nothing more.
 *
 * If you would rather use a library, PHPMailer drops in here without changing
 * anything else in this file.
 */
function smtp_deliver(array $c, string $from, string $to, string $subject, array $headers, string $encodedBody, &$why): bool
{
    $secure = strtolower((string) ($c['secure'] ?? ''));
    $host   = (string) ($c['host'] ?? '');
    $port   = (int) ($c['port'] ?? 587);
    $target = ($secure === 'ssl' ? 'ssl://' : '') . $host . ':' . $port;

    $context = stream_context_create(['ssl' => ['SNI_enabled' => true, 'peer_name' => $host]]);
    $fh = @stream_socket_client($target, $errNo, $errStr, 15, STREAM_CLIENT_CONNECT, $context);
    if (!$fh) {
        $why = 'Could not reach the SMTP server: ' . $errStr;
        return false;
    }
    stream_set_timeout($fh, 15);

    $read = static function () use ($fh) {
        $out = '';
        while (($line = fgets($fh, 1024)) !== false) {
            $out .= $line;
            if (strlen($line) < 4 || $line[3] !== '-') {
                break;
            }
        }
        return $out;
    };
    $say = static function (string $line) use ($fh, $read) {
        fwrite($fh, $line . "\r\n");
        return $read();
    };
    $code = static fn(string $reply): int => (int) substr(trim($reply), 0, 3);

    $ehlo = 'orchard.' . preg_replace('/[^a-z0-9.\-]/i', '', (string) ($_SERVER['HTTP_HOST'] ?? 'localhost'));

    try {
        if ($code($read()) !== 220) {
            throw new RuntimeException('the server did not greet us');
        }

        $reply = $say('EHLO ' . $ehlo);
        if ($code($reply) !== 250) {
            $reply = $say('HELO ' . $ehlo);
            if ($code($reply) !== 250) {
                throw new RuntimeException('EHLO was refused');
            }
        }

        if ($secure === 'tls') {
            if ($code($say('STARTTLS')) !== 220) {
                throw new RuntimeException('the server refused STARTTLS');
            }
            $crypto = STREAM_CRYPTO_METHOD_TLS_CLIENT;
            if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
                $crypto |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
            }
            if (!@stream_socket_enable_crypto($fh, true, $crypto)) {
                throw new RuntimeException('the TLS handshake failed');
            }
            $reply = $say('EHLO ' . $ehlo);
            if ($code($reply) !== 250) {
                throw new RuntimeException('EHLO after STARTTLS was refused');
            }
        }

        if (($c['user'] ?? '') !== '') {
            if ($code($say('AUTH LOGIN')) !== 334) {
                throw new RuntimeException('the server does not accept AUTH LOGIN');
            }
            if ($code($say(base64_encode((string) $c['user']))) !== 334) {
                throw new RuntimeException('the username was refused');
            }
            if ($code($say(base64_encode((string) ($c['pass'] ?? '')))) !== 235) {
                throw new RuntimeException('the username and password were refused');
            }
        }

        if ($code($say('MAIL FROM:<' . $from . '>')) !== 250) {
            throw new RuntimeException('the From address was refused');
        }
        $rcpt = $code($say('RCPT TO:<' . $to . '>'));
        if ($rcpt !== 250 && $rcpt !== 251) {
            throw new RuntimeException('the To address was refused');
        }
        if ($code($say('DATA')) !== 354) {
            throw new RuntimeException('the server would not take the message');
        }

        $wire = 'To: <' . $to . ">\r\n"
              . 'Subject: ' . $subject . "\r\n";
        foreach ($headers as $k => $v) {
            $wire .= $k . ': ' . $v . "\r\n";
        }
        $wire .= "\r\n" . $encodedBody;

        // A line that is only a full stop would end the message early.
        $wire = preg_replace('/^\./m', '..', $wire);
        fwrite($fh, rtrim($wire, "\r\n") . "\r\n.\r\n");

        if ($code($read()) !== 250) {
            throw new RuntimeException('the server did not accept the message');
        }

        @fwrite($fh, "QUIT\r\n");
        fclose($fh);
        return true;
    } catch (Throwable $e) {
        $why = 'SMTP: ' . $e->getMessage() . '.';
        @fclose($fh);
        return false;
    }
}

// =========================================================================
//  Answering
// =========================================================================

/** Send the answer and stop. JSON for the page's script, HTML for a browser. */
function finish(bool $ok, string $message, array $errors, bool $json, string $redirect): void
{
    if ($json) {
        header('Content-Type: application/json; charset=UTF-8');
        header('Cache-Control: no-store');
        http_response_code($ok ? 200 : 422);
        echo json_encode(['ok' => $ok, 'message' => $message, 'errors' => (object) $errors]);
        exit;
    }

    if ($ok && $redirect !== '') {
        header('Location: ' . $redirect, true, 303);
        exit;
    }

    $safe = static fn(string $s): string => htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
    header('Content-Type: text/html; charset=UTF-8');
    http_response_code($ok ? 200 : 422);
    ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title><?= $ok ? 'Thank you' : 'Please check the form' ?></title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<main>
  <section class="band">
    <div class="wrap narrow">
      <p class="eyebrow"><?= $ok ? 'Order received' : 'Not sent yet' ?></p>
      <h1><?= $ok ? 'Thank you' : 'Please check the form' ?></h1>
      <p class="lede"><?= $safe($message) ?></p>
      <?php if ($errors): ?>
        <ul class="list">
          <?php foreach ($errors as $line): ?>
            <li><?= $safe($line) ?></li>
          <?php endforeach; ?>
        </ul>
      <?php endif; ?>
      <p><a class="btn" href="index.html<?= $ok ? '' : '#order' ?>"><?= $ok ? 'Back to the site' : 'Back to the form' ?></a></p>
    </div>
  </section>
</main>
</body>
</html>
    <?php
    exit;
}
