• File: settings.php
  • Full Path: /home/daemondevin/public_html/portableapps/data/backups/20261023081324-1.2.0-to-1.3.0/settings.php
  • Date Modified: 10/26/2026 5:36 AM
  • File size: 10.82 KB
  • MIME-type: text/x-php
  • Charset: utf-8
<?php
defined('TS_ROOT') or exit;
// Settings, state and private data files.
//
// Three layers, later ones win:
//   1. built in defaults
//   2. data/settings.php   written by the Settings screen in the admin
//   3. config.php          optional, for advanced users. Anything set here is locked in the admin
//
// Private files are stored as PHP files that begin with a guard line. If a web server hands one out
// instead of running it, the visitor gets an empty 404 page, never the contents.

const TS_GUARD = "<?php http_response_code(404); exit; ?>\n";

function ts_defaults(): array
{
    return [
        'title' => 'My Blog', 'tagline' => '', 'description' => '', 'author' => '', 'language' => 'en',
        'timezone' => 'UTC', 'url' => '', 'store' => 'files', 'posts_per_page' => 8, 'date_format' => 'M j, Y',
        'feed_items' => 20, 'footer' => '', 'allow_html' => false, 'theme' => 'default',
        'reading_progress' => true,  // progress bar at the top of post pages
        'scroll_shadow' => true,     // "keep reading" fade on post pages (loads jQuery and one plugin on post pages)
        'analytics' => 'off',        // off | on: page views and link clicks, shown in the admin
        'analytics_ip' => 'full',    // full | masked | none: what is kept of a visitor's IP address
        'analytics_days' => 90,      // how long records are kept
        'ip_lookup_url' => '',       // advanced, config.php only: IP lookup service address ending in ?ip=. Empty uses the built in one
        'comments' => 'off',         // off | moderate | open | closed
        'pretty_urls' => 'auto',     // auto | on | off
        'post_files' => 'auto',      // auto | plain | protected
        'admin_password_hash' => '', 'debug' => false,
        'update_url' => '',            // where to look for new versions (a latest.json address). Empty uses the built in one
        'update_check' => true,        // look for updates automatically when you open the admin
        'update_public_key' => '',     // advanced, config.php only: overrides the built in signing key
        'update_allow_insecure' => false, // advanced, config.php only: allow http://localhost for testing
    ];
}

// Private data files

function ts_data_dir(): string { return $GLOBALS['ts_data_dir'] ?? (TS_ROOT . '/data'); }

function ts_strip_guard(string $raw): string
{
    return strpos($raw, '<?php') === 0 ? (string) substr($raw, (int) strpos($raw, "\n") + 1) : $raw;
}

function ts_data_read(string $name): array
{
    $f = ts_data_dir() . '/' . $name . '.php';
    if (!is_file($f)) return [];
    $j = json_decode(ts_strip_guard((string) @file_get_contents($f)), true);
    return is_array($j) ? $j : [];
}

function ts_data_write(string $name, array $data): bool
{
    $f = ts_data_dir() . '/' . $name . '.php';
    $tmp = $f . '.' . bin2hex(random_bytes(3)) . '.tmp';
    if (@file_put_contents($tmp, TS_GUARD . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX) === false) return false;
    @chmod($tmp, 0600);
    if (!@rename($tmp, $f)) { @unlink($tmp); return false; }
    return true;
}

function ts_state_set(array $kv): bool
{
    $s = $kv + ts_data_read('state');
    $GLOBALS['ts_cfg']['state'] = $s;
    return ts_data_write('state', $s);
}

function ts_log(string $msg): void
{
    $f = ts_data_dir() . '/log.php';
    $lines = is_file($f) ? explode("\n", ts_strip_guard((string) @file_get_contents($f))) : [];
    $lines = array_slice(array_filter($lines, 'strlen'), -99);
    $lines[] = date('Y-m-d H:i:s') . ' ' . str_replace("\n", ' ', $msg);
    @file_put_contents($f, TS_GUARD . implode("\n", $lines) . "\n", LOCK_EX);
}

