• File: front.php
  • Full Path: /home/daemondevin/public_html/portableapps/lib/vendor/sodium_compat/namespaced/front.php
  • Date Modified: 10/23/2026 3:13 PM
  • File size: 10.22 KB
  • MIME-type: text/x-php
  • Charset: utf-8
<?php
defined('TS_ROOT') or exit;
// Front controller: routes every public page.


require __DIR__ . '/app.php';

function ts_friendly_error(string $title, string $message, int $code = 500): void
{
    if (!headers_sent()) { http_response_code($code); header('Content-Type: text/html; charset=utf-8'); }
    echo '<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>' . htmlspecialchars($title) . '</title>'
        . '<body style="font:16px/1.6 system-ui,sans-serif;max-width:38rem;margin:4rem auto;padding:0 1rem;color:#1f2937">'
        . '<h1 style="font-size:1.5rem">' . htmlspecialchars($title) . '</h1><p>' . $message . '</p>';
}

$cfg = null;
$store = null;
try {
    $cfg = ts_boot(TS_ROOT);
    if (empty($cfg['debug'])) {
        ini_set('display_errors', '0');
    }
    set_exception_handler(function ($e) use (&$cfg) {
        ts_log(get_class($e) . ': ' . $e->getMessage() . ' in ' . basename($e->getFile()) . ':' . $e->getLine());
        ts_friendly_error('Something went wrong', htmlspecialchars(!empty($cfg['debug']) ? $e->getMessage() : 'The blog hit a problem and saved the details. If you are the owner, sign in and open <strong>Settings</strong> to see what happened.'));
    });
    register_shutdown_function(function () {
        $err = error_get_last();
        if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
            ts_log('Fatal: ' . $err['message'] . ' in ' . basename($err['file']) . ':' . $err['line']);
            if (!headers_sent()) ts_friendly_error('Something went wrong', 'The blog hit a problem and saved the details. If you are the owner, sign in and open <strong>Settings</strong> to see what happened.');
        }
    });
    $store = TSStore::open($cfg);
    $GLOBALS['ts_store'] = $store;
} catch (Throwable $e) {
    @ts_log($e->getMessage());
    ts_friendly_error('The blog could not start', htmlspecialchars($e->getMessage()));
    exit;
}

// Work out which page was asked for, whether it came as /my-post or index.php?r=my-post
$viaQuery = isset($_GET['r']) && is_string($_GET['r']);
$uri = (string) parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
if ($viaQuery) {
    $path = '/' . trim($_GET['r'], '/');
} else {
    $path = rawurldecode($uri);
    if ($cfg['base'] !== '' && strpos($path, $cfg['base']) === 0) $path = substr($path, strlen($cfg['base']));
    if (preg_match('#^/index\.php$#', $path)) $path = '/';
    $path = '/' . trim($path, '/');
    if ($path !== '/' && substr($uri, -1) === '/' && strpos($path, '/admin') !== 0) {
        ts_redirect(ts_url(ltrim($path, '/')), 301);
    }
    // A clean address reached this file, so the server sends unknown paths here: pretty links work
    if ($path !== '/' && ($cfg['state']['pretty'] ?? null) !== true) {
        ts_state_set(['pretty' => true]);
    }
}
$seg = $path === '/' ? [] : explode('/', trim($path, '/'));
$GLOBALS['ts_path'] = $path;
$first = $seg[0] ?? '';

// An update is being installed: a short pause beats a half updated page
if (ts_update_locked()) {
    header('Retry-After: 10');
    if (ts_wants_json()) ts_json(['ok' => false, 'message' => 'An update is running right now. Please wait a minute and try again.'], 503);
    ts_friendly_error('Updating…', 'This site is being updated and will be back in a few seconds. Please reload in a moment.', 503);
    exit;
}

if ($path === '/ts-probe') {
    header('Cache-Control: no-store');
    header('Content-Type: text/plain');
    echo 'tsblog-probe';
    exit;
}

