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:
		
							parent
							
								
									e5945e91a3
								
							
						
					
					
						commit
						086c3e2ebb
					
				| @ -20,14 +20,14 @@ jobs: | ||||
|         run: | | ||||
|           if [[ "${{ matrix.php }}" < "8.2" ]]; then | ||||
|             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 }}" | ||||
|               exit 1 | ||||
|             fi | ||||
|             echo "✓ Correctly failed with old PHP version" | ||||
|           else | ||||
|             echo "Testing PHP ${{ matrix.php }} - should pass" | ||||
|             php check-prerequisites.php | ||||
|             php tkr-setup.php --validate-only | ||||
|             echo "✓ Correctly passed with supported PHP version" | ||||
|           fi | ||||
| 
 | ||||
| @ -66,7 +66,7 @@ jobs: | ||||
|       - name: Test failure with missing extensions | ||||
|         run: | | ||||
|           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" | ||||
|             exit 1 | ||||
|           fi | ||||
| @ -86,7 +86,7 @@ jobs: | ||||
|       - name: Test still fails without SQLite | ||||
|         run: | | ||||
|           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" | ||||
|             exit 1 | ||||
|           fi | ||||
| @ -105,7 +105,7 @@ jobs: | ||||
|       - name: Test now passes with required extensions | ||||
|         run: | | ||||
|           echo "Testing with all required extensions - should pass" | ||||
|           php check-prerequisites.php | ||||
|           php tkr-setup.php --validate-only | ||||
|           echo "✓ All required extensions detected correctly" | ||||
| 
 | ||||
|       - name: Install recommended extensions and retest | ||||
| @ -117,7 +117,7 @@ jobs: | ||||
|           else | ||||
|             apt-get install -y php-mbstring php-curl | ||||
|           fi | ||||
|           php check-prerequisites.php | ||||
|           php tkr-setup.php --validate-only | ||||
|           echo "✓ Recommended extensions also detected" | ||||
| 
 | ||||
|   test-permission-scenarios: | ||||
| @ -142,7 +142,7 @@ jobs: | ||||
|           chown root:root storage | ||||
| 
 | ||||
|           # 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" | ||||
|             exit 1 | ||||
|           fi | ||||
| @ -157,7 +157,7 @@ jobs: | ||||
|           rm -rf src templates | ||||
| 
 | ||||
|           # Should fail | ||||
|           if php check-prerequisites.php; then | ||||
|           if php tkr-setup.php --validate-only; then | ||||
|             echo "ERROR: Should have failed with missing directories" | ||||
|             exit 1 | ||||
|           fi | ||||
| @ -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); | ||||
| } | ||||
| @ -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/', | ||||
| ]; | ||||
| @ -22,27 +22,37 @@ include_once(dirname(dirname(__FILE__)) . "/config/bootstrap.php"); | ||||
|  *  Validate application state before processing request | ||||
|  */ | ||||
| 
 | ||||
| // Check prerequisites (includes database connection and migrations)
 | ||||
| // Check system requirements first
 | ||||
