diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4be5c3..f1acae9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,3 +162,102 @@ jobs: - name: Cleanup if: always() run: docker compose down -v + + # ─── Prefix Codemod Idempotency ───────────────────────────── + # Re-runs tools/prefix-codemod.php in --verify mode against the committed + # tree. Any unprefixed table reference in SQL or any unrewritten bare-name + # SQL ref in PHP would be picked up here. + codemod-verify: + name: Prefix Codemod Idempotency + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: pdo_mysql, json, mbstring + + - name: Run codemod in dry-run mode (must produce zero changes) + run: | + OUT=$(php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM --quiet 2>&1 || true) + # Re-run normally to capture stats + STATS=$(php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM) + echo "$STATS" + # Both pass numbers must be zero + SQL_CHANGED=$(echo "$STATS" | grep -oE "SQL: ([0-9]+) files changed" | grep -oE "[0-9]+" | head -1) + PHP_CHANGED=$(echo "$STATS" | grep -oE "PHP: ([0-9]+) files changed" | grep -oE "[0-9]+" | head -1) + if [ "$SQL_CHANGED" != "0" ] || [ "$PHP_CHANGED" != "0" ]; then + echo "❌ Codemod is not idempotent: SQL=$SQL_CHANGED files, PHP=$PHP_CHANGED files would change." + echo "Run: php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM --apply" + exit 1 + fi + echo "✅ Codemod idempotent: 0 SQL + 0 PHP changes on second run." + + - name: Run codemod in --verify mode (zero unprefixed SQL refs) + run: | + php tools/prefix-codemod.php --root FINAL_PRODUCTION_SYSTEM --verify + echo "✅ Verify mode pass: zero unprefixed table references." + + # ─── Restricted-PHP Smoke Test (panel-style env) ──────────── + # Simulates aaPanel-style restrictive PHP settings: low max_execution_time + # (forces async per-migration runner) and no allow_url_fopen. + installer-restricted-php: + name: Installer (restricted PHP env) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP with restrictive settings + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: pdo_mysql, curl, openssl, json, mbstring, zip + ini-values: | + max_execution_time=15 + memory_limit=128M + allow_url_fopen=Off + + - name: Verify install/ajax.php parses under restrictive settings + run: | + # Lint pass with the restrictive INI loaded. + php -l FINAL_PRODUCTION_SYSTEM/install/ajax.php + php -l FINAL_PRODUCTION_SYSTEM/install/index.php + # Smoke import: load ajax.php's helpers and confirm no fatals at load time. + php -r ' + $_POST = ["action" => ""]; + $_GET = []; + ob_start(); + include "FINAL_PRODUCTION_SYSTEM/install/ajax.php"; + $out = ob_get_clean(); + echo "Loaded successfully. Output: " . substr($out, 0, 200) . "\n"; + if (!function_exists("installerBuildDsn")) { echo "FAIL: installerBuildDsn missing\n"; exit(1); } + if (!function_exists("installerRunSqlFile")) { echo "FAIL: installerRunSqlFile missing\n"; exit(1); } + if (!function_exists("installerSplitSql")) { echo "FAIL: installerSplitSql missing\n"; exit(1); } + if (!function_exists("installerProbeSockets")) { echo "FAIL: installerProbeSockets missing\n"; exit(1); } + if (!function_exists("installerCheckIncompleteState")) { echo "FAIL: installerCheckIncompleteState missing\n"; exit(1); } + if (!function_exists("installerT")) { echo "FAIL: installerT missing\n"; exit(1); } + echo "✅ All installer helpers loaded under restricted PHP.\n"; + ' + + - name: Verify SQL splitter handles every existing migration + run: | + php -r ' + include "FINAL_PRODUCTION_SYSTEM/install/ajax.php"; + $files = glob("FINAL_PRODUCTION_SYSTEM/database/*.sql"); + $totalStmts = 0; + foreach ($files as $f) { + $sql = file_get_contents($f); + $sql = preg_replace("/DELIMITER\s+[^\n]+/i", "", $sql); + $sql = preg_replace("/^\s*(START\s+TRANSACTION|BEGIN)\s*;\s*\$/im", "", $sql); + $sql = preg_replace("/^\s*COMMIT\s*;\s*\$/im", "", $sql); + $stmts = installerSplitSql($sql); + $count = count(array_filter($stmts, fn($s) => trim($s) !== "")); + $totalStmts += $count; + if ($count === 0) { + echo "WARN: " . basename($f) . " produced 0 statements\n"; + } + } + echo "✅ Splitter handled " . count($files) . " files, " . $totalStmts . " total statements.\n"; + ' diff --git a/CLAUDE.md b/CLAUDE.md index 00b9974..b9433bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -280,8 +280,49 @@ powershell -ExecutionPolicy Bypass -File "FINAL_PRODUCTION_SYSTEM/activation/mai # Deploy license server cd license-server && npx wrangler login && npx wrangler deploy + +# Run prefix codemod (only when adding NEW tables — output already in repo) +docker cp tools/prefix-codemod.php oem-activation-web:/tmp/codemod.php +docker compose exec web php /tmp/codemod.php --root /var/www/html/activate # dry-run +docker compose exec web php /tmp/codemod.php --root /var/www/html/activate --apply # write +docker compose exec web php /tmp/codemod.php --root /var/www/html/activate --verify # second-run must be 0/0 ``` +## Multi-Panel Web Installer (P0 + P1 + P2) + +The web installer at `FINAL_PRODUCTION_SYSTEM/install/` works on aaPanel, +cPanel, Plesk, DirectAdmin, CyberPanel, ISPConfig, Vesta. Highlights: + +| Feature | What it does | +|---------|--------------| +| Async per-migration runner | `install_db_init` + `install_db_step` survives 30–60s `max_execution_time` caps | +| Per-statement SQL splitter | Respects backticks, quotes, line + block comments | +| Charset auto-fallback | MySQL <5.7 / MariaDB <5.5.3 → utf8mb3 instead of utf8mb4 | +| CREATE DATABASE skip-toggle | Step-2 checkbox for Plesk/CyberPanel users | +| Reverse-proxy IP hardening | Trust X-Forwarded-For only when REMOTE_ADDR is RFC1918/loopback | +| Auto-unlock recovery | `install.lock` + `admin_users` empty/missing → silent unlock | +| Unix-socket auto-detect | Probes 8 common paths, populates step-2 input | +| Joomla `#__` table prefix | Optional prefix in step-2 advanced section; empty default | +| Resumable `.progress.json` | Reload prompts "Resume from step N?" | +| Per-migration retry/skip | Inline buttons appear next to a failed migration | +| Structured `install.log` | Audit trail of every preflight/step/error | +| Step-6 health probe | `Run health check` button calls `?action=health` | + +### DB_PREFIX (Joomla-style) + +- **Sentinel**: SQL files use `#__tablename`. Substituted at install time + (`installerRunSqlFile` in `install/ajax.php`) or Docker init time + (`KEYGATE_DB_PREFIX` env var in `00-init.sh`). +- **Runtime helper**: `t('admin_users')` returns `DB_PREFIX . 'admin_users'`. + Defined in `functions/db-helpers.php`, loaded from `constants.php` + before any controller runs. +- **Backward compat**: legacy installs without `define('DB_PREFIX', ...)` + in `config.php` get an empty default → identical behavior to pre-prefix + release. +- **When adding a new table**: write SQL with `#__yourtable` placeholder; + reference from PHP via `t('yourtable')` (or run `tools/prefix-codemod.php` + `--apply` to convert all references mechanically). + ## Contributing Guide ### "I need to add a new admin feature"