File "ServerConfigController.php"

Full Path: /home/trinadezambia/public_html/admin_panel/app/Http/Controllers/ServerConfigController.php
File size: 14.49 KB
MIME-type: text/x-php
Charset: utf-8

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\SystemSetting;
use App\Repositories\SystemSetting\SystemSettingInterface;
use App\Services\CachingService;
use App\Services\ResponseService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use PDO;
use PDOException;
use Throwable;

class ServerConfigController extends Controller
{
    private SystemSettingInterface $systemSettings;
    private CachingService $cache;

    public function __construct(SystemSettingInterface $systemSettings, CachingService $cachingService)
    {
        $this->systemSettings = $systemSettings;
        $this->cache = $cachingService;
    }

    /**
     * Display the Server Configuration Check page.
     */
    public function index()
    {
        ResponseService::noPermissionThenRedirect('system-setting-manage');

        $dbCheck = $this->systemSettings->builder()
            ->where('name', 'server_config_db_checkMark')
            ->value('data');

        $queueCheck = $this->systemSettings->builder()
            ->where('name', 'server_config_queue_checkMark')
            ->value('data');

        $reverbCheck = $this->systemSettings->builder()
            ->where('name', 'server_config_reverb_checkMark')
            ->value('data');

        $checksPassedCount = (int)($dbCheck == 1) + (int)($queueCheck == 1) + (int)($reverbCheck == 1);

        if ($checksPassedCount == 3) {
            $this->saveCheckMark('server_config_wizard_checkMark', 1);
            return redirect()->route('dashboard');
        }

        $DBPassword = env('DB_PASSWORD');
        if (env('DEMO_MODE')) {
            $DBPassword = '**********';
        }

        $data = [
            'db_host' => env('DB_HOST'),
            'db_port' => env('DB_PORT'),
            'db_username' => env('DB_USERNAME'),
            'db_password' => $DBPassword,
        ];

        return view('server-config.index', compact('dbCheck', 'queueCheck', 'reverbCheck', 'checksPassedCount', 'data'));
    }

    /**
     * Test raw database connection + CREATE / DROP privileges.
     */
    public function testDatabasePrivileges(Request $request): JsonResponse
    {
        $request->validate([
            'db_host'     => ['required', 'string'],
            'db_port'     => ['required', 'numeric'],
            'db_username' => ['required', 'string'],
            'db_password' => ['nullable', 'string'],
        ]);

        $host     = $request->input('db_host');
        $port     = (int)$request->input('db_port');
        $username = $request->input('db_username');
        $password = $request->input('db_password', '');

        $logs = [];

        try {
            $logs[] = "[INFO] Connecting to {$host}:{$port} as '{$username}'...";

            $pdo = new PDO(
                "mysql:host={$host};port={$port};charset=utf8mb4",
                $username,
                $password,
                [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]
            );

            $logs[] = "[OK]   Connection established.";

            // Test CREATE privilege
            $testDb = 'eschool_privilege_test_' . time();
            $pdo->exec("CREATE DATABASE `{$testDb}`");
            $logs[] = "[OK]   CREATE DATABASE privilege confirmed.";

            // Test DROP privilege
            $pdo->exec("DROP DATABASE `{$testDb}`");
            $logs[] = "[OK]   DROP DATABASE privilege confirmed.";

            // Save check mark
            $this->saveCheckMark('server_config_db_checkMark', 1);
            $logs[] = "[OK]   Database root privileges check PASSED.";

            // update .env file
            changeEnv([
                'DB_HOST'     => $host,
                'DB_PORT'     => $port,
                'DB_USERNAME' => $username,
                'DB_PASSWORD' => $password,
            ]);

            while (ob_get_level()) {
                ob_end_clean();
            }
            return response()->json([
                'success' => true,
                'message' => 'Database root privileges verified successfully.',
                'logs'    => $logs,
            ]);
        } catch (PDOException $e) {
            $logs[] = "[ERROR] " . $e->getMessage();
            $this->saveCheckMark('server_config_db_checkMark', 0);

            while (ob_get_level()) {
                ob_end_clean();
            }
            return response()->json([
                'success' => false,
                'message' => 'Database connection or privilege check failed.',
                'logs'    => $logs,
            ]);
        } catch (Throwable $e) {
            $logs[] = "[ERROR] Unexpected error: " . $e->getMessage();
            $this->saveCheckMark('server_config_db_checkMark', 0);

            while (ob_get_level()) {
                ob_end_clean();
            }
            return response()->json([
                'success' => false,
                'message' => 'An unexpected error occurred.',
                'logs'    => $logs,
            ]);
        }
    }