// Admin
if ($first === 'admin') {
    require __DIR__ . '/admin.php';
    ts_headers(true);
    ts_admin($store, array_slice($seg, 1));
    exit;
}

ts_headers();
$admin = ts_is_admin();
$GLOBALS['ts_admin'] = $admin;
$GLOBALS['ts_nav_pages'] = $store->published('page');

$perPage = max(1, (int) $cfg['posts_per_page']);
$pageNum = static fn($v) => max(1, (int) $v);

function ts_list_page(array $all, int $n, int $per, string $base): array
{
    $total = max(1, (int) ceil(count($all) / $per));
    if ($n > $total) ts_not_found();
    $b = trim($base, '/');
    $first = $b === '' ? '' : $b;
    return [
        'posts' => array_slice($all, ($n - 1) * $per, $per),
        'page' => $n, 'pages' => $total, 'base' => $b,
        'newer' => $n > 1 ? ($n === 2 ? ts_url($first) : ts_url($b . '/page/' . ($n - 1))) : null,
        'older' => $n < $total ? ts_url(($b === '' ? '' : $b . '/') . 'page/' . ($n + 1)) : null,
    ];
}

function ts_not_found(): void
{
    ts_send(ts_page('404', [], ['title' => 'Not found', 'noindex' => true]), 'text/html; charset=utf-8', 404, false);
    exit;
}

// Home and pagination
if ($seg === [] || ($first === 'page' && count($seg) === 2 && ctype_digit($seg[1]))) {
    $n = $seg === [] ? 1 : $pageNum($seg[1]);
    $v = ts_list_page($store->published(), $n, $perPage, '');
    ts_send(ts_page('home', $v + ['heading' => null], [
        'title' => $cfg['title'] . ($cfg['tagline'] ? ' · ' . $cfg['tagline'] : ''),
        'canonical' => $n > 1 ? ts_abs('page/' . $v['page']) : ts_abs(),
    ]));
    exit;
}

// Tags
if ($first === 'tags' && count($seg) === 1) {
    ts_send(ts_page('tags', ['tags' => $store->tags()], ['title' => 'Tags · ' . $cfg['title'], 'canonical' => ts_abs('tags')]));
    exit;
}
if ($first === 'tag' && isset($seg[1])) {
    $tag = $seg[1];
    $n = (count($seg) === 4 && $seg[2] === 'page') ? $pageNum($seg[3]) : 1;
    $all = $store->byTag($tag);
    if (!$all) ts_not_found();
    $name = $tag;
    foreach ($all[0]['tags'] as $t) if (mb_strtolower($t) === mb_strtolower($tag)) $name = $t;
    $route = 'tag/' . mb_strtolower($tag);
    $v = ts_list_page($all, $n, $perPage, $route);
    ts_send(ts_page('home', $v + ['heading' => 'Tagged “' . $name . '”'], [
        'title' => $name . ' · ' . $cfg['title'], 'canonical' => ts_abs($route),
        'description' => 'Posts tagged ' . $name . ' on ' . $cfg['title'] . '.',
    ]));
    exit;
}

// Search
if ($first === 'search' && count($seg) === 1) {
    $q = trim((string) ($_GET['q'] ?? ''));
    $found = $q !== '' ? $store->search($q) : [];
    ts_send(ts_page('home', [
        'posts' => array_slice($found, 0, 50), 'page' => 1, 'pages' => 1, 'newer' => null, 'older' => null,
        'heading' => $q === '' ? 'Search' : count($found) . ' result' . (count($found) === 1 ? '' : 's') . ' for “' . $q . '”',
        'search' => $q,
    ], ['title' => 'Search · ' . $cfg['title'], 'noindex' => true]), 'text/html; charset=utf-8', 200, false);
    exit;
}

// Link click counting (a tiny message from assets/track.js)
if ($first === 'track' && count($seg) === 1) {
    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') TSAnalytics::click($_POST);
    http_response_code(204);
    header('Cache-Control: no-store');
    exit;
}

