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
/docker-compose.yml
scratch
storage.bak
# Build artifacts
tkr.tgz
# Test logs
storage/prerequisite-check.log
storage/prerequisite-check.log

View File

@ -5,6 +5,10 @@ declare(strict_types=1);
// - define paths
// - 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('APP_ROOT', dirname(dirname(__FILE__)));
// Root-level directories

View File

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

View File

@ -10,6 +10,7 @@ services:
- ./src:/var/www/html/tkr/src
- ./storage:/var/www/html/tkr/storage
- ./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
command: >
bash -c "a2enmod rewrite headers expires &&

View File

@ -10,6 +10,7 @@ services:
- ./src:/var/www/tkr/src
- ./storage:/var/www/tkr/storage
- ./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
command: >
bash -c "a2enmod rewrite headers expires &&

View File

@ -10,6 +10,7 @@ services:
- ./src:/var/www/tkr/src
- ./storage:/var/www/tkr/storage
- ./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
command: >
bash -c "a2enmod rewrite headers expires &&

View File

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

View File

@ -20,6 +20,7 @@ services:
- ./src:/var/www/tkr/src
- ./storage:/var/www/tkr/storage
- ./templates:/var/www/tkr/templates
- ./tkr-setup.php:/var/www/html/tkr/tkr-setup.php
command: >
sh -c "
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
RewriteRule ^\..*$ - [F,L]
# Block access to setup script
RewriteRule ^tkr-setup\.php$ - [F,L]
# Route everything else through the front controller
RewriteCond %{REQUEST_FILENAME} !-f
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)
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 {
$user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn();
$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>";
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();
// Remove the base path from the URL
if (strpos($path, $app['settings']->basePath) === 0) {
$path = substr($path, strlen($app['settings']->basePath));
// If basePath isn't already set (i.e. we're not autodetecting it en route to tkr-setup),
// 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
@ -106,7 +114,7 @@ Log::debug("Path requested: {$path}");
// if this is a POST and we aren't in setup,
// make sure there's a valid session
// if not, redirect to /login or die as appropriate
if ($method === 'POST' && $path != 'setup') {
if ($method === 'POST' && $path != 'tkr-setup') {
if ($path != 'login'){
if (!Session::isValid($_POST['csrf_token'])) {
// Invalid session - redirect to /login

View File

@ -100,7 +100,7 @@ class CssController extends Controller {
}
// Get the data for the selected CSS file
$cssId = $_POST['selectCssFile'];
$cssId = (int) $_POST['selectCssFile'];
$cssModel = new CssModel($app['db']);
$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';
// Test database connection (will create file if needed)
@ -442,7 +442,7 @@ class Prerequisites {
}
}
public function applyMigrations($db) {
public function applyMigrations($db): bool {
try {
$migrator = new Migrator($db);
$migrator->migrate();
@ -452,8 +452,6 @@ class Prerequisites {
true,
'All database migrations applied successfully'
);
return true;
} catch (Exception $e) {
$this->addCheck(
'Database Migrations',
@ -463,6 +461,8 @@ class Prerequisites {
);
return false;
}
return true;
}
// Validate system requirements that can't be fixed by the script
@ -511,6 +511,8 @@ class Prerequisites {
// Create missing application components
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);
if ($this->isCli) {
@ -526,8 +528,8 @@ class Prerequisites {
$this->generateCliSummary($results);
}
// Return true only if no errors occurred
return count($this->errors) === 0;
// Return true only if no NEW errors occurred
return count($this->errors) === $currentErrors;
}
/**

View File

@ -7,6 +7,15 @@ class Session {
// global $_SESSION associative array
public static function start(): void{
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;
session_start();

View File

@ -125,6 +125,13 @@ class Util {
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '/index.php';
$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 === '') {
$basePath = '/';
} else {
@ -132,10 +139,7 @@ class Util {
}
// Construct full URL
$fullUrl = $baseUrl;
if ($basePath !== '/') {
$fullUrl .= ltrim($basePath, '/');
}
$fullUrl = $baseUrl . $basePath;
return [
'baseUrl' => $baseUrl,

View File

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

View File

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