    /**
     * Check if a Laravel queue worker is running.
     */
    public function testQueueWorker(Request $request): JsonResponse
    {
        $logs = [];
        $logs[] = "[INFO] Checking for active queue worker process...";

        try {
            $workerRunning = $this->isProcessRunning('queue:work', $logs);

            if ($workerRunning) {
                $logs[] = "[OK]   Queue worker process is active.";
                $logs[] = "[OK]   Queue Worker (Supervisor) check PASSED.";
                $this->saveCheckMark('server_config_queue_checkMark', 1);

                while (ob_get_level()) {
                    ob_end_clean();
                }
                return response()->json([
                    'success' => true,
                    'message' => 'Queue worker is running.',
                    'logs'    => $logs,
                ]);
            } else {
                $logs[] = "[WARN] No active 'queue:work' process found.";
                $logs[] = "[INFO] On production, ensure Supervisor is configured to run: php artisan queue:work";
                $logs[] = "[WARN] Queue Worker check: NOT RUNNING (configure Supervisor on production).";
                $this->saveCheckMark('server_config_queue_checkMark', 0);

                while (ob_get_level()) {
                    ob_end_clean();
                }
                return response()->json([
                    'success' => false,
                    'message' => 'No active queue worker detected. Configure Supervisor on production.',
                    'logs'    => $logs,
                ]);
            }
        } catch (Throwable $e) {
            $logs[] = "[ERROR] " . $e->getMessage();
            $this->saveCheckMark('server_config_queue_checkMark', 0);

            while (ob_get_level()) {
                ob_end_clean();
            }
            return response()->json([
                'success' => false,
                'message' => 'Could not determine queue worker status.',
                'logs'    => $logs,
            ]);
        }
    }

    /**
     * Check if a Laravel Reverb WebSocket server is running.
     */
    public function testReverbWorker(Request $request): JsonResponse
    {
        $logs = [];
        $logs[] = "[INFO] Checking for active Reverb (reverb:start) process...";

        try {
            $reverbRunning = $this->isProcessRunning('reverb:start', $logs);

            if ($reverbRunning) {
                $logs[] = "[OK]   Reverb process is active.";
                $logs[] = "[OK]   Laravel Reverb (Supervisor) check PASSED.";
                $this->saveCheckMark('server_config_reverb_checkMark', 1);

                while (ob_get_level()) {
                    ob_end_clean();
                }
                return response()->json([
                    'success' => true,
                    'message' => 'Reverb WebSocket server is running.',
                    'logs'    => $logs,
                ]);
            } else {
                $logs[] = "[WARN] No active 'reverb:start' process found.";
                $logs[] = "[INFO] On production, ensure Supervisor is configured to run: php artisan reverb:start";
                $logs[] = "[WARN] Reverb check: NOT RUNNING (configure Supervisor on production).";
                $this->saveCheckMark('server_config_reverb_checkMark', 0);

                while (ob_get_level()) {
                    ob_end_clean();
                }
                return response()->json([
                    'success' => false,
                    'message' => 'No active Reverb process detected. Configure Supervisor on production.',
                    'logs'    => $logs,
                ]);
            }
        } catch (Throwable $e) {
            $logs[] = "[ERROR] " . $e->getMessage();
            $this->saveCheckMark('server_config_reverb_checkMark', 0);

            while (ob_get_level()) {
                ob_end_clean();
            }
            return response()->json([
                'success' => false,
                'message' => 'Could not determine Reverb worker status.',
                'logs'    => $logs,
            ]);
        }
    }

    /**
     * Mark server configuration as fully complete and redirect to dashboard.
     */
    public function markComplete(): JsonResponse
    {
        $this->saveCheckMark('server_config_wizard_checkMark', 1);

        while (ob_get_level()) {
            ob_end_clean();
        }
        return response()->json([
            'success'  => true,
            'redirect' => route('dashboard'),
        ]);
    }

