Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
portableapps
/
theme
/
portableapps
/
vendor
:
front.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?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();