// Comments (form posts only)
if ($first === 'comment' && count($seg) === 1) {
    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') ts_redirect(ts_url());
    $slug = (string) ($_POST['post'] ?? '');
    $wantsJson = ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'fetch';
    $res = TSStore::validSlug($slug) ? TSComments::submit($slug, $_POST, $store) : ['code' => 'post', 'comment' => null];
    $msgs = TSComments::messages();
    $good = in_array($res['code'], ['ok', 'pending'], true);
    if ($wantsJson) {
        ts_json(['ok' => $good, 'code' => $res['code'], 'message' => $msgs[$res['code']] ?? $msgs['save'],
            'count' => $good ? TSComments::count($slug) : null], $good ? 200 : 422);
        exit;
    }
    ts_redirect(ts_url($slug, ['c' => $res['code']]) . '#comments');
}

// Feed, sitemap, robots
if ($path === '/feed.xml') {
    $posts = array_slice($store->published(), 0, (int) $cfg['feed_items']);
    foreach ($posts as &$p) $p['html'] = ts_render($store->get($p['slug'])['body'] ?? '', 'plain');
    unset($p);
    ts_send(ts_feed($posts), 'application/rss+xml; charset=utf-8');
    exit;
}
if ($path === '/sitemap.xml') {
    ts_send(ts_sitemap($store->published(), $store->published('page'), $store->tags()), 'application/xml; charset=utf-8');
    exit;
}
if ($path === '/robots.txt') {
    ts_send("User-agent: *\nDisallow: " . $cfg['base'] . "/admin\nDisallow: " . $cfg['base'] . "/search\n\nSitemap: " . ts_abs('sitemap.xml') . "\n", 'text/plain; charset=utf-8');
    exit;
}

// Single post or page
if (count($seg) === 1 && TSStore::validSlug($first)) {
    $post = $store->get($first);
    $live = $post && $post['status'] === 'published' && strtotime($post['date']) <= time();
    if (!$post || (!$live && !$admin)) ts_not_found();

    $post['html'] = ts_render($post['body']);
    $post['words'] = TSMarkdown::wordCount($post['body']);
    $post['minutes'] = max(1, (int) ceil($post['words'] / 220));
    $desc = $post['description'] !== '' ? $post['description'] : TSMarkdown::excerpt($post['body'], 160);

    $prev = $next = null;
    if ($post['type'] === 'post' && $live) {
        $all = $store->published();
        foreach ($all as $k => $p) {
            if ($p['slug'] === $post['slug']) { $next = $all[$k - 1] ?? null; $prev = $all[$k + 1] ?? null; break; }
        }
    }
    $cover = TSMedia::firstImage($post);
    $img = $cover !== '' ? ts_absolute_path($cover) : '';
    $ld = [
        '@context' => 'https://schema.org', '@type' => $post['type'] === 'post' ? 'BlogPosting' : 'WebPage',
        'headline' => $post['title'], 'description' => $desc, 'url' => ts_abs($post['slug']),
        'datePublished' => date('c', strtotime($post['date'])), 'dateModified' => date('c', strtotime($post['updated'] ?: $post['date'])),
        'author' => ['@type' => 'Person', 'name' => $cfg['author'] ?: $cfg['title']],
        'publisher' => ['@type' => 'Organization', 'name' => $cfg['title']],
    ];
    if ($img) $ld['image'] = $img;
    if (!empty($cfg['scroll_shadow']) || strpos($post['html'], 'data-plugin-alert') !== false) ts_headers(false, true);
    ts_send(ts_page('post', ['post' => $post, 'prev' => $prev, 'next' => $next, 'live' => $live], [
        'title' => $post['title'] . ' · ' . $cfg['title'], 'description' => $desc, 'canonical' => ts_abs($post['slug']),
        'image' => $img, 'type' => $post['type'] === 'post' ? 'article' : 'website', 'noindex' => !$live,
        'published' => $post['date'], 'modified' => $post['updated'], 'tags' => $post['tags'], 'jsonld' => $ld,
    ]), 'text/html; charset=utf-8', 200, !$admin);
    exit;
}

ts_not_found();