function ts_log_lines(int $n = 5): array
{
    $f = ts_data_dir() . '/log.php';
    if (!is_file($f)) return [];
    return array_slice(array_filter(explode("\n", ts_strip_guard((string) @file_get_contents($f))), 'strlen'), -$n);
}

// Folders: created on demand, with files that keep them private and unlisted on any server

function ts_prepare_dirs(string $root, string $data, string $content): void
{
    $deny = "<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\nOrder deny,allow\nDeny from all\n</IfModule>\n";
    $noexec = "<FilesMatch \"\\.(php|phtml|phar|php[0-9])\$\">\n" . $deny . "</FilesMatch>\n";
    foreach ([[$data, $deny], [$content, $deny], [$root . '/uploads', $noexec]] as [$dir, $rules]) {
        if (!is_dir($dir)) @mkdir($dir, 0775, true);
        if (!is_dir($dir)) continue;
        if (!is_file($dir . '/index.html')) @file_put_contents($dir . '/index.html', '');
        if (!is_file($dir . '/.htaccess')) @file_put_contents($dir . '/.htaccess', $rules);
    }
    if (!is_dir($content . '/posts')) @mkdir($content . '/posts', 0775, true);
    if (is_dir($content) && !is_file($content . '/ts-probe.txt')) @file_put_contents($content . '/ts-probe.txt', 'tsblog-private-probe');
}

function ts_effective_guard(array $cfg): bool
{
    $m = $cfg['post_files'] ?? 'auto';
    return $m === 'protected' || ($m === 'auto' && (($cfg['state']['exposed'] ?? null) !== false));
}

function ts_boot(string $root): array
{
    require_once __DIR__ . '/compat.php';
    $file = [];
    if (is_file($root . '/config.php')) {
        $f = (static function ($__file) { return require $__file; })($root . '/config.php');
        if (is_array($f)) $file = $f;
    }
    $data = rtrim(str_replace('\\', '/', (string) ($file['data_dir'] ?? $root . '/data')), '/');
    $content = rtrim(str_replace('\\', '/', (string) ($file['content_dir'] ?? $root . '/content')), '/');
    $GLOBALS['ts_data_dir'] = $data;
    ts_prepare_dirs($root, $data, $content);

    $cfg = $file + ts_data_read('settings') + ts_defaults();
    $cfg['locked'] = array_keys($file);
    $cfg['state'] = ts_data_read('state');
    mb_internal_encoding('UTF-8');

    if (!in_array($cfg['timezone'], DateTimeZone::listIdentifiers(), true)) $cfg['timezone'] = 'UTC';
    date_default_timezone_set($cfg['timezone']);

    $theme = basename((string) $cfg['theme']);
    if (!is_file($root . '/theme/' . $theme . '/layout.php')) $theme = 'default';
    $cfg['theme'] = $theme;
    $cfg['paths'] = ['root' => $root, 'data' => $data, 'content' => $content, 'uploads' => $root . '/uploads', 'theme' => $root . '/theme/' . $theme];

    $base = rtrim(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? '/')), '/');
    $cfg['base'] = $base;
    if ($cfg['url'] === '') {
        $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
        $cfg['url'] = ($https ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? 'localhost') . $base;
    }
    $cfg['url'] = rtrim($cfg['url'], '/');

    $mode = $cfg['pretty_urls'];
    $cfg['pretty'] = $mode === 'on' || $mode === true || (($mode === 'auto') && (($cfg['state']['pretty'] ?? null) === true));
    $cfg['guard'] = ts_effective_guard($cfg);

    if ($cfg['store'] === 'sqlite') {
        if (empty($cfg['state']['sqlite_file'])) {
            $name = 'blog-' . bin2hex(random_bytes(8)) . '.db';
            $cfg['state']['sqlite_file'] = $name;
            ts_data_write('state', $cfg['state']);
        }
        $cfg['sqlite_file'] = $cfg['state']['sqlite_file'];
    }
    return $GLOBALS['ts_cfg'] = $cfg;
}