| $prerequisites = new Prerequisites(); | ||||
| if (!$prerequisites->validate()) { | ||||
| if (!$prerequisites->validateSystem()) { | ||||
|     $prerequisites->generateWebSummary(); | ||||
|     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
 | ||||
| $db = $prerequisites->getDatabase(); | ||||
| 
 | ||||
| // Make sure the initial setup is complete unless we're already heading to setup
 | ||||
| if (!(preg_match('/setup$/', $path))) { | ||||
| // Check if setup is complete (user exists and URL is configured)
 | ||||
| if (!(preg_match('/tkr-setup$/', $path))) { | ||||
|     try { | ||||
|         // Make sure required tables (user, settings) are populated
 | ||||
|         $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.
 | ||||
|         if ($user_count === 0 || $settings_count === 0){ | ||||
|             $init = require APP_ROOT . '/config/init.php'; | ||||
|             header('Location: ' . $init['base_path'] . 'setup'); | ||||
|         $hasUser = $user_count > 0; | ||||
|         $hasUrl = !empty($config->baseUrl) && !empty($config->basePath); | ||||
|          | ||||
|         if (!$hasUser || !$hasUrl) { | ||||
|             // Redirect to setup with auto-detected URL
 | ||||
|             $autodetected = Util::getAutodetectedUrl(); | ||||
|             header('Location: ' . $autodetected['fullUrl'] . '/tkr-setup'); | ||||
|             exit; | ||||
|         } | ||||
|     } catch (Exception $e) { | ||||
|  | ||||
| @ -9,6 +9,21 @@ class AdminController extends Controller { | ||||
| 
 | ||||
|     public function showSetup(){ | ||||
|         $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); | ||||
|     } | ||||
|      | ||||
|  | ||||
| @ -5,7 +5,6 @@ | ||||
|  * This class checks all system requirements for tkr and provides | ||||
|  * detailed logging of any missing components or configuration issues. | ||||
|  * | ||||
|  * ZERO DEPENDENCIES - Uses only core PHP functions available since PHP 5.3 | ||||
|  */ | ||||
| 
 | ||||
| class Prerequisites { | ||||
| @ -17,6 +16,12 @@ class Prerequisites { | ||||
|     private $isCli; | ||||
|     private $isWeb; | ||||
|     private $database = null; | ||||
|     private $storageSubdirs = [ | ||||
|         'storage/db', | ||||
|         'storage/logs', | ||||
|         'storage/upload', | ||||
|         'storage/upload/css', | ||||
|     ]; | ||||
| 
 | ||||
|     public function __construct() { | ||||
|         $this->isCli = php_sapi_name() === 'cli'; | ||||
| @ -175,7 +180,7 @@ class Prerequisites { | ||||
|         return $allPresent; | ||||
|     } | ||||
| 
 | ||||
|     private function checkStoragePermissions() { | ||||
|     private function checkExistingStoragePermissions() { | ||||
|         // Issue a warning if running as root in CLI context
 | ||||
|         // Write out guidance for storage directory permissions
 | ||||
|         // if running the CLI script as root (since it will always appear to be writable)
 | ||||
| @ -195,20 +200,68 @@ class Prerequisites { | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         $storageDirs = array( | ||||
|             'storage', | ||||
|             'storage/db', | ||||
|             'storage/logs', | ||||
|             'storage/upload', | ||||
|             'storage/upload/css' | ||||
|         $storageDirs = array_merge( | ||||
|             array('storage'), | ||||
|             $this->storageSubdirs | ||||
|         ); | ||||
| 
 | ||||
|         $allWritable = true; | ||||
|         foreach ($storageDirs as $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)) { | ||||
|                 // Try to create the directory
 | ||||
|                 $created = @mkdir($path, 0770, true); | ||||
|                 if ($created) { | ||||
|                     $this->addCheck( | ||||
| @ -223,27 +276,18 @@ class Prerequisites { | ||||
|                         "Could not create directory: {$dir}", | ||||
|                         'error' | ||||
|                     ); | ||||
|                     $allWritable = false; | ||||
|                     continue; | ||||
|                     $allCreated = false; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             $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; | ||||
|             } else { | ||||
|                 $this->addCheck( | ||||
|                     "Storage Directory: {$dir}", | ||||
|                     true, | ||||
|                     "Already exists" | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return $allWritable; | ||||
|         return $allCreated; | ||||
|     } | ||||
| 
 | ||||
|     private function checkWebServerConfig() { | ||||
| @ -283,73 +327,19 @@ class Prerequisites { | ||||
|         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() { | ||||
|         $dbFile = $this->baseDir . '/storage/db/tkr.sqlite'; | ||||
|         $dbDir = dirname($dbFile); | ||||
| 
 | ||||
|         if (!is_dir($dbDir)) { | ||||
|             $created = @mkdir($dbDir, 0770, true); | ||||
|             if (!$created) { | ||||
|                 $this->addCheck( | ||||
|                     'Database Directory', | ||||
|                     false, | ||||
|                     'Could not create storage/db directory', | ||||
|                     'error' | ||||
|                 ); | ||||
|                 return false; | ||||
|             } | ||||
|             $this->addCheck( | ||||
|                 'Database Directory', | ||||
|                 false, | ||||
|                 'Database directory does not exist', | ||||
|                 'error' | ||||
|             ); | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         $canCreateDb = is_writable($dbDir); | ||||
| @ -370,19 +360,55 @@ class Prerequisites { | ||||
|                 $dbReadable && $dbWritable ? 'Exists and is accessible' : 'Exists but has permission issues', | ||||
|                 $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 { | ||||
|             $this->addCheck( | ||||
|                 'Database File', | ||||
|                 true, | ||||
|                 'Will be created on first run' | ||||
|                 $canCreateDb, | ||||
|                 $canCreateDb ? 'Will be created during setup' : 'Cannot create - directory not writable', | ||||
|                 $canCreateDb ? 'info' : 'error' | ||||
|             ); | ||||
|             return $canCreateDb; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|         if (!$canCreateDb) { | ||||
|             return false; | ||||
|         } | ||||
|     private function createDatabase() { | ||||
|         $dbFile = $this->baseDir . '/storage/db/tkr.sqlite'; | ||||
| 
 | ||||
|         // Test database connection
 | ||||
|         // Test database connection (will create file if needed)
 | ||||
|         try { | ||||
|             $db = new PDO("sqlite:" . $dbFile); | ||||
|             $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); | ||||
| @ -400,8 +426,8 @@ class Prerequisites { | ||||
|             // Store working database connection
 | ||||
|             $this->database = $db; | ||||
| 
 | ||||
|             // Test migrations
 | ||||
|             return $this->checkMigrations($db); | ||||
|             // Run migrations
 | ||||
|             return $this->applyMigrations($db); | ||||
| 
 | ||||
|         } catch (PDOException $e) { | ||||
|             $this->addCheck( | ||||
| @ -414,7 +440,7 @@ class Prerequisites { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private function checkMigrations($db) { | ||||
|     private function applyMigrations($db) { | ||||
|         try { | ||||
|             $migrator = new Migrator($db); | ||||
|             $migrator->migrate(); | ||||
| @ -437,23 +463,20 @@ class Prerequisites { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // validate prereqs
 | ||||
|     // runs on each request and can be run from CLI
 | ||||
|     public function validate(): bool { | ||||
|         $this->log("=== tkr prerequisites check started at " . date('Y-m-d H:i:s') . " ===", true); | ||||
|     // Validate system requirements that can't be fixed by the script
 | ||||
|     public function validateSystem(): bool { | ||||
|         $this->log("=== tkr system validation started at " . date('Y-m-d H:i:s') . " ===", true); | ||||
| 
 | ||||
|         if ($this->isCli) { | ||||
|             $this->log("\n🔍 Validating prerequisites...\n"); | ||||
|             $this->log("\n🔍 Validating system requirements...\n"); | ||||
|         } | ||||
| 
 | ||||
|         $results = array( | ||||
|             'php_version' => $this->checkPhpVersion(), | ||||
|             'critical_extensions' => $this->checkRequiredExtensions(), | ||||
|             'directory_structure' => $this->checkDirectoryStructure(), | ||||
|             'storage_permissions' => $this->checkStoragePermissions(), | ||||
|             'web_server' => $this->checkWebServerConfig(), | ||||
|             'configuration' => $this->checkConfiguration(), | ||||
|             'database' => $this->checkDatabase() | ||||
|             'existing_storage_permissions' => $this->checkExistingStoragePermissions(), | ||||
|             'web_server' => $this->checkWebServerConfig() | ||||
|         ); | ||||
| 
 | ||||
|         // Check recommended extensions too
 | ||||
| @ -467,6 +490,44 @@ class Prerequisites { | ||||
|         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 | ||||
|      */ | ||||
|  | ||||
| @ -20,8 +20,8 @@ class Router { | ||||
|         ['logout', 'AuthController@handleLogout', ['GET', 'POST']], | ||||
|         ['mood', 'MoodController'], | ||||
|         ['mood', 'MoodController@handlePost', ['POST']], | ||||
|         ['setup', 'AdminController@showSetup'], | ||||
|         ['setup', 'AdminController@handleSetup', ['POST']], | ||||
|         ['tkr-setup', 'AdminController@showSetup'], | ||||
|         ['tkr-setup', 'AdminController@handleSetup', ['POST']], | ||||
|         ['tick/{id}', 'TickController'], | ||||
|         ['css/custom/{filename}.css', 'CssController@serveCustomCss'], | ||||
|     ]; | ||||
|  | ||||
| @ -103,4 +103,42 @@ class Util { | ||||
| 
 | ||||
|         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, '/') | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @ -15,10 +15,7 @@ class ConfigModel { | ||||
| 
 | ||||
|     // Instance method that uses injected database
 | ||||
|     public function get(): self { | ||||
|         $init = require APP_ROOT . '/config/init.php'; | ||||
|         $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,
 | ||||
|                                    site_description, | ||||
|  | ||||
							
								
								
									
										214
									
								
								tkr-setup.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								tkr-setup.php
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user