Fix first-time setup issues. (#68)

Fixes for issues found testing first time setup in the different configurations.

Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/68
Co-authored-by: Greg Sarjeant <greg@subcultureofone.org>
Co-committed-by: Greg Sarjeant <greg@subcultureofone.org>
This commit is contained in:
Greg Sarjeant 2025-08-13 12:02:37 +00:00 committed by greg
parent 801bbebf4f
commit d3a537aa6c
16 changed files with 77 additions and 33 deletions

3
.gitignore vendored
View File

@ -14,9 +14,10 @@ storage/logs
# Testing stuff # Testing stuff
/docker-compose.yml /docker-compose.yml
scratch scratch
storage.bak
# Build artifacts # Build artifacts
tkr.tgz tkr.tgz
# Test logs # Test logs
storage/prerequisite-check.log storage/prerequisite-check.log

View File

@ -5,6 +5,10 @@ declare(strict_types=1);
// - define paths // - define paths
// - set up autoloader // - set up autoloader
// Set a couple ini settings for security
ini_set('allow_url_fopen', 0); // don't allow remote files to be read
ini_set('expose_php', 0); // don't advertise the PHP version
// Define all the important paths // Define all the important paths
define('APP_ROOT', dirname(dirname(__FILE__))); define('APP_ROOT', dirname(dirname(__FILE__)));
// Root-level directories // Root-level directories

View File

@ -13,6 +13,9 @@ RewriteRule ^(storage|src|templates|config)(/.*)?$ - [F,L]
# Block access to hidden files # Block access to hidden files
RewriteRule ^\..*$ - [F,L] RewriteRule ^\..*$ - [F,L]
# Block access to setup script
RewriteRule ^tkr-setup\.php$ - [F,L]
# Route everything else through public/index.php # Route everything else through public/index.php
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d

View File

@ -10,6 +10,7 @@ services:
- ./src:/var/www/html/tkr/src - ./src:/var/www/html/tkr/src
- ./storage:/var/www/html/tkr/storage - ./storage:/var/www/html/tkr/storage
- ./templates:/var/www/html/tkr/templates - ./templates:/var/www/html/tkr/templates
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
- ./docker/apache/shared-hosting/.htaccess:/var/www/html/tkr/.htaccess - ./docker/apache/shared-hosting/.htaccess:/var/www/html/tkr/.htaccess
command: > command: >
bash -c "a2enmod rewrite headers expires && bash -c "a2enmod rewrite headers expires &&

View File

@ -10,6 +10,7 @@ services:
- ./src:/var/www/tkr/src - ./src:/var/www/tkr/src
- ./storage:/var/www/tkr/storage - ./storage:/var/www/tkr/storage
- ./templates:/var/www/tkr/templates - ./templates:/var/www/tkr/templates
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
- ./docker/apache/vps/root/tkr.my-domain.com.conf:/etc/apache2/sites-enabled/tkr.my-domain.com.conf - ./docker/apache/vps/root/tkr.my-domain.com.conf:/etc/apache2/sites-enabled/tkr.my-domain.com.conf
command: > command: >
bash -c "a2enmod rewrite headers expires && bash -c "a2enmod rewrite headers expires &&

View File

@ -10,6 +10,7 @@ services:
- ./src:/var/www/tkr/src - ./src:/var/www/tkr/src
- ./storage:/var/www/tkr/storage - ./storage:/var/www/tkr/storage
- ./templates:/var/www/tkr/templates - ./templates:/var/www/tkr/templates
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
- ./docker/apache/vps/subfolder/my-domain.com.conf:/etc/apache2/sites-enabled/my-domain.com.conf - ./docker/apache/vps/subfolder/my-domain.com.conf:/etc/apache2/sites-enabled/my-domain.com.conf
command: > command: >
bash -c "a2enmod rewrite headers expires && bash -c "a2enmod rewrite headers expires &&

View File

@ -20,6 +20,7 @@ services:
- ./src:/var/www/tkr/src - ./src:/var/www/tkr/src
- ./storage:/var/www/tkr/storage - ./storage:/var/www/tkr/storage
- ./templates:/var/www/tkr/templates - ./templates:/var/www/tkr/templates
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
command: > command: >
sh -c " sh -c "
chown -R www-data:www-data /var/www/tkr/storage && chown -R www-data:www-data /var/www/tkr/storage &&

View File

@ -20,6 +20,7 @@ services:
- ./src:/var/www/tkr/src - ./src:/var/www/tkr/src
- ./storage:/var/www/tkr/storage - ./storage:/var/www/tkr/storage
- ./templates:/var/www/tkr/templates - ./templates:/var/www/tkr/templates
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
command: > command: >
sh -c " sh -c "
chown -R www-data:www-data /var/www/tkr/storage && chown -R www-data:www-data /var/www/tkr/storage &&

View File

@ -14,6 +14,9 @@ RewriteRule ^(storage|src|templates|config)(/.*)?$ - [F,L]
# Block access to hidden files # Block access to hidden files
RewriteRule ^\..*$ - [F,L] RewriteRule ^\..*$ - [F,L]
# Block access to setup script
RewriteRule ^tkr-setup\.php$ - [F,L]
# Route everything else through the front controller # Route everything else through the front controller
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d

View File

@ -49,7 +49,8 @@ if (!$prerequisites->applyMigrations($db)){
} }
// Check if setup is complete (user exists and URL is configured) // Check if setup is complete (user exists and URL is configured)
if (!(preg_match('/tkr-setup$/', $path))) { // Skip the setup check for the default css
if (!(preg_match('/tkr-setup$/', $path) || preg_match('/default.css$/', $path))) {
try { try {
$user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn(); $user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn();
$settings = (new SettingsModel($db))->get(); $settings = (new SettingsModel($db))->get();
@ -72,6 +73,10 @@ if (!(preg_match('/tkr-setup$/', $path))) {
echo "<p>Please check your installation or contact your hosting provider.</p>"; echo "<p>Please check your installation or contact your hosting provider.</p>";
exit; exit;
} }
} else {
// we're heading to setup. the base path hasn't been set. autodetect it
$autodetected = Util::getAutodetectedUrl();
$basePath = $autodetected['basePath'];
} }
/* /*
@ -92,8 +97,11 @@ Session::start();
Session::generateCsrfToken(); Session::generateCsrfToken();
// Remove the base path from the URL // Remove the base path from the URL
if (strpos($path, $app['settings']->basePath) === 0) { // If basePath isn't already set (i.e. we're not autodetecting it en route to tkr-setup),
$path = substr($path, strlen($app['settings']->basePath)); // set it to the value from settings
$basePath ??= $app['settings']->basePath;
if (strpos($path, $basePath) === 0) {
$path = substr($path, strlen($basePath));
} }
// strip the trailing slash from the resulting route // strip the trailing slash from the resulting route
@ -106,7 +114,7 @@ Log::debug("Path requested: {$path}");
// if this is a POST and we aren't in setup, // if this is a POST and we aren't in setup,
// make sure there's a valid session // make sure there's a valid session
// if not, redirect to /login or die as appropriate // if not, redirect to /login or die as appropriate
if ($method === 'POST' && $path != 'setup') { if ($method === 'POST' && $path != 'tkr-setup') {
if ($path != 'login'){ if ($path != 'login'){
if (!Session::isValid($_POST['csrf_token'])) { if (!Session::isValid($_POST['csrf_token'])) {
// Invalid session - redirect to /login // Invalid session - redirect to /login

View File

@ -100,7 +100,7 @@ class CssController extends Controller {
} }
// Get the data for the selected CSS file // Get the data for the selected CSS file
$cssId = $_POST['selectCssFile']; $cssId = (int) $_POST['selectCssFile'];
$cssModel = new CssModel($app['db']); $cssModel = new CssModel($app['db']);
$cssRow = $cssModel->getById($cssId); $cssRow = $cssModel->getById($cssId);

View File

@ -407,7 +407,7 @@ class Prerequisites {
} }
} }
private function createDatabase() { private function createDatabase(): bool {
$dbFile = $this->baseDir . '/storage/db/tkr.sqlite'; $dbFile = $this->baseDir . '/storage/db/tkr.sqlite';
// Test database connection (will create file if needed) // Test database connection (will create file if needed)
@ -442,7 +442,7 @@ class Prerequisites {
} }
} }
public function applyMigrations($db) { public function applyMigrations($db): bool {
try { try {
$migrator = new Migrator($db); $migrator = new Migrator($db);
$migrator->migrate(); $migrator->migrate();
@ -452,8 +452,6 @@ class Prerequisites {
true, true,
'All database migrations applied successfully' 'All database migrations applied successfully'
); );
return true;
} catch (Exception $e) { } catch (Exception $e) {
$this->addCheck( $this->addCheck(
'Database Migrations', 'Database Migrations',
@ -463,6 +461,8 @@ class Prerequisites {
); );
return false; return false;
} }
return true;
} }
// Validate system requirements that can't be fixed by the script // Validate system requirements that can't be fixed by the script
@ -511,6 +511,8 @@ class Prerequisites {
// Create missing application components // Create missing application components
public function createMissing(): bool { public function createMissing(): bool {
// If we're calling this, there were likely setup validation errors
$currentErrors = count($this->errors);
$this->log("=== tkr setup started at " . date('Y-m-d H:i:s') . " ===", true); $this->log("=== tkr setup started at " . date('Y-m-d H:i:s') . " ===", true);
if ($this->isCli) { if ($this->isCli) {
@ -526,8 +528,8 @@ class Prerequisites {
$this->generateCliSummary($results); $this->generateCliSummary($results);
} }
// Return true only if no errors occurred // Return true only if no NEW errors occurred
return count($this->errors) === 0; return count($this->errors) === $currentErrors;
} }
/** /**

View File

@ -7,6 +7,15 @@ class Session {
// global $_SESSION associative array // global $_SESSION associative array
public static function start(): void{ public static function start(): void{
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
// Cookie security settings
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_samesite', 'Strict');
// Enable secure cookie flag if HTTPS is being used
if (($_SERVER['HTTPS'] ?? 'off') === 'on') {
ini_set('session.cookie_secure', 1);
}
$existingSessionId = $_COOKIE['PHPSESSID'] ?? null; $existingSessionId = $_COOKIE['PHPSESSID'] ?? null;
session_start(); session_start();

View File

@ -125,6 +125,13 @@ class Util {
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '/index.php'; $scriptName = $_SERVER['SCRIPT_NAME'] ?? '/index.php';
$basePath = dirname($scriptName); $basePath = dirname($scriptName);
// Handle shared hosting scenario where document root can't be set to public/
// If script name ends with /public/index.php, we need to go up one directory
if (str_ends_with($scriptName, '/public/index.php')) {
$basePath = dirname($basePath);
}
# Ensure base path always has a trailing /
if ($basePath === '/' || $basePath === '.' || $basePath === '') { if ($basePath === '/' || $basePath === '.' || $basePath === '') {
$basePath = '/'; $basePath = '/';
} else { } else {
@ -132,10 +139,7 @@ class Util {
} }
// Construct full URL // Construct full URL
$fullUrl = $baseUrl; $fullUrl = $baseUrl . $basePath;
if ($basePath !== '/') {
$fullUrl .= ltrim($basePath, '/');
}
return [ return [
'baseUrl' => $baseUrl, 'baseUrl' => $baseUrl,

View File

@ -1,12 +1,16 @@
<?php /** @var SettingsModel $settings */ ?> <?php /** @var SettingsModel $settings */ ?>
<?php /** @var UserModel $user */ ?> <?php /** @var UserModel $user */ ?>
<?php /** @var isSetup bool */ ?> <?php /** @var isSetup bool */ ?>
<h1><?php if ($isSetup): ?>Setup<?php else: ?>Admin<?php endif; ?></h1> <?php
$title = $isSetup ? 'Setup' : 'Admin';
$urlPath = $isSetup ? 'tkr-setup' : 'admin'
?>
<h1><?php echo $title ?></h1>
<main> <main>
<form <form
action="<?php echo Util::buildRelativeUrl($settings->basePath, ($isSetup ? 'setup' : 'admin')) ?>" action="<?php echo Util::buildRelativeUrl($settings->basePath, $urlPath) ?>"
method="post"> method="post">
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>"> <input type="hidden" name="csrf_token" value="<?php echo Util::escape_html($_SESSION['csrf_token']) ?>">
<fieldset> <fieldset>
<legend>User settings</legend> <legend>User settings</legend>
<div class="fieldset-items"> <div class="fieldset-items">
@ -14,19 +18,19 @@
<input type="text" <input type="text"
id="username" id="username"
name="username" name="username"
value="<?= Util::escape_html($user->username) ?>" value="<?php echo Util::escape_html($user->username) ?>"
required> required>
<label for="display_name">Display name <span class=required>*</span></label> <label for="display_name">Display name <span class=required>*</span></label>
<input type="text" <input type="text"
id="display_name" id="display_name"
name="display_name" name="display_name"
value="<?= Util::escape_html($user->displayName) ?>" value="<?php echo Util::escape_html($user->displayName) ?>"
required> required>
<label for="website">Website </label> <label for="website">Website </label>
<input type="text" <input type="text"
id="website" id="website"
name="website" name="website"
value="<?= Util::escape_html($user->website) ?>"> value="<?php echo Util::escape_html($user->website) ?>">
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
@ -36,36 +40,36 @@
<input type="text" <input type="text"
id="site_title" id="site_title"
name="site_title" name="site_title"
value="<?= Util::escape_html($settings->siteTitle) ?>" value="<?php echo Util::escape_html($settings->siteTitle) ?>"
required> required>
<label for="site_description">Description <span class=required>*</span></label> <label for="site_description">Description <span class=required>*</span></label>
<input type="text" <input type="text"
id="site_description" id="site_description"
name="site_description" name="site_description"
value="<?= Util::escape_html($settings->siteDescription) ?>"> value="<?php echo Util::escape_html($settings->siteDescription) ?>">
<label for="base_url">Base URL <span class=required>*</span></label> <label for="base_url">Base URL <span class=required>*</span></label>
<input type="text" <input type="text"
id="base_url" id="base_url"
name="base_url" name="base_url"
value="<?= Util::escape_html($settings->baseUrl) ?>" value="<?php echo Util::escape_html($settings->baseUrl) ?>"
required> required>
<label for="base_path">Base path <span class=required>*</span></label> <label for="base_path">Base path <span class=required>*</span></label>
<input type="text" <input type="text"
id="base_path" id="base_path"
name="base_path" name="base_path"
value="<?= Util::escape_html($settings->basePath) ?>" value="<?php echo Util::escape_html($settings->basePath) ?>"
required> required>
<label for="items_per_page">Ticks per page (max 50) <span class=required>*</span></label> <label for="items_per_page">Ticks per page (max 50) <span class=required>*</span></label>
<input type="number" <input type="number"
id="items_per_page" id="items_per_page"
name="items_per_page" name="items_per_page"
value="<?= $settings->itemsPerPage ?>" min="1" max="50" value="<?php echo $settings->itemsPerPage ?>" min="1" max="50"
required> required>
<label for="tick_delete_hours">Tick delete window (hours)</label> <label for="tick_delete_hours">Tick delete window (hours)</label>
<input type="number" <input type="number"
id="tick_delete_hours" id="tick_delete_hours"
name="tick_delete_hours" name="tick_delete_hours"
value="<?= ($settings->tickDeleteHours ?? 1) ?>" min="1"> value="<?php echo ($settings->tickDeleteHours ?? 1) ?>" min="1">
<label for="strict_accessibility">Strict accessibility</label> <label for="strict_accessibility">Strict accessibility</label>
<input type="checkbox" <input type="checkbox"
id="strict_accessibility" id="strict_accessibility"
@ -74,10 +78,10 @@
<?php if ($settings->strictAccessibility): ?> checked <?php endif; ?>> <?php if ($settings->strictAccessibility): ?> checked <?php endif; ?>>
<label for="log_level">Log Level</label> <label for="log_level">Log Level</label>
<select id="log_level" name="log_level"> <select id="log_level" name="log_level">
<option value="1" <?= ($settings->logLevel ?? 2) == 1 ? 'selected' : '' ?>>DEBUG</option> <option value="1" <?php echo ($settings->logLevel ?? 2) == 1 ? 'selected' : '' ?>>DEBUG</option>
<option value="2" <?= ($settings->logLevel ?? 2) == 2 ? 'selected' : '' ?>>INFO</option> <option value="2" <?php echo ($settings->logLevel ?? 2) == 2 ? 'selected' : '' ?>>INFO</option>
<option value="3" <?= ($settings->logLevel ?? 2) == 3 ? 'selected' : '' ?>>WARNING</option> <option value="3" <?php echo ($settings->logLevel ?? 2) == 3 ? 'selected' : '' ?>>WARNING</option>
<option value="4" <?= ($settings->logLevel ?? 2) == 4 ? 'selected' : '' ?>>ERROR</option> <option value="4" <?php echo ($settings->logLevel ?? 2) == 4 ? 'selected' : '' ?>>ERROR</option>
</select> </select>
</div> </div>
</fieldset> </fieldset>

View File

@ -177,6 +177,7 @@ try {
// Create/update settings // Create/update settings
$settingsModel = new SettingsModel($db); $settingsModel = new SettingsModel($db);
$settingsModel->siteTitle = $siteTitle; $settingsModel->siteTitle = $siteTitle;
$settingsModel->siteDescription = $siteTitle;
$settingsModel->baseUrl = $baseUrl; $settingsModel->baseUrl = $baseUrl;
$settingsModel->basePath = $basePath; $settingsModel->basePath = $basePath;
$settings = $settingsModel->save(); $settings = $settingsModel->save();
@ -184,7 +185,7 @@ try {
// Create admin user // Create admin user
$userModel = new UserModel($db); $userModel = new UserModel($db);
$userModel->username = $adminUsername; $userModel->username = $adminUsername;
$userModel->display_name = $adminUsername; $userModel->displayName = $adminUsername;
$userModel->website = ''; $userModel->website = '';
$userModel->mood = ''; $userModel->mood = '';
$user = $userModel->save(); $user = $userModel->save();