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: | |         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 | ||||||
| @ -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 |  *  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) { | ||||||
|  | |||||||
| @ -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); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  | |||||||
| @ -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,39 +200,17 @@ 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; | ||||||
| 
 | 
 | ||||||
|             if (!is_dir($path)) { |             // Only check directories that exist - missing ones are handled in application validation
 | ||||||
|                 // Try to create the directory
 |             if (is_dir($path)) { | ||||||
|                 $created = @mkdir($path, 0770, true); |  | ||||||
|                 if ($created) { |  | ||||||
|                     $this->addCheck( |  | ||||||
|                         "Storage Directory: {$dir}", |  | ||||||
|                         true, |  | ||||||
|                         "Created with correct permissions (0770)" |  | ||||||
|                     ); |  | ||||||
|                 } else { |  | ||||||
|                     $this->addCheck( |  | ||||||
|                         "Storage Directory: {$dir}", |  | ||||||
|                         false, |  | ||||||
|                         "Could not create directory: {$dir}", |  | ||||||
|                         'error' |  | ||||||
|                     ); |  | ||||||
|                     $allWritable = false; |  | ||||||
|                     continue; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|                 $writable = is_writable($path); |                 $writable = is_writable($path); | ||||||
|                 $permissions = substr(sprintf('%o', fileperms($path)), -4); |                 $permissions = substr(sprintf('%o', fileperms($path)), -4); | ||||||
| 
 | 
 | ||||||
| @ -242,10 +225,71 @@ class Prerequisites { | |||||||
|                     $allWritable = false; |                     $allWritable = false; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         return $allWritable; |         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)) { | ||||||
|  |                 $created = @mkdir($path, 0770, true); | ||||||
|  |                 if ($created) { | ||||||
|  |                     $this->addCheck( | ||||||
|  |                         "Storage Directory: {$dir}", | ||||||
|  |                         true, | ||||||
|  |                         "Created with correct permissions (0770)" | ||||||
|  |                     ); | ||||||
|  |                 } else { | ||||||
|  |                     $this->addCheck( | ||||||
|  |                         "Storage Directory: {$dir}", | ||||||
|  |                         false, | ||||||
|  |                         "Could not create directory: {$dir}", | ||||||
|  |                         'error' | ||||||
|  |                     ); | ||||||
|  |                     $allCreated = false; | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 $this->addCheck( | ||||||
|  |                     "Storage Directory: {$dir}", | ||||||
|  |                     true, | ||||||
|  |                     "Already exists" | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $allCreated; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private function checkWebServerConfig() { |     private function checkWebServerConfig() { | ||||||
|         if ($this->isCli) { |         if ($this->isCli) { | ||||||
|             $this->addCheck( |             $this->addCheck( | ||||||
| @ -283,74 +327,20 @@ 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); |  | ||||||
|             if (!$created) { |  | ||||||
|             $this->addCheck( |             $this->addCheck( | ||||||
|                 'Database Directory', |                 'Database Directory', | ||||||
|                 false, |                 false, | ||||||
|                     'Could not create storage/db directory', |                 'Database directory does not exist', | ||||||
|                 'error' |                 'error' | ||||||
|             ); |             ); | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         $canCreateDb = is_writable($dbDir); |         $canCreateDb = is_writable($dbDir); | ||||||
|         $this->addCheck( |         $this->addCheck( | ||||||
| @ -370,18 +360,8 @@ 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' | ||||||
|             ); |             ); | ||||||
|         } else { |  | ||||||
|             $this->addCheck( |  | ||||||
|                 'Database File', |  | ||||||
|                 true, |  | ||||||
|                 'Will be created on first run' |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (!$canCreateDb) { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|  |             if ($dbReadable && $dbWritable) { | ||||||
|                 // Test database connection
 |                 // Test database connection
 | ||||||
|                 try { |                 try { | ||||||
|                     $db = new PDO("sqlite:" . $dbFile); |                     $db = new PDO("sqlite:" . $dbFile); | ||||||
| @ -400,8 +380,54 @@ class Prerequisites { | |||||||
|                     // Store working database connection
 |                     // Store working database connection
 | ||||||
|                     $this->database = $db; |                     $this->database = $db; | ||||||
| 
 | 
 | ||||||
|             // Test migrations
 |                     return true; | ||||||
|             return $this->checkMigrations($db); | 
 | ||||||
|  |                 } catch (PDOException $e) { | ||||||
|  |                     $this->addCheck( | ||||||
|  |                         'Database Connection', | ||||||
|  |                         false, | ||||||
|  |                         'Failed to connect: ' . $e->getMessage(), | ||||||
|  |                         'error' | ||||||
|  |                     ); | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             $this->addCheck( | ||||||
|  |                 'Database File', | ||||||
|  |                 $canCreateDb, | ||||||
|  |                 $canCreateDb ? 'Will be created during setup' : 'Cannot create - directory not writable', | ||||||
|  |                 $canCreateDb ? 'info' : 'error' | ||||||
|  |             ); | ||||||
|  |             return $canCreateDb; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function createDatabase() { | ||||||
|  |         $dbFile = $this->baseDir . '/storage/db/tkr.sqlite'; | ||||||
|  | 
 | ||||||
|  |         // Test database connection (will create file if needed)
 | ||||||
|  |         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; | ||||||
|  | 
 | ||||||
|  |             // Run migrations
 | ||||||
|  |             return $this->applyMigrations($db); | ||||||
| 
 | 
 | ||||||
|         } catch (PDOException $e) { |         } catch (PDOException $e) { | ||||||
|             $this->addCheck( |             $this->addCheck( | ||||||
| @ -414,7 +440,7 @@ class Prerequisites { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private function checkMigrations($db) { |     private function applyMigrations($db) { | ||||||
|         try { |         try { | ||||||
|             $migrator = new Migrator($db); |             $migrator = new Migrator($db); | ||||||
|             $migrator->migrate(); |             $migrator->migrate(); | ||||||
| @ -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 | ||||||
|      */ |      */ | ||||||
|  | |||||||
| @ -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'], | ||||||
|     ]; |     ]; | ||||||
|  | |||||||
| @ -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, '/') | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
| } | } | ||||||
| @ -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
									
								
							
							
						
						
									
										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