Make first time setup more robust. (#58)

Split out prerequisite validation from creation. Distinguish system prereqs from application prereqs. Support URL autodetection to eliminate requirement to edit file as part of setup.

Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/58
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-05 21:48:10 +00:00 committed by greg
parent e5945e91a3
commit 086c3e2ebb
10 changed files with 473 additions and 166 deletions

View File

@ -20,14 +20,14 @@ jobs:
run: | run: |
if [[ "${{ matrix.php }}" < "8.2" ]]; then if [[ "${{ matrix.php }}" < "8.2" ]]; then
echo "Testing PHP ${{ matrix.php }} - should fail" echo "Testing PHP ${{ matrix.php }} - should fail"
if php check-prerequisites.php; then if php tkr-setup.php --validate-only; then
echo "ERROR: Should have failed with PHP ${{ matrix.php }}" echo "ERROR: Should have failed with PHP ${{ matrix.php }}"
exit 1 exit 1
fi fi
echo "✓ Correctly failed with old PHP version" echo "✓ Correctly failed with old PHP version"
else else
echo "Testing PHP ${{ matrix.php }} - should pass" echo "Testing PHP ${{ matrix.php }} - should pass"
php check-prerequisites.php php tkr-setup.php --validate-only
echo "✓ Correctly passed with supported PHP version" echo "✓ Correctly passed with supported PHP version"
fi fi
@ -66,7 +66,7 @@ jobs:
- name: Test failure with missing extensions - name: Test failure with missing extensions
run: | run: |
echo "Testing with base PHP - should fail" echo "Testing with base PHP - should fail"
if php check-prerequisites.php; then if php tkr-setup.php --validate-only; then
echo "ERROR: Should have failed with missing extensions" echo "ERROR: Should have failed with missing extensions"
exit 1 exit 1
fi fi
@ -86,7 +86,7 @@ jobs:
- name: Test still fails without SQLite - name: Test still fails without SQLite
run: | run: |
echo "Testing with PDO but no SQLite - should still fail" echo "Testing with PDO but no SQLite - should still fail"
if php check-prerequisites.php; then if php tkr-setup.php --validate-only; then
echo "ERROR: Should have failed without SQLite" echo "ERROR: Should have failed without SQLite"
exit 1 exit 1
fi fi
@ -105,7 +105,7 @@ jobs:
- name: Test now passes with required extensions - name: Test now passes with required extensions
run: | run: |
echo "Testing with all required extensions - should pass" echo "Testing with all required extensions - should pass"
php check-prerequisites.php php tkr-setup.php --validate-only
echo "✓ All required extensions detected correctly" echo "✓ All required extensions detected correctly"
- name: Install recommended extensions and retest - name: Install recommended extensions and retest
@ -117,7 +117,7 @@ jobs:
else else
apt-get install -y php-mbstring php-curl apt-get install -y php-mbstring php-curl
fi fi
php check-prerequisites.php php tkr-setup.php --validate-only
echo "✓ Recommended extensions also detected" echo "✓ Recommended extensions also detected"
test-permission-scenarios: test-permission-scenarios:
@ -142,7 +142,7 @@ jobs:
chown root:root storage chown root:root storage
# Run as the non-root user - should fail # Run as the non-root user - should fail
if su testuser -c "php check-prerequisites.php"; then if su testuser -c "php tkr-setup.php --validate-only"; then
echo "ERROR: Should have failed with unwritable storage" echo "ERROR: Should have failed with unwritable storage"
exit 1 exit 1
fi fi
@ -157,7 +157,7 @@ jobs:
rm -rf src templates rm -rf src templates
# Should fail # Should fail
if php check-prerequisites.php; then if php tkr-setup.php --validate-only; then
echo "ERROR: Should have failed with missing directories" echo "ERROR: Should have failed with missing directories"
exit 1 exit 1
fi fi

View File

@ -1,21 +0,0 @@
#!/usr/bin/env php
<?php
/**
* tkr Prerequisites Checker - CLI Diagnostic Tool
*
* This script provides comprehensive diagnostic information for tkr.
* It can be run from the command line or uploaded separately for troubleshooting.
*
* Usage: php check-prerequisites.php
*/
// Minimal bootstrap just for prerequisites
include_once __DIR__ . '/config/bootstrap.php';
$prerequisites = new Prerequisites();
$results = $prerequisites->validate();
// Exit with appropriate code for shell scripts
if (php_sapi_name() === 'cli') {
exit(count($prerequisites->getErrors()) > 0 ? 1 : 0);
}

View File

@ -1,7 +0,0 @@
<?php
// initial configuration. These need to be set on first run so the app loads properly.
// Other settings can be defined in the admin page that loads on first run.
return [
'base_url' => 'http://localhost',
'base_path' => '/tkr/',
];

View File

@ -22,27 +22,37 @@ include_once(dirname(dirname(__FILE__)) . "/config/bootstrap.php");
* Validate application state before processing request * Validate application state before processing request
*/ */
// Check prerequisites (includes database connection and migrations) // Check system requirements first
$prerequisites = new Prerequisites(); $prerequisites = new Prerequisites();
if (!$prerequisites->validate()) { if (!$prerequisites->validateSystem()) {
$prerequisites->generateWebSummary(); $prerequisites->generateWebSummary();
exit; exit;
} }
// Check application state and create missing components if needed
if (!$prerequisites->validateApplication()) {
if (!$prerequisites->createMissing()) {
$prerequisites->generateWebSummary();
exit;
}
}
// Get the working database connection from prerequisites // Get the working database connection from prerequisites
$db = $prerequisites->getDatabase(); $db = $prerequisites->getDatabase();
// Make sure the initial setup is complete unless we're already heading to setup // Check if setup is complete (user exists and URL is configured)
if (!(preg_match('/setup$/', $path))) { if (!(preg_match('/tkr-setup$/', $path))) {
try { try {
// Make sure required tables (user, settings) are populated
$user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn(); $user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn();
$settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); $config = (new ConfigModel($db))->get();
// If either required table has no records, redirect to setup. $hasUser = $user_count > 0;
if ($user_count === 0 || $settings_count === 0){ $hasUrl = !empty($config->baseUrl) && !empty($config->basePath);
$init = require APP_ROOT . '/config/init.php';
header('Location: ' . $init['base_path'] . 'setup'); if (!$hasUser || !$hasUrl) {
// Redirect to setup with auto-detected URL
$autodetected = Util::getAutodetectedUrl();
header('Location: ' . $autodetected['fullUrl'] . '/tkr-setup');
exit; exit;
} }
} catch (Exception $e) { } catch (Exception $e) {

View File

@ -9,6 +9,21 @@ class AdminController extends Controller {
public function showSetup(){ public function showSetup(){
$data = $this->getAdminData(true); $data = $this->getAdminData(true);
// Auto-detect URL and pre-fill if not already configured
if (empty($data['config']->baseUrl) || empty($data['config']->basePath)) {
$autodetected = Util::getAutodetectedUrl();
$data['autodetectedUrl'] = $autodetected;
// Pre-fill empty values with auto-detected ones
if (empty($data['config']->baseUrl)) {
$data['config']->baseUrl = $autodetected['baseUrl'];
}
if (empty($data['config']->basePath)) {
$data['config']->basePath = $autodetected['basePath'];
}
}
$this->render("admin.php", $data); $this->render("admin.php", $data);
} }

View File

@ -5,7 +5,6 @@
* This class checks all system requirements for tkr and provides * This class checks all system requirements for tkr and provides
* detailed logging of any missing components or configuration issues. * detailed logging of any missing components or configuration issues.
* *
* ZERO DEPENDENCIES - Uses only core PHP functions available since PHP 5.3
*/ */
class Prerequisites { class Prerequisites {
@ -17,6 +16,12 @@ class Prerequisites {
private $isCli; private $isCli;
private $isWeb; private $isWeb;
private $database = null; private $database = null;
private $storageSubdirs = [
'storage/db',
'storage/logs',
'storage/upload',
'storage/upload/css',
];
public function __construct() { public function __construct() {
$this->isCli = php_sapi_name() === 'cli'; $this->isCli = php_sapi_name() === 'cli';
@ -175,7 +180,7 @@ class Prerequisites {
return $allPresent; return $allPresent;
} }
private function checkStoragePermissions() { private function checkExistingStoragePermissions() {
// Issue a warning if running as root in CLI context // Issue a warning if running as root in CLI context
// Write out guidance for storage directory permissions // Write out guidance for storage directory permissions
// if running the CLI script as root (since it will always appear to be writable) // if running the CLI script as root (since it will always appear to be writable)
@ -195,20 +200,68 @@ class Prerequisites {
); );
} }
$storageDirs = array( $storageDirs = array_merge(
'storage', array('storage'),
'storage/db', $this->storageSubdirs
'storage/logs',
'storage/upload',
'storage/upload/css'
); );
$allWritable = true; $allWritable = true;
foreach ($storageDirs as $dir) { foreach ($storageDirs as $dir) {
$path = $this->baseDir . '/' . $dir; $path = $this->baseDir . '/' . $dir;
// Only check directories that exist - missing ones are handled in application validation
if (is_dir($path)) {
$writable = is_writable($path);
$permissions = substr(sprintf('%o', fileperms($path)), -4);
$this->addCheck(
"Storage Permissions: {$dir}",
$writable,
$writable ? "Writable (permissions: {$permissions})" : "Not writable (permissions: {$permissions})",
$writable ? 'info' : 'error'
);
if (!$writable) {
$allWritable = false;
}
}
}
return $allWritable;
}
private function checkStorageDirectoriesExist() {
$allPresent = true;
foreach ($this->storageSubdirs as $dir) {
$path = $this->baseDir . '/' . $dir;
$exists = is_dir($path);
$this->addCheck(
"Storage Directory: {$dir}",
$exists,
$exists ? "Present" : "Missing - will be created during setup",
$exists ? 'info' : 'info' // Not an error - can be auto-created
);
if (!$exists) {
$allPresent = false;
}
}
return $allPresent;
}
private function createStorageDirectories() {
$storageDirs = array_merge(
array('storage'),
$this->storageSubdirs
);
$allCreated = true;
foreach ($storageDirs as $dir) {
$path = $this->baseDir . '/' . $dir;
if (!is_dir($path)) { if (!is_dir($path)) {
// Try to create the directory
$created = @mkdir($path, 0770, true); $created = @mkdir($path, 0770, true);
if ($created) { if ($created) {
$this->addCheck( $this->addCheck(
@ -223,27 +276,18 @@ class Prerequisites {
"Could not create directory: {$dir}", "Could not create directory: {$dir}",
'error' 'error'
); );
$allWritable = false; $allCreated = false;
continue;
} }
} } else {
$this->addCheck(
$writable = is_writable($path); "Storage Directory: {$dir}",
$permissions = substr(sprintf('%o', fileperms($path)), -4); true,
"Already exists"
$this->addCheck( );
"Storage Permissions: {$dir}",
$writable,
$writable ? "Writable (permissions: {$permissions})" : "Not writable (permissions: {$permissions})",
$writable ? 'info' : 'error'
);
if (!$writable) {
$allWritable = false;
} }
} }
return $allWritable; return $allCreated;
} }
private function checkWebServerConfig() { private function checkWebServerConfig() {
@ -283,73 +327,19 @@ class Prerequisites {
return true; return true;
} }
private function checkConfiguration() {
$configFile = $this->baseDir . '/config/init.php';
$configExists = file_exists($configFile);
if (!$configExists) {
$this->addCheck(
'Configuration File',
false,
'config/init.php not found',
'error'
);
return false;
}
try {
$config = include $configFile;
$hasBaseUrl = isset($config['base_url']) && !empty($config['base_url']);
$hasBasePath = isset($config['base_path']) && !empty($config['base_path']);
$this->addCheck(
'Configuration File',
true,
'config/init.php exists and is readable'
);
$this->addCheck(
'Base URL Configuration',
$hasBaseUrl,
$hasBaseUrl ? "Set to: {$config['base_url']}" : 'Not configured',
$hasBaseUrl ? 'info' : 'warning'
);
$this->addCheck(
'Base Path Configuration',
$hasBasePath,
$hasBasePath ? "Set to: {$config['base_path']}" : 'Not configured',
$hasBasePath ? 'info' : 'warning'
);
return $hasBaseUrl && $hasBasePath;
} catch (Exception $e) {
$this->addCheck(
'Configuration File',
false,
'Error reading config/init.php: ' . $e->getMessage(),
'error'
);
return false;
}
}
private function checkDatabase() { private function checkDatabase() {
$dbFile = $this->baseDir . '/storage/db/tkr.sqlite'; $dbFile = $this->baseDir . '/storage/db/tkr.sqlite';
$dbDir = dirname($dbFile); $dbDir = dirname($dbFile);
if (!is_dir($dbDir)) { if (!is_dir($dbDir)) {
$created = @mkdir($dbDir, 0770, true); $this->addCheck(
if (!$created) { 'Database Directory',
$this->addCheck( false,
'Database Directory', 'Database directory does not exist',
false, 'error'
'Could not create storage/db directory', );
'error' return false;
);
return false;
}
} }
$canCreateDb = is_writable($dbDir); $canCreateDb = is_writable($dbDir);
@ -370,39 +360,75 @@ class Prerequisites {
$dbReadable && $dbWritable ? 'Exists and is accessible' : 'Exists but has permission issues', $dbReadable && $dbWritable ? 'Exists and is accessible' : 'Exists but has permission issues',
$dbReadable && $dbWritable ? 'info' : 'error' $dbReadable && $dbWritable ? 'info' : 'error'
); );
if ($dbReadable && $dbWritable) {
// Test database connection
try {
$db = new PDO("sqlite:" . $dbFile);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
// Test basic query to ensure database is functional
$db->query("SELECT 1")->fetchColumn();
$this->addCheck(
'Database Connection',
true,
'Successfully connected to database'
);
// Store working database connection
$this->database = $db;
return true;
} catch (PDOException $e) {
$this->addCheck(
'Database Connection',
false,
'Failed to connect: ' . $e->getMessage(),
'error'
);
return false;
}
} else {
return false;
}
} else { } else {
$this->addCheck( $this->addCheck(
'Database File', 'Database File',
true, $canCreateDb,
'Will be created on first run' $canCreateDb ? 'Will be created during setup' : 'Cannot create - directory not writable',
$canCreateDb ? 'info' : 'error'
); );
return $canCreateDb;
} }
}
if (!$canCreateDb) { private function createDatabase() {
return false; $dbFile = $this->baseDir . '/storage/db/tkr.sqlite';
}
// Test database connection // Test database connection (will create file if needed)
try { try {
$db = new PDO("sqlite:" . $dbFile); $db = new PDO("sqlite:" . $dbFile);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
// Test basic query to ensure database is functional // Test basic query to ensure database is functional
$db->query("SELECT 1")->fetchColumn(); $db->query("SELECT 1")->fetchColumn();
$this->addCheck( $this->addCheck(
'Database Connection', 'Database Connection',
true, true,
'Successfully connected to database' 'Successfully connected to database'
); );
// Store working database connection // Store working database connection
$this->database = $db; $this->database = $db;
// Test migrations // Run migrations
return $this->checkMigrations($db); return $this->applyMigrations($db);
} catch (PDOException $e) { } catch (PDOException $e) {
$this->addCheck( $this->addCheck(
'Database Connection', 'Database Connection',
@ -413,19 +439,19 @@ class Prerequisites {
return false; return false;
} }
} }
private function checkMigrations($db) { private function applyMigrations($db) {
try { try {
$migrator = new Migrator($db); $migrator = new Migrator($db);
$migrator->migrate(); $migrator->migrate();
$this->addCheck( $this->addCheck(
'Database Migrations', 'Database Migrations',
true, true,
'All database migrations applied successfully' 'All database migrations applied successfully'
); );
return true; return true;
} catch (Exception $e) { } catch (Exception $e) {
$this->addCheck( $this->addCheck(
'Database Migrations', 'Database Migrations',
@ -437,23 +463,20 @@ class Prerequisites {
} }
} }
// validate prereqs // Validate system requirements that can't be fixed by the script
// runs on each request and can be run from CLI public function validateSystem(): bool {
public function validate(): bool { $this->log("=== tkr system validation started at " . date('Y-m-d H:i:s') . " ===", true);
$this->log("=== tkr prerequisites check started at " . date('Y-m-d H:i:s') . " ===", true);
if ($this->isCli) { if ($this->isCli) {
$this->log("\n🔍 Validating prerequisites...\n"); $this->log("\n🔍 Validating system requirements...\n");
} }
$results = array( $results = array(
'php_version' => $this->checkPhpVersion(), 'php_version' => $this->checkPhpVersion(),
'critical_extensions' => $this->checkRequiredExtensions(), 'critical_extensions' => $this->checkRequiredExtensions(),
'directory_structure' => $this->checkDirectoryStructure(), 'directory_structure' => $this->checkDirectoryStructure(),
'storage_permissions' => $this->checkStoragePermissions(), 'existing_storage_permissions' => $this->checkExistingStoragePermissions(),
'web_server' => $this->checkWebServerConfig(), 'web_server' => $this->checkWebServerConfig()
'configuration' => $this->checkConfiguration(),
'database' => $this->checkDatabase()
); );
// Check recommended extensions too // Check recommended extensions too
@ -467,6 +490,44 @@ class Prerequisites {
return count($this->errors) === 0; return count($this->errors) === 0;
} }
// Validate application state - things that can be fixed
public function validateApplication(): bool {
$currentErrors = count($this->errors);
if ($this->isCli) {
$this->log("\n🔍 Validating application state...\n");
}
$results = array(
'storage_directories' => $this->checkStorageDirectoriesExist(),
'database' => $this->checkDatabase()
);
// Return true if no NEW errors occurred
return count($this->errors) === $currentErrors;
}
// Create missing application components
public function createMissing(): bool {
$this->log("=== tkr setup started at " . date('Y-m-d H:i:s') . " ===", true);
if ($this->isCli) {
$this->log("\n🚀 Creating missing components...\n");
}
$results = array(
'storage_setup' => $this->createStorageDirectories(),
'database_setup' => $this->createDatabase()
);
if ($this->isCli) {
$this->generateCliSummary($results);
}
// Return true only if no errors occurred
return count($this->errors) === 0;
}
/** /**
* Display web-friendly error page when minimum requirements aren't met * Display web-friendly error page when minimum requirements aren't met
*/ */
@ -625,7 +686,7 @@ class Prerequisites {
public function getWarnings() { public function getWarnings() {
return $this->warnings; return $this->warnings;
} }
/** /**
* Get working database connection (only call after validate() returns true) * Get working database connection (only call after validate() returns true)
*/ */

View File

@ -20,8 +20,8 @@ class Router {
['logout', 'AuthController@handleLogout', ['GET', 'POST']], ['logout', 'AuthController@handleLogout', ['GET', 'POST']],
['mood', 'MoodController'], ['mood', 'MoodController'],
['mood', 'MoodController@handlePost', ['POST']], ['mood', 'MoodController@handlePost', ['POST']],
['setup', 'AdminController@showSetup'], ['tkr-setup', 'AdminController@showSetup'],
['setup', 'AdminController@handleSetup', ['POST']], ['tkr-setup', 'AdminController@handleSetup', ['POST']],
['tick/{id}', 'TickController'], ['tick/{id}', 'TickController'],
['css/custom/{filename}.css', 'CssController@serveCustomCss'], ['css/custom/{filename}.css', 'CssController@serveCustomCss'],
]; ];

View File

@ -103,4 +103,42 @@ class Util {
return $basePath . '/' . $path; return $basePath . '/' . $path;
} }
/**
* Auto-detect base URL and path from HTTP request headers
* Returns array with baseUrl, basePath, and fullUrl
*/
public static function getAutodetectedUrl(): array {
// Detect base URL
$baseUrl = ($_SERVER['HTTPS'] ?? 'off') === 'on' ? 'https://' : 'http://';
$baseUrl .= $_SERVER['HTTP_HOST'] ?? 'localhost';
// Don't include standard ports in URL
$port = $_SERVER['SERVER_PORT'] ?? null;
if ($port && $port != 80 && $port != 443) {
$baseUrl .= ':' . $port;
}
// Detect base path from script location
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '/index.php';
$basePath = dirname($scriptName);
if ($basePath === '/' || $basePath === '.' || $basePath === '') {
$basePath = '/';
} else {
$basePath = '/' . trim($basePath, '/') . '/';
}
// Construct full URL
$fullUrl = $baseUrl;
if ($basePath !== '/') {
$fullUrl .= ltrim($basePath, '/');
}
return [
'baseUrl' => $baseUrl,
'basePath' => $basePath,
'fullUrl' => rtrim($fullUrl, '/')
];
}
} }

View File

@ -15,10 +15,7 @@ class ConfigModel {
// Instance method that uses injected database // Instance method that uses injected database
public function get(): self { public function get(): self {
$init = require APP_ROOT . '/config/init.php';
$c = new self($this->db); $c = new self($this->db);
$c->baseUrl = ($c->baseUrl === '') ? $init['base_url'] : $c->baseUrl;
$c->basePath = ($c->basePath === '') ? $init['base_path'] : $c->basePath;
$stmt = $this->db->query("SELECT site_title, $stmt = $this->db->query("SELECT site_title,
site_description, site_description,

214
tkr-setup.php Normal file
View File

@ -0,0 +1,214 @@
#!/usr/bin/env php
<?php
/**
* tkr Setup Script
*
* Interactive CLI setup for tkr - run this once after installation
* Usage: php tkr-setup.php [--validate-only]
*/
// Ensure this is run from command line only
if (php_sapi_name() !== 'cli') {
http_response_code(404);
exit;
}
// Check for validate-only flag
$validateOnly = in_array('--validate-only', $argv);
// Load the bootstrap
require_once __DIR__ . '/config/bootstrap.php';
if (!$validateOnly) {
echo "🚀 Welcome to tkr Setup!\n";
echo "This will configure your tkr installation.\n\n";
}
// Check system requirements first
$prerequisites = new Prerequisites();
if (!$prerequisites->validateSystem()) {
echo "\n❌ System requirements not met. Please resolve the issues above before continuing.\n";
exit(1);
}
echo "✅ System requirements met\n\n";
// Check application state
$applicationReady = $prerequisites->validateApplication();
if ($applicationReady) {
echo "✅ All prerequisites satisfied - tkr is ready to run!\n";
} else {
echo "⚠️ Application components need to be created\n\n";
if ($validateOnly) {
echo "⚠️ Run 'php tkr-setup.php' (without --validate-only) to complete setup.\n";
}
}
// If validate-only flag, exit here
if ($validateOnly) {
// Always exit with success.
// If app configuration needs to be completed, the script can handle that.
exit(0);
}
// Continue with setup process
$db = null;
try {
if ($applicationReady) {
$db = $prerequisites->getDatabase();
// Check if user already exists
$userCount = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn();
if ($userCount > 0) {
echo "⚠️ tkr appears to already be set up.\n";
echo "Continue anyway? (y/N): ";
$continue = trim(fgets(STDIN));
if (strtolower($continue) !== 'y') {
echo "Setup cancelled.\n";
exit(0);
}
echo "\n";
}
}
} catch (Exception $e) {
// Application not ready - will create below
}
// If application isn't ready, create missing components
if (!$db) {
echo "Setting up application components...\n";
if (!$prerequisites->createMissing()) {
echo "❌ Failed to create application components. Check the errors above.\n";
exit(1);
}
try {
$db = $prerequisites->getDatabase();
echo "✅ Application components created\n\n";
} catch (Exception $e) {
echo "❌ Failed to get database connection: " . $e->getMessage() . "\n";
exit(1);
}
}
// Prompt for configuration
echo "📝 Please provide the following information:\n\n";
// 1. Site URL (with auto-detect option)
echo "1. Site URL (including base path if not root)\n";
echo " Examples: https://example.com or https://example.com/tkr\n";
echo " Leave blank to auto-detect from first web request\n";
echo " Site URL (optional): ";
$siteUrl = trim(fgets(STDIN));
if (empty($siteUrl)) {
echo "✅ Will auto-detect URL on first web request\n";
$baseUrl = '';
$basePath = '';
} else {
// Parse URL to extract base URL and base path
$parsedUrl = parse_url($siteUrl);
if (!$parsedUrl || !isset($parsedUrl['scheme']) || !isset($parsedUrl['host'])) {
echo "❌ Invalid URL format\n";
exit(1);
}
// Validate host for basic security
if (!preg_match('/^[a-zA-Z0-9.-]+$/', $parsedUrl['host'])) {
echo "❌ Invalid characters in hostname\n";
exit(1);
}
$baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
if (isset($parsedUrl['port']) && $parsedUrl['port'] != 80 && $parsedUrl['port'] != 443) {
$baseUrl .= ':' . $parsedUrl['port'];
}
$basePath = isset($parsedUrl['path']) ? rtrim($parsedUrl['path'], '/') : '';
if (empty($basePath)) {
$basePath = '/';
} else {
$basePath = '/' . trim($basePath, '/') . '/';
}
}
echo "\n";
// 2. Admin credentials
echo "2. Admin username: ";
$adminUsername = trim(fgets(STDIN));
if (empty($adminUsername)) {
echo "❌ Admin username is required\n";
exit(1);
}
echo "3. Admin password: ";
// Hide password input on Unix systems
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
system('stty -echo');
}
$adminPassword = trim(fgets(STDIN));
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
system('stty echo');
}
echo "\n";
if (empty($adminPassword)) {
echo "❌ Admin password is required\n";
exit(1);
}
echo "\n4. Site title (optional, default: 'My tkr Site'): ";
$siteTitle = trim(fgets(STDIN));
if (empty($siteTitle)) {
$siteTitle = 'My tkr Site';
}
echo "\n";
// Save configuration
try {
echo "💾 Saving configuration...\n";
// Create/update settings
$configModel = new ConfigModel($db);
$configModel->siteTitle = $siteTitle;
$configModel->baseUrl = $baseUrl;
$configModel->basePath = $basePath;
$config = $configModel->save();
// Create admin user
$userModel = new UserModel($db);
$userModel->username = $adminUsername;
$userModel->display_name = $adminUsername;
$userModel->website = '';
$userModel->mood = '';
$user = $userModel->save();
// Set admin password
$userModel->setPassword($adminPassword);
echo "✅ Configuration saved\n";
echo "✅ Admin user created\n\n";
echo "🎉 Setup complete!\n\n";
if (!empty($baseUrl)) {
echo "Your tkr site is ready at: $siteUrl\n";
} else {
echo "Your tkr site will be ready after you visit it in a web browser\n";
echo "The URL will be auto-detected on first access\n";
}
echo "Login with username: $adminUsername\n\n";
echo "You can now:\n";
echo "• Point your web server document root to the 'public/' directory\n";
echo "• Visit your site and log in\n";
echo "• Customize additional settings through the admin interface\n\n";
} catch (Exception $e) {
echo "❌ Setup failed: " . $e->getMessage() . "\n";
exit(1);
}