    /**
     * Run a system command safely using proc_open (avoids shell_exec restrictions).
     * Returns the combined stdout+stderr output, or null on failure.
     */
    private function runCommand(string $command): ?string
    {
        if (!function_exists('proc_open')) {
            return null;
        }

        $descriptors = [
            0 => ['pipe', 'r'],  // stdin
            1 => ['pipe', 'w'],  // stdout
            2 => ['pipe', 'w'],  // stderr
        ];

        $process = @proc_open($command, $descriptors, $pipes);
        if (!is_resource($process)) {
            return null;
        }

        fclose($pipes[0]);
        $output = stream_get_contents($pipes[1]);
        $stderr  = stream_get_contents($pipes[2]);
        fclose($pipes[1]);
        fclose($pipes[2]);
        proc_close($process);

        return ($output !== false ? $output : '') . ($stderr !== false ? $stderr : '');
    }

    /**
     * Check if a given artisan command keyword is running as a process.
     * Works on Windows (via WMIC/tasklist) and Linux/Mac (via ps).
     */
    private function isProcessRunning(string $keyword, array &$logs): bool
    {
        $isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';

        if ($isWindows) {
            // First check if any php.exe process exists
            $taskOutput = $this->runCommand('tasklist /FI "IMAGENAME eq php.exe" /FO CSV /NH');
            $logs[] = "[INFO] tasklist output received.";

            if ($taskOutput === null || stripos($taskOutput, 'php.exe') === false) {
                $logs[] = "[INFO] No php.exe processes found.";
                return false;
            }

            // Use WMIC to get command lines — falls back to false if unavailable
            $wmicOutput = $this->runCommand('wmic process where "name=\'php.exe\'" get commandline /format:csv');
            if ($wmicOutput !== null && stripos($wmicOutput, $keyword) !== false) {
                return true;
            }

            // Fallback: try PowerShell Get-Process
            $psOutput = $this->runCommand(
                'powershell -NoProfile -Command "Get-WmiObject Win32_Process -Filter \"name=\'php.exe\'\" | Select-Object -ExpandProperty CommandLine"'
            );
            if ($psOutput !== null && stripos($psOutput, $keyword) !== false) {
                return true;
            }

            return false;
        } else {
            // Linux / Mac
            //
            // aaPanel Supervisor daemons use a *relative* start command
            // (e.g. "php artisan queue:work") with "Process directory" set to
            // the app root. The process command line therefore does NOT contain
            // the app path — only the process working directory (cwd) does.
            //
            // Strategy:
            //   1. Find all PIDs whose command line contains the keyword (pgrep -f).
            //   2. Resolve each PID's cwd:
            //        Linux → readlink /proc/<pid>/cwd   (fast, no extra tools)
            //        macOS → lsof -a -d cwd -p <pid>   (/proc does not exist on Mac)
            //   3. Accept the process only if its cwd matches base_path().
            //
            // This isolates per-domain regardless of absolute vs relative commands.
            $appBasePath = rtrim(base_path(), '/');
            $isLinux     = strtolower(PHP_OS) === 'linux';

            // Step 1: get matching PIDs
            $pgrepOutput = $this->runCommand('pgrep -f ' . escapeshellarg($keyword) . ' 2>/dev/null');

            if ($pgrepOutput !== null && trim($pgrepOutput) !== '') {
                foreach (explode("\n", trim($pgrepOutput)) as $pid) {
                    $pid = trim($pid);
                    if (!ctype_digit($pid)) {
                        continue;
                    }

                    // Step 2: resolve cwd — method differs by OS
                    if ($isLinux) {
                        // Linux: /proc/<pid>/cwd is a symlink to the cwd
                        $cwd = $this->runCommand("readlink /proc/{$pid}/cwd 2>/dev/null");
                    } else {
                        // macOS: lsof reports the cwd; last column of the data row is the path
                        $cwd = $this->runCommand(
                            "lsof -a -d cwd -p {$pid} 2>/dev/null | awk 'NR==2{print \$NF}'"
                        );
                    }

                    if ($cwd === null) {
                        continue;
                    }

                    $cwd = rtrim(trim($cwd), '/');

                    // Step 3: cwd must match this app's base path
                    if ($cwd === $appBasePath) {
                        return true;
                    }
                }
            }

            return false;
        }
    }

    /**
     * Upsert a check mark value in system_settings.
     */
    private function saveCheckMark(string $name, int $value): void
    {
        SystemSetting::upsert(
            [['name' => $name, 'data' => $value]],
            ['name'],
            ['data']
        );
        $this->cache->removeSystemCache(config('constants.CACHE.SYSTEM.SETTINGS'));
    }
}