function ts_cfg(string $k, $d = null) { return $GLOBALS['ts_cfg'][$k] ?? $d; }

// The largest upload this server accepts, in bytes (capped at 8 MB by the blog itself)
function ts_upload_limit(): int
{
    $n = static function ($v) {
        $v = trim((string) $v); $u = strtolower(substr($v, -1)); $x = (float) $v;
        return (int) ($u === 'g' ? $x * 1073741824 : ($u === 'm' ? $x * 1048576 : ($u === 'k' ? $x * 1024 : $x)));
    };
    $a = $n(ini_get('upload_max_filesize')) ?: PHP_INT_MAX;
    $b = $n(ini_get('post_max_size')) ?: PHP_INT_MAX;
    return max(262144, min($a, $b, 8 * 1048576));
}

// Plain language health report: list of ['level' => ok|warn|fail, 'label', 'detail']
function ts_health(): array
{
    $p = ts_cfg('paths');
    $out = [];
    $add = static function ($level, $label, $detail) use (&$out) { $out[] = compact('level', 'label', 'detail'); };
    $fixTip = 'In your hosting file manager (or FTP program), right-click the folder, choose Permissions, and set it to 755. If it still fails, try 775, then 777.';

    $add(version_compare(PHP_VERSION, '8.1.0', '>=') ? 'ok' : 'fail', 'PHP version ' . PHP_VERSION,
        version_compare(PHP_VERSION, '8.1.0', '>=') ? 'Good.' : 'This blog needs PHP 8.1 or newer. Your hosting control panel has a "PHP version" setting.');

    foreach ([['data', $p['data'], 'private settings and passwords'], ['content/posts', $p['content'] . '/posts', 'your posts'], ['uploads', $p['uploads'], 'your images']] as [$name, $dir, $what]) {
        $test = $dir . '/.write-test-' . bin2hex(random_bytes(2));
        $ok = is_dir($dir) && @file_put_contents($test, 'x') !== false;
        if ($ok) @unlink($test);
        $add($ok ? 'ok' : 'fail', "The “{$name}” folder",
            $ok ? 'The blog can save here. Good.' : "The blog cannot save {$what} here. {$fixTip}");
    }

    $lim = ts_upload_limit();
    $add($lim >= 2097152 ? 'ok' : 'warn', 'Largest image upload: ' . round($lim / 1048576, 1) . ' MB',
        $lim >= 2097152 ? 'Good.' : 'This is small, but photos are shrunk automatically in your browser before they upload, so it will still work.');

    $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
    $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
    $local = (bool) preg_match('/^(localhost|127\.|\[::1\])/', $host);
    $add($secure || $local ? 'ok' : 'warn', $secure ? 'Secure connection (HTTPS)' : 'Your connection is not secure (HTTP)',
        $secure || $local ? 'Good.' : 'Your password travels unprotected. Most hosts offer a free certificate: look for "SSL" or "Let\'s Encrypt" in your hosting control panel, then visit this page again with https:// at the start.');

    $s = ts_cfg('state', []);
    $add(ts_cfg('pretty') ? 'ok' : 'warn', ts_cfg('pretty') ? 'Pretty links are on' : 'Pretty links are off',
        ts_cfg('pretty') ? 'Addresses look like /my-post.' : 'Addresses look like /index.php?r=my-post. Everything works, they are just longer. Open Settings → Links to try turning pretty links on.');

    $exposed = $s['exposed'] ?? null;
    $add($exposed === false ? 'ok' : ($exposed === true ? 'ok' : 'warn'), 'Private files',
        $exposed === false ? 'Your server keeps private folders private.' : ($exposed === true ? 'Your server shows folder contents to visitors, so posts are stored in a protected format automatically.' : 'Posts are stored in a protected format to be safe.'));

    if (function_exists('ts_update_problems') && ts_update_url() !== '' && ($p = ts_update_problems())) {
        $add('warn', 'Updates', $p[0]);
    }

    $errs = ts_log_lines(3);
    if ($errs) $add('warn', 'Recent errors were recorded', implode(' | ', $errs));
    return $out;
}