Add Prerequisite class and cli script to confirm prereqs. (#30)
Add a Prerequisite class to manage all initilaization prerequisites. Invoke before each request. Provide a CLI script to allow checking Prereqs from command line. Add CI workflow to confirm prereq validation behavior. Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/30 Co-authored-by: Greg Sarjeant <greg@subcultureofone.org> Co-committed-by: Greg Sarjeant <greg@subcultureofone.org>
This commit is contained in:
		
							parent
							
								
									4255f46fc7
								
							
						
					
					
						commit
						6337fa2dfb
					
				
							
								
								
									
										163
									
								
								.gitea/workflows/prerequisites.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								.gitea/workflows/prerequisites.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,163 @@ | ||||
| name: Prerequisites Testing | ||||
| on: [pull_request] | ||||
| 
 | ||||
| jobs: | ||||
|   test-php-version-requirements: | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         php: ['7.4', '8.1', '8.2', '8.3'] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
| 
 | ||||
|       - name: Setup PHP ${{ matrix.php }} | ||||
|         uses: shivammathur/setup-php@v2 | ||||
|         with: | ||||
|           php-version: ${{ matrix.php }} | ||||
|           extensions: pdo,pdo_sqlite | ||||
| 
 | ||||
|       - name: Test PHP version requirement | ||||
|         run: | | ||||
|           if [[ "${{ matrix.php }}" < "8.2" ]]; then | ||||
|             echo "Testing PHP ${{ matrix.php }} - should fail" | ||||
|             if php check-prerequisites.php; 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 | ||||
|             echo "✓ Correctly passed with supported PHP version" | ||||
|           fi | ||||
| 
 | ||||
|   test-extension-progression: | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         php: ['8.2', '8.3'] | ||||
|         container: ['fedora:39', 'debian:bookworm', 'alpine:latest'] | ||||
|     container: ${{ matrix.container }} | ||||
| 
 | ||||
|     steps: | ||||
|       - name: Install Node.js fnd git or actions | ||||
|         run: | | ||||
|           if [ -f /etc/fedora-release ]; then | ||||
|             dnf install -y nodejs npm git | ||||
|           elif [ -f /etc/alpine-release ]; then | ||||
|             apk add --no-cache nodejs npm git | ||||
|           else | ||||
|             apt-get update && apt-get install -y nodejs npm git | ||||
|           fi | ||||
| 
 | ||||
|       - uses: actions/checkout@v3 | ||||
| 
 | ||||
|       - name: Install base PHP only | ||||
|         run: | | ||||
|           if [ -f /etc/fedora-release ]; then | ||||
|             dnf --setopt=install_weak_deps=False install -y php php-cli | ||||
|           elif [ -f /etc/alpine-release ]; then | ||||
|             apk add --no-cache php82 | ||||
|             ln -s /usr/bin/php82 /usr/bin/php | ||||
|           else | ||||
|             apt-get update && apt-get install -y php | ||||
|           fi | ||||
| 
 | ||||
|       - name: Test failure with missing extensions | ||||
|         run: | | ||||
|           echo "Testing with base PHP - should fail" | ||||
|           if php check-prerequisites.php; then | ||||
|             echo "ERROR: Should have failed with missing extensions" | ||||
|             exit 1 | ||||
|           fi | ||||
|           echo "✓ Correctly failed with missing extensions" | ||||
| 
 | ||||
|       - name: Install PDO extension | ||||
|         run: | | ||||
|           if [ -f /etc/fedora-release ]; then | ||||
|             echo "Not installing PDO on fedora because it includes SQLite support." | ||||
|             echo "Will install in subsequent test so this step fails as expected." | ||||
|           elif [ -f /etc/alpine-release ]; then | ||||
|             apk add --no-cache php82-pdo | ||||
|           else | ||||
|             apt-get install -y php-pdo | ||||
|           fi | ||||
| 
 | ||||
|       - name: Test still fails without SQLite | ||||
|         run: | | ||||
|           echo "Testing with PDO but no SQLite - should still fail" | ||||
|           if php check-prerequisites.php; then | ||||
|             echo "ERROR: Should have failed without SQLite" | ||||
|             exit 1 | ||||
|           fi | ||||
|           echo "✓ Correctly failed without SQLite extension" | ||||
| 
 | ||||
|       - name: Install SQLite extension | ||||
|         run: | | ||||
|           if [ -f /etc/fedora-release ]; then | ||||
|             dnf --setopt=install_weak_deps=False install -y php-pdo | ||||
|           elif [ -f /etc/alpine-release ]; then | ||||
|             apk add --no-cache php82-pdo_sqlite | ||||
|           else | ||||
|             apt-get install -y php-sqlite3 | ||||
|           fi | ||||
| 
 | ||||
|       - name: Test now passes with required extensions | ||||
|         run: | | ||||
|           echo "Testing with all required extensions - should pass" | ||||
|           php check-prerequisites.php | ||||
|           echo "✓ All required extensions detected correctly" | ||||
| 
 | ||||
|       - name: Install recommended extensions and retest | ||||
|         run: | | ||||
|           if [ -f /etc/fedora-release ]; then | ||||
|             dnf install -y php-mbstring php-curl | ||||
|           elif [ -f /etc/alpine-release ]; then | ||||
|             apk add --no-cache php82-mbstring php82-curl | ||||
|           else | ||||
|             apt-get install -y php-mbstring php-curl | ||||
|           fi | ||||
|           php check-prerequisites.php | ||||
|           echo "✓ Recommended extensions also detected" | ||||
| 
 | ||||
|   test-permission-scenarios: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
| 
 | ||||
|       - name: Setup PHP | ||||
|         uses: shivammathur/setup-php@v2 | ||||
|         with: | ||||
|           php-version: '8.2' | ||||
|           extensions: pdo,pdo_sqlite | ||||
| 
 | ||||
|       - name: Test with unwritable storage directory | ||||
|         run: | | ||||
|           # Create a non-root user for testing | ||||
|           useradd -m -s /bin/bash testuser | ||||
| 
 | ||||
|           # Make storage unwritable by non-root | ||||
|           mkdir -p storage | ||||
|           chmod 444 storage | ||||
|           chown root:root storage | ||||
| 
 | ||||
|           # Run as the non-root user - should fail | ||||
|           if su testuser -c "php check-prerequisites.php"; then | ||||
|             echo "ERROR: Should have failed with unwritable storage" | ||||
|             exit 1 | ||||
|           fi | ||||
|           echo "✓ Correctly failed with unwritable storage" | ||||
| 
 | ||||
|           # Restore permissions for cleanup | ||||
|           chmod 755 storage | ||||
| 
 | ||||
|       - name: Test with missing directories | ||||
|         run: | | ||||
|           # Remove required directories | ||||
|           rm -rf src templates config | ||||
| 
 | ||||
|           # Should fail | ||||
|           if php check-prerequisites.php; then | ||||
|             echo "ERROR: Should have failed with missing directories" | ||||
|             exit 1 | ||||
|           fi | ||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -15,4 +15,7 @@ storage/upload/css | ||||
| scratch | ||||
| 
 | ||||
| # Build artifacts | ||||
| tkr.tgz | ||||
| tkr.tgz | ||||
| 
 | ||||
| # Test logs | ||||
| storage/prerequisite-check.log | ||||
| @ -1,4 +1,5 @@ | ||||
| # tkr | ||||
|  | ||||
|  | ||||
| 
 | ||||
| A lightweight, HTML-only status feed for self-hosted personal websites. Written in PHP. Heavily inspired by [status.cafe](https://status.cafe). | ||||
|  | ||||
							
								
								
									
										21
									
								
								check-prerequisites.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								check-prerequisites.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| #!/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); | ||||
| } | ||||
| @ -9,10 +9,9 @@ define('CONFIG_DIR', APP_ROOT . '/config'); | ||||
| define('SRC_DIR', APP_ROOT . '/src'); | ||||
| define('STORAGE_DIR', APP_ROOT . '/storage'); | ||||
| define('TEMPLATES_DIR', APP_ROOT . '/templates'); | ||||
| define('TICKS_DIR', STORAGE_DIR . '/ticks'); | ||||
| define('DATA_DIR', STORAGE_DIR . '/db'); | ||||
| define('CSS_UPLOAD_DIR', STORAGE_DIR . '/upload/css'); | ||||
| define('DB_FILE', DATA_DIR . '/tkr.sqlite'); | ||||
| define('CSS_UPLOAD_DIR', STORAGE_DIR . '/upload/css'); | ||||
| 
 | ||||
| // Janky autoloader function
 | ||||
| // This is a bit more consistent with current frameworks
 | ||||
|  | ||||
| @ -14,20 +14,27 @@ if (preg_match('/\.php$/', $path)) { | ||||
| // Define base paths and load classes
 | ||||
| include_once(dirname(dirname(__FILE__)) . "/config/bootstrap.php"); | ||||
| 
 | ||||
| // validate that necessary directories exist and are writable
 | ||||
| $fsMgr = new Filesystem(); | ||||
| $fsMgr->validate(); | ||||
| // Check prerequisites.
 | ||||
| $prerequisites = new Prerequisites(); | ||||
| $results = $prerequisites->validate(); | ||||
| if (count($prerequisites->getErrors()) > 0) { | ||||
|     $prerequisites->generateWebSummary($results); | ||||
|     exit; | ||||
| } | ||||
| 
 | ||||
| // do any necessary database migrations
 | ||||
| // Do any necessary database migrations
 | ||||
| $dbMgr = new Database(); | ||||
| $dbMgr->migrate(); | ||||
| 
 | ||||
| // Make sure the initial setup is complete
 | ||||
| // unless we're already heading to setup
 | ||||
| //
 | ||||
| // TODO: Consider simplifying this.
 | ||||
| // Might not need the custom exception now that the prereq checker is more robust.
 | ||||
| if (!(preg_match('/setup$/', $path))) { | ||||
|     try { | ||||
|         // database validation
 | ||||
|         $dbMgr->validate(); | ||||
|         // Make sure setup has been completed
 | ||||
|         $dbMgr->confirmSetup(); | ||||
|     } catch (SetupException $e) { | ||||
|         $e->handle(); | ||||
|         exit; | ||||
|  | ||||
| @ -119,7 +119,7 @@ class Database{ | ||||
|     } | ||||
| 
 | ||||
|     // make sure tables that need to be seeded have been
 | ||||
|     private function validateTableContents(): void { | ||||
|     public function confirmSetup(): void { | ||||
|         $db = self::get(); | ||||
| 
 | ||||
|         // make sure required tables (user, settings) are populated
 | ||||
|  | ||||
| @ -13,13 +13,8 @@ class SetupException extends Exception { | ||||
|     // but this is a very specific case.
 | ||||
|     public function handle(){ | ||||
|         switch ($this->setupIssue){ | ||||
|             case 'storage_missing': | ||||
|             case 'storage_permissions': | ||||
|             case 'directory_creation': | ||||
|             case 'directory_permissions': | ||||
|             case 'database_connection': | ||||
|             case 'load_classes': | ||||
|             case 'table_creation': | ||||
|             case 'db_migration': | ||||
|                 // Unrecoverable errors.
 | ||||
|                 // Show error message and exit
 | ||||
|                 http_response_code(500); | ||||
| @ -31,7 +26,7 @@ class SetupException extends Exception { | ||||
|                 // Redirect to setup if we aren't already headed there.
 | ||||
|                 // NOTE: Just read directly from init.php instead of
 | ||||
|                 //       trying to use the config object. This is the initial
 | ||||
|                 //       setup. It shouldn't assume anything can be loaded.
 | ||||
|                 //       setup. It shouldn't assume any data can be loaded.
 | ||||
|                 $init = require APP_ROOT . '/config/init.php'; | ||||
|                 $currentPath = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/'); | ||||
| 
 | ||||
|  | ||||
| @ -1,54 +0,0 @@ | ||||
| <?php | ||||
| // Validates that required directories exists
 | ||||
| // and files have correct formats
 | ||||
| class Filesystem { | ||||
|     public function validate(): void{ | ||||
|         $this->validateStorageDir(); | ||||
|         $this->validateStorageSubdirs(); | ||||
|     } | ||||
| 
 | ||||
|     // Make sure the storage/ directory exists and is writable
 | ||||
|     private function validateStorageDir(): void{ | ||||
|         if (!is_dir(STORAGE_DIR)) { | ||||
|             throw new SetupException( | ||||
|                 STORAGE_DIR . "does not exist. Please check your installation.", | ||||
|                 'storage_missing' | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (!is_writable(STORAGE_DIR)) { | ||||
|             throw new SetupException( | ||||
|                 STORAGE_DIR . "is not writable. Exiting.", | ||||
|                 'storage_permissions' | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // validate that the required storage subdirectories exist
 | ||||
|     // attempt to create them if they don't
 | ||||
|     private function validateStorageSubdirs(): void { | ||||
|         $storageSubdirs = array(); | ||||
|         $storageSubdirs[] = CSS_UPLOAD_DIR; | ||||
|         $storageSubdirs[] = DATA_DIR; | ||||
| 
 | ||||
|         foreach($storageSubdirs as $storageSubdir){ | ||||
|             if (!is_dir($storageSubdir)) { | ||||
|                 if (!mkdir($storageSubdir, 0770, true)) { | ||||
|                     throw new SetupException( | ||||
|                         "Failed to create required directory: $dir", | ||||
|                         'directory_creation' | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (!is_writable($storageSubdir)) { | ||||
|                 if (!chmod($storageSubdir, 0770)) { | ||||
|                     throw new SetupException( | ||||
|                         "Required directory is not writable: $dir", | ||||
|                         'directory_permissions' | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										536
									
								
								src/Framework/Prerequisites/Prerequisites.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										536
									
								
								src/Framework/Prerequisites/Prerequisites.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,536 @@ | ||||
| <?php | ||||
| /** | ||||
|  * tkr Prerequisites Checker | ||||
|  * | ||||
|  * 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 { | ||||
|     private $checks = array(); | ||||
|     private $warnings = array(); | ||||
|     private $errors = array(); | ||||
|     private $baseDir; | ||||
|     private $logFile; | ||||
|     private $isCli; | ||||
|     private $isWeb; | ||||
| 
 | ||||
|     public function __construct() { | ||||
|         $this->isCli = php_sapi_name() === 'cli'; | ||||
|         $this->isWeb = !$this->isCli && isset($_SERVER['HTTP_HOST']); | ||||
|         $this->baseDir = APP_ROOT; | ||||
|         $this->logFile = $this->baseDir . '/storage/prerequisite-check.log'; | ||||
| 
 | ||||
|         if ($this->isWeb) { | ||||
|             header('Content-Type: text/html; charset=utf-8'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** Log validation output | ||||
|      * | ||||
|      * This introduces a chicken-and-egg problem, because | ||||
|      * if the storage directory isn't writable, this will fail. | ||||
|      * In that case, I'll just write to stdout. | ||||
|      * | ||||
|      */ | ||||
|     private function log($message, $overwrite=false) { | ||||
|         $logDir = dirname($this->logFile); | ||||
|         //print("Log dir: {$logDir}");
 | ||||
|         if (!is_dir($logDir)) { | ||||
|             if (!@mkdir($logDir, 0770, true)) { | ||||
|                 // Can't create storage dir - just output, don't log to file
 | ||||
|                 if ($this->isCli) { | ||||
|                     echo $message . "\n"; | ||||
|                 } | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         $flags = LOCK_EX; | ||||
|         if (!$overwrite) { | ||||
|             $flags |= FILE_APPEND; | ||||
|         } | ||||
| 
 | ||||
|         // Try to write to log file
 | ||||
|         if (@file_put_contents($this->logFile, $message . "\n", $flags) === false) { | ||||
|             // Logging failed, but continue - just output to CLI if possible
 | ||||
|             if ($this->isCli) { | ||||
|                 echo "Warning: Could not write to log file\n"; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if ($this->isCli) { | ||||
|             echo $message . "\n"; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private function addCheck($name, $status, $message, $severity = 'info') { | ||||
|         $this->checks[] = array( | ||||
|             'name' => $name, | ||||
|             'status' => $status, | ||||
|             'message' => $message, | ||||
|             'severity' => $severity | ||||
|         ); | ||||
| 
 | ||||
|         if ($severity === 'error') { | ||||
|             $this->errors[] = $message; | ||||
|         } elseif ($severity === 'warning') { | ||||
|             $this->warnings[] = $message; | ||||
|         } | ||||
| 
 | ||||
|         $statusIcon = $status ? '✓' : '✗'; | ||||
|         $this->log("[{$statusIcon}] {$name}: {$message}"); | ||||
|     } | ||||
| 
 | ||||
|     private function checkPhpVersion() { | ||||
|         $minVersion = '8.2.0'; | ||||
|         $currentVersion = PHP_VERSION; | ||||
|         $versionOk = version_compare($currentVersion, $minVersion, '>='); | ||||
| 
 | ||||
|         if ($versionOk) { | ||||
|             $this->addCheck( | ||||
|                 'PHP Version', | ||||
|                 true, | ||||
|                 "PHP {$currentVersion} (meets minimum requirement of {$minVersion})" | ||||
|             ); | ||||
|         } else { | ||||
|             $this->addCheck( | ||||
|                 'PHP Version', | ||||
|                 false, | ||||
|                 "PHP {$currentVersion} is below minimum requirement of {$minVersion}", | ||||
|                 'error' | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return $versionOk; | ||||
|     } | ||||
| 
 | ||||
|     private function checkRequiredExtensions() { | ||||
|         $requiredExtensions = array('PDO', 'pdo_sqlite'); | ||||
| 
 | ||||
|         $allRequired = true; | ||||
|         foreach ($requiredExtensions as $ext) { | ||||
|             $loaded = extension_loaded($ext); | ||||
|             $this->addCheck( | ||||
|                 "PHP Extension: {$ext}", | ||||
|                 $loaded, | ||||
|                 $loaded ? 'Available' : "Missing (REQUIRED) - {$ext}", | ||||
|                 $loaded ? 'info' : 'error' | ||||
|             ); | ||||
|             if (!$loaded) { | ||||
|                 $allRequired = false; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return $allRequired; | ||||
|     } | ||||
| 
 | ||||
|     private function checkRecommendedExtensions() { | ||||
|         $recommendedExtensions = array('mbstring', 'curl', 'fileinfo', 'session'); | ||||
| 
 | ||||
|         foreach ($recommendedExtensions as $ext) { | ||||
|             $loaded = extension_loaded($ext); | ||||
|             $this->addCheck( | ||||
|                 "PHP Extension: {$ext}", | ||||
|                 $loaded, | ||||
|                 $loaded ? 'Available' : "Missing (recommended) - {$ext}", | ||||
|                 $loaded ? 'info' : 'warning' | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private function checkDirectoryStructure() { | ||||
|         $baseDir = $this->baseDir; | ||||
|         $requiredDirs = array( | ||||
|             'config' => 'Configuration files', | ||||
|             'public' => 'Web server document root', | ||||
|             'src' => 'Application source code', | ||||
|             'storage' => 'Data storage (must be writable)', | ||||
|             'templates' => 'Template files' | ||||
|         ); | ||||
| 
 | ||||
|         $allPresent = true; | ||||
|         foreach ($requiredDirs as $dir => $description) { | ||||
|             $path = $baseDir . '/' . $dir; | ||||
|             $exists = is_dir($path); | ||||
|             $this->addCheck( | ||||
|                 "Directory: {$dir}", | ||||
|                 $exists, | ||||
|                 $exists ? "Present - {$description}" : "Missing - {$description} at {$path}", | ||||
|                 $exists ? 'info' : 'error' | ||||
|             ); | ||||
|             if (!$exists) { | ||||
|                 $allPresent = false; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return $allPresent; | ||||
|     } | ||||
| 
 | ||||
|     private function checkStoragePermissions() { | ||||
|         $storageDirs = array( | ||||
|             'storage', | ||||
|             'storage/db', | ||||
|             'storage/upload', | ||||
|             'storage/upload/css' | ||||
|         ); | ||||
| 
 | ||||
|         $allWritable = 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( | ||||
|                         "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); | ||||
|             $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 checkWebServerConfig() { | ||||
|         if ($this->isCli) { | ||||
|             $this->addCheck( | ||||
|                 'Web Server Test', | ||||
|                 false, | ||||
|                 'Cannot test web server configuration from CLI - run via web browser', | ||||
|                 'warning' | ||||
|             ); | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         // Check if we're being served from the correct document root
 | ||||
|         $documentRoot = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : ''; | ||||
|         $expectedPath = realpath($this->baseDir . '/public'); | ||||
|         $correctRoot = ($documentRoot === $expectedPath); | ||||
| 
 | ||||
|         $this->addCheck( | ||||
|             'Document Root', | ||||
|             $correctRoot, | ||||
|             $correctRoot ? | ||||
|                 "Correctly set to {$expectedPath}" : | ||||
|                 "Should be {$expectedPath}, currently {$documentRoot}", | ||||
|             $correctRoot ? 'info' : 'warning' | ||||
|         ); | ||||
| 
 | ||||
|         // Check for URL rewriting
 | ||||
|         $rewriteWorking = isset($_SERVER['REQUEST_URI']); | ||||
|         $this->addCheck( | ||||
|             'URL Rewriting', | ||||
|             $rewriteWorking, | ||||
|             $rewriteWorking ? 'Available' : 'May not be properly configured', | ||||
|             $rewriteWorking ? 'info' : 'warning' | ||||
|         ); | ||||
| 
 | ||||
|         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; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         $canCreateDb = is_writable($dbDir); | ||||
|         $this->addCheck( | ||||
|             'Database Directory', | ||||
|             $canCreateDb, | ||||
|             $canCreateDb ? 'Writable - can create database' : 'Not writable - cannot create database', | ||||
|             $canCreateDb ? 'info' : 'error' | ||||
|         ); | ||||
| 
 | ||||
|         if (file_exists($dbFile)) { | ||||
|             $dbReadable = is_readable($dbFile); | ||||
|             $dbWritable = is_writable($dbFile); | ||||
| 
 | ||||
|             $this->addCheck( | ||||
|                 'Database File', | ||||
|                 $dbReadable && $dbWritable, | ||||
|                 $dbReadable && $dbWritable ? 'Exists and is accessible' : 'Exists but has permission issues', | ||||
|                 $dbReadable && $dbWritable ? 'info' : 'error' | ||||
|             ); | ||||
|         } else { | ||||
|             $this->addCheck( | ||||
|                 'Database File', | ||||
|                 true, | ||||
|                 'Will be created on first run' | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return $canCreateDb; | ||||
|     } | ||||
| 
 | ||||
|     // validate prereqs
 | ||||
|     // runs on each request and can be run from CLI
 | ||||
|     public function validate() { | ||||
|         $this->log("=== tkr prerequisites check started at " . date('Y-m-d H:i:s') . " ===", true); | ||||
| 
 | ||||
|         if ($this->isCli) { | ||||
|             $this->log("\n🔍 Validating prerequisites...\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() | ||||
|         ); | ||||
| 
 | ||||
|         // Check recommended extensions too
 | ||||
|         $this->checkRecommendedExtensions(); | ||||
| 
 | ||||
|         if ($this->isCli) { | ||||
|             $this->generateCliSummary($results); | ||||
|         } | ||||
| 
 | ||||
|         return $results; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Display web-friendly error page when minimum requirements aren't met | ||||
|      */ | ||||
|     public function generateWebSummary() { | ||||
|         echo '<!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>tkr - Setup Required</title> | ||||
|     <style> | ||||
|         body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 2rem; line-height: 1.6; background: #f8f9fa; }
 | ||||
|         .container { max-width: 800px; margin: 0 auto; background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } | ||||
|         .header { text-align: center; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 2px solid #dee2e6; }
 | ||||
|         .header h1 { color: #dc3545; margin: 0; }
 | ||||
|         .header p { color: #6c757d; margin: 0.5rem 0 0 0; }
 | ||||
|         .error-item { margin: 1rem 0; padding: 1rem; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; border-left: 4px solid #dc3545; }
 | ||||
|         .warning-item { margin: 1rem 0; padding: 1rem; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px; border-left: 4px solid #ffc107; }
 | ||||
|         .error-title { font-weight: 600; color: #721c24; margin-bottom: 0.5rem; }
 | ||||
|         .warning-title { font-weight: 600; color: #856404; margin-bottom: 0.5rem; }
 | ||||
|         .resolution { margin-top: 2rem; padding: 1rem; background: #e9ecef; border-radius: 4px; }
 | ||||
|         .resolution h3 { margin-top: 0; color: #495057; }
 | ||||
|         .resolution ul { margin: 0; } | ||||
|         .resolution li { margin: 0.5rem 0; } | ||||
|         .log-info { margin-top: 2rem; padding: 1rem; background: #f8f9fa; border-radius: 4px; font-size: 0.9em; color: #6c757d; }
 | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <div class="container"> | ||||
|         <div class="header"> | ||||
|             <h1>⚠️ Setup Required</h1> | ||||
|             <p>tkr cannot start due to system configuration issues</p> | ||||
|         </div>'; | ||||
| 
 | ||||
|         $hasErrors = false; | ||||
|         $hasWarnings = false; | ||||
| 
 | ||||
|         // Display errors
 | ||||
|         foreach ($this->checks as $check) { | ||||
|             if (!$check['status'] && $check['severity'] === 'error') { | ||||
|                 if (!$hasErrors) { | ||||
|                     echo '<h2>Critical Issues</h2>'; | ||||
|                     $hasErrors = true; | ||||
|                 } | ||||
|                 echo '<div class="error-item"> | ||||
|                     <div class="error-title">✗ ' . htmlspecialchars($check['name']) . '</div> | ||||
|                     ' . htmlspecialchars($check['message']) . ' | ||||
|                 </div>'; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Display warnings
 | ||||
|         foreach ($this->checks as $check) { | ||||
|             if (!$check['status'] && $check['severity'] === 'warning') { | ||||
|                 if (!$hasWarnings) { | ||||
|                     echo '<h2>Warnings</h2>'; | ||||
|                     $hasWarnings = true; | ||||
|                 } | ||||
|                 echo '<div class="warning-item"> | ||||
|                     <div class="warning-title">⚠ ' . htmlspecialchars($check['name']) . '</div> | ||||
|                     ' . htmlspecialchars($check['message']) . ' | ||||
|                 </div>'; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Resolution steps
 | ||||
|         echo '<div class="resolution"> | ||||
|             <h3>How to Fix These Issues</h3> | ||||
|             <ul>'; | ||||
| 
 | ||||
|         if (!version_compare(PHP_VERSION, '8.2.0', '>=')) { | ||||
|             echo '<li><strong>PHP Version:</strong> Contact your hosting provider to upgrade PHP to version 8.2 or higher</li>'; | ||||
|         } | ||||
| 
 | ||||
|         if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) { | ||||
|             echo '<li><strong>SQLite Support:</strong> Contact your hosting provider to enable PDO and PDO_SQLITE extensions</li>'; | ||||
|         } | ||||
| 
 | ||||
|         if (count($this->errors) > 0) { | ||||
|             echo '<li><strong>File Permissions:</strong> Ensure the storage directory and subdirectories are writable by the web server</li>'; | ||||
|             echo '<li><strong>Missing Directories:</strong> Upload the complete tkr application with all required directories</li>'; | ||||
|         } | ||||
| 
 | ||||
|         echo '    </ul> | ||||
|             <p><strong>Need Help?</strong> Check the tkr documentation or contact your hosting provider with the error details above.</p> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="log-info"> | ||||
|             <p><strong>Technical Details:</strong> Full diagnostic information has been logged to ' . htmlspecialchars($this->logFile) . '</p> | ||||
|             <p><strong>Check Time:</strong> ' . date('Y-m-d H:i:s') . '</p> | ||||
|         </div> | ||||
|     </div> | ||||
| </body> | ||||
| </html>'; | ||||
|     } | ||||
| 
 | ||||
|     private function generateCliSummary($results) { | ||||
|         $this->log("\n" . str_repeat("=", 60)); | ||||
|         $this->log("PREREQUISITE CHECK SUMMARY"); | ||||
|         $this->log(str_repeat("=", 60)); | ||||
| 
 | ||||
|         $totalChecks = count($this->checks); | ||||
|         $passedChecks = 0; | ||||
|         foreach ($this->checks as $check) { | ||||
|             if ($check['status']) { | ||||
|                 $passedChecks++; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         $this->log("Total checks: {$totalChecks}"); | ||||
|         $this->log("Passed: {$passedChecks}"); | ||||
|         $this->log("Errors: " . count($this->errors)); | ||||
|         $this->log("Warnings: " . count($this->warnings)); | ||||
| 
 | ||||
|         if (count($this->errors) === 0) { | ||||
|             $this->log("\n✅ ALL PREREQUISITES SATISFIED"); | ||||
|             $this->log("tkr should install and run successfully."); | ||||
|         } else { | ||||
|             $this->log("\n❌ CRITICAL ISSUES FOUND"); | ||||
|             $this->log("The following issues must be resolved before installing tkr:"); | ||||
|             foreach ($this->errors as $error) { | ||||
|                 $this->log("  • {$error}"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (count($this->warnings) > 0) { | ||||
|             $this->log("\n⚠️  WARNINGS:"); | ||||
|             foreach ($this->warnings as $warning) { | ||||
|                 $this->log("  • {$warning}"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         $this->log("\n📝 Full log saved to: " . $this->logFile); | ||||
|         $this->log("=== Check completed at " . date('Y-m-d H:i:s') . " ==="); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get array of errors for external use | ||||
|      */ | ||||
|     public function getErrors() { | ||||
|         return $this->errors; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get array of warnings for external use | ||||
|      */ | ||||
|     public function getWarnings() { | ||||
|         return $this->warnings; | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user