Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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";
'
41 changes: 41 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading