From b431b9deed1c6d845b74be80bf20bfbd001a9d70 Mon Sep 17 00:00:00 2001 From: VibTools Date: Thu, 6 Aug 2026 14:55:59 -0700 Subject: [PATCH] release: finalize Licora v5.1.0 installer --- .github/workflows/ci.yml | 1 + CHANGELOG.md | 10 +- PHASE2_INSTALLER_SUMMARY.md | 20 ++++ README.md | 38 ++++-- RELEASE_COMMANDS_v5.1.0.md | 157 +++++++++++++++++++++++++ RELEASE_NOTES_v5.1.0.md | 36 ++++++ REPOSITORY_METADATA.md | 13 +- SECURITY.md | 14 +++ audit/V5.1.0_DELTA_FILE_SHA256SUMS.txt | 24 ++++ audit/V5.1.0_DELTA_PATCH_MANIFEST.txt | 35 ++++++ audit/V5.1.0_RELEASE_DELTA_AUDIT.md | 115 ++++++++++++++++++ docs/COMPATIBILITY_MATRIX.md | 48 ++++++++ docs/CONFIGURATION.md | 67 +++++++---- docs/FAQ.md | 45 +++++++ docs/INSTALLATION.md | 14 +++ docs/RELEASE.md | 76 +++++++++--- docs/TROUBLESHOOTING.md | 25 +++- docs/UPGRADE_GUIDE.md | 6 + includes/config.php | 37 +++--- includes/installation.php | 83 +++++++++++-- install.php | 2 +- scripts/package-release.sh | 88 ++++++++++---- scripts/validate.sh | 54 +++++++-- tests/installer_smoke.php | 53 +++++++++ tests/release_readiness.php | 140 ++++++++++++++++++++++ 25 files changed, 1091 insertions(+), 110 deletions(-) create mode 100644 RELEASE_COMMANDS_v5.1.0.md create mode 100644 audit/V5.1.0_DELTA_FILE_SHA256SUMS.txt create mode 100644 audit/V5.1.0_DELTA_PATCH_MANIFEST.txt create mode 100644 audit/V5.1.0_RELEASE_DELTA_AUDIT.md create mode 100644 docs/COMPATIBILITY_MATRIX.md create mode 100644 docs/FAQ.md create mode 100644 tests/release_readiness.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42cfdad..9b5d1c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,7 @@ name: CI on: push: branches: [main] + tags: ['v*'] pull_request: workflow_dispatch: diff --git a/CHANGELOG.md b/CHANGELOG.md index 275c44e..4295b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ All notable public-release changes are recorded here. Historical project notes r - Continue reviewed Zero Freedom development after the v5.1.0 installer release. -## [5.1.0] - 2026-07-23 +## [5.1.0] - 2026-08-06 ### Added @@ -33,6 +33,14 @@ All notable public-release changes are recorded here. Historical project notes r - Enforced the installation lock for same-session requests to installer steps 1-8 after successful installation. - Restricted the completion screen to one pending view and cleared completion-session state before the admin-login redirect. +- Prevented private installer configuration from pinning the source release version during future upgrades. +- Removed exception messages, source paths, line numbers, and absolute installer paths from public responses. +- Rejected Base URLs containing embedded credentials, query parameters, or fragments. +- Rejected CR/LF characters in Mail From Name and validated installer-generated secrets before activation. +- Refreshed cached file-status data before checking the `includes` directory and made the permission regression test portable across root-based CI containers. +- Added a safe actionable diagnostic for database accounts that lack the `TRIGGER` privilege required by the existing schema. +- Replaced release packaging based on the live directory with validated `git archive` packaging and SHA-256 output. +- Corrected release, configuration, metadata, FAQ, compatibility, troubleshooting, and upgrade documentation for v5.1.0. ### Compatibility diff --git a/PHASE2_INSTALLER_SUMMARY.md b/PHASE2_INSTALLER_SUMMARY.md index d04a1ce..876e4bf 100644 --- a/PHASE2_INSTALLER_SUMMARY.md +++ b/PHASE2_INSTALLER_SUMMARY.md @@ -6,6 +6,7 @@ - Stable base: `v5.0.1.1` - Base commit: `7fafd2c34b3425df6ef310b9f25ffa426588d294` - Development mode: Zero Freedom Development +- Final release date: `2026-08-06` ## Implementation summary @@ -101,6 +102,22 @@ The raw generated API credential is never displayed or logged. - Installer lock after completion - Existing configured database outage does not reopen installer + +## Final release hardening + +The unpublished v5.1.0 line was finalized before tagging with the following compatibility-preserving corrections: + +- Generic public exception responses and safe diagnostic references +- Safe installer error mapping, including missing `TRIGGER` privilege +- Hidden absolute server paths in requirement output +- Base URL credential/query/fragment rejection +- Mail From Name CR/LF rejection +- Installer-generated secret validation +- Source version precedence over preserved private configuration +- Cached writability-state refresh and portable permission regression coverage +- Git-ref-only release packaging with SHA-256 output +- Corrected release and configuration documentation + ## Upgrade compatibility - v5.0.1 and v5.0.1.1 private configuration remains supported. @@ -125,3 +142,6 @@ Automated coverage includes: - Non-secret installation flag - Preserved schema/migration/frontend hashes - Preserved API and route markers +- Safe exception and installer-error output +- URL, mail-header, and generated-secret validation +- Release-version consistency and Git-ref packaging diff --git a/README.md b/README.md index ab72d3e..e3ca2a1 100644 --- a/README.md +++ b/README.md @@ -68,16 +68,25 @@ The application is a server-rendered PHP project with no Composer runtime depend ## Quick start +### Fresh installation + 1. Place the project in a non-public staging environment. -2. Create an empty database and import `database.sql`, or open `install.php` once. -3. Configure `includes/config.local.php` or environment variables. -4. Sign in at `admin/login.php` using the temporary local-only account: - - Username: `admin` - - Password: `ChangeMe!2026` -5. Change the password immediately from **Admin Users**. -6. Delete or deny access to `install.php`. +2. Create an empty MySQL/MariaDB database and a database account with the schema privileges required by `database.sql`, including `TRIGGER`. +3. Open `install.php` or `/install` and complete the ten-step wizard. +4. Create the first administrator in the wizard; no default password is retained after successful wizard installation. +5. Confirm that `includes/config.local.php` and `includes/.licora-installed` were created and are not web-accessible. +6. Sign in at `admin/login.php`, open `admin/health.php`, and complete an API/license/device smoke test. 7. Configure HTTPS, cron, backups, and web-server deny rules before production use. +### Manual import + +Importing `database.sql` directly creates a temporary local-only account: + +- Username: `admin` +- Password: `ChangeMe!2026` + +Change that password immediately and create `includes/config.local.php` from `config.sample.php`. + Complete steps: [docs/INSTALLATION.md](docs/INSTALLATION.md). ## API quick example @@ -105,10 +114,13 @@ The application accepts deployment-specific values through environment variables | Purpose | Preferred variable | Default | |---|---|---| | Database host | `LICENSE_DB_HOST` | `localhost` | +| Database port | `LICENSE_DB_PORT` | `3306` | | Database name | `LICENSE_DB_NAME` | empty | | Database user | `LICENSE_DB_USER` | empty | | Database password | `LICENSE_DB_PASS` | empty | +| Application name | `APP_NAME` | `Licora` | | Application URL | `APP_URL` | `http://localhost` | +| Application version | `APP_VERSION` | `5.1.0` | | Environment | `APP_ENV` | `production` | | Encryption key | `LICENSE_ENCRYPTION_KEY` | empty fallback | | API limit | `API_RATE_LIMIT` | `1000` | @@ -122,7 +134,7 @@ Full reference: [docs/CONFIGURATION.md](docs/CONFIGURATION.md). bash scripts/validate.sh ``` -The validation script checks PHP syntax, JavaScript syntax when Node.js is available, public-release secret markers, expected repository files, and the built-in security smoke test. Database-backed behavior requires a disposable MySQL/MariaDB instance and is not simulated by the static validation suite. +The validation script checks PHP syntax, security behavior, compatibility invariants, installer parsing and lock behavior, release-version consistency, safe public errors, JavaScript syntax, public-release secret markers, and SQL seed scope. Database-backed behavior still requires a disposable MySQL/MariaDB instance. ## Documentation @@ -137,6 +149,7 @@ The validation script checks PHP syntax, JavaScript syntax when Node.js is avail - [Maintenance](docs/MAINTENANCE.md) - [Release guide](docs/RELEASE.md) - [Troubleshooting](docs/TROUBLESHOOTING.md) +- [v5.1.0 release notes](RELEASE_NOTES_v5.1.0.md) - [v5.0.1 release notes](RELEASE_NOTES-v5.0.1.md) - [v5.0.0 release notes](RELEASE_NOTES.md) - [Forensic audit](audit/FORENSIC_AUDIT_REPORT.md) @@ -145,7 +158,14 @@ The validation script checks PHP syntax, JavaScript syntax when Node.js is avail ## Known limitations -The audit intentionally records unresolved behavior rather than silently changing application logic. Important items include a legacy unauthenticated verification endpoint, Bearer-header parsing inconsistencies, an unused session-timeout method, settings that are stored but not enforced, unauthenticated encryption, destructive admin actions implemented through query strings, and CDN supply-chain exposure. Review the audit before production deployment. +- The legacy `/api/check_license.php` endpoint remains unauthenticated for compatibility; new clients should use `/api/verify.php` with an API key. +- Several stored settings remain informational or only partially connected to runtime enforcement. +- The admin interface depends on public CDN assets unless a deployment vendors them locally. +- Nginx and LiteSpeed operators must reproduce the supplied Apache deny rules. +- The standard schema requires database privileges including `TRIGGER`; some free shared hosts do not provide them. +- Full browser and database-backed regression testing remains a release/deployment responsibility. + +Review [SECURITY.md](SECURITY.md), [docs/COMPATIBILITY_MATRIX.md](docs/COMPATIBILITY_MATRIX.md), and the forensic audit before production deployment. ## Roadmap diff --git a/RELEASE_COMMANDS_v5.1.0.md b/RELEASE_COMMANDS_v5.1.0.md new file mode 100644 index 0000000..bfee3cd --- /dev/null +++ b/RELEASE_COMMANDS_v5.1.0.md @@ -0,0 +1,157 @@ +# Licora v5.1.0 Release Commands for Windows + +These commands assume the repository is located at: + +```text +D:\VibTools_Workspace\02_Websites\01_Licora_Open_Source_Cental_License_System\github_release +``` + +## 1. Preserve the current v5.1.1 branch work + +Run before extracting the v5.1.0 delta patch: + +```bat +cd /d D:\VibTools_Workspace\02_Websites\01_Licora_Open_Source_Cental_License_System\github_release + +git status +git stash push -u -m "backup-before-v5.1.0-final-release" +git fetch origin --prune --tags +git switch main +git pull --ff-only origin main +git switch -c release/v5.1.0-final +``` + +Extract the delta ZIP into the repository root and allow overwrite/replace. + +Do not extract the patch while still on `feature/v5.1.1-quality-stability`. + +## 2. Validate the patched source + +From Git Bash: + +```bash +bash scripts/validate.sh +``` + +From Command Prompt: + +```bat +git diff --check +git status --short +``` + +Complete the manual release gate described in `docs/RELEASE.md` before tagging. + +## 3. Review and commit + +```bat +git status +git diff --stat +git diff --check +git add -A +git diff --cached --check +git diff --cached --stat +git commit -m "release: finalize Licora v5.1.0 installer" +git push -u origin release/v5.1.0-final +``` + +## 4. Create and merge the pull request + +With GitHub CLI: + +```bat +gh pr create ^ + --base main ^ + --head release/v5.1.0-final ^ + --title "release: Licora v5.1.0 Smart Installer" ^ + --body-file RELEASE_NOTES_v5.1.0.md + +gh pr checks --watch +gh pr merge --merge +``` + +If branch protection requires a PR number, use: + +```bat +gh pr view --web +``` + +After merge: + +```bat +git switch main +git pull --ff-only origin main +git status +git log --oneline --decorate -10 +``` + +## 5. Run final validation on `main` + +From Git Bash: + +```bash +bash scripts/validate.sh +``` + +Confirm the exact version markers: + +```bat +git grep -n "5.1.0" -- includes/config.php includes/installation.php install.php config.sample.php CHANGELOG.md RELEASE_NOTES_v5.1.0.md +``` + +## 6. Create the annotated tag + +```bat +git tag -a v5.1.0 -m "Licora v5.1.0 - Smart Installer and First-Run Wizard" +git show --stat --oneline v5.1.0 +git push origin v5.1.0 +``` + +## 7. Build the release ZIP and checksum + +Run from Git Bash: + +```bash +bash scripts/package-release.sh v5.1.0 v5.1.0 +``` + +Expected files in the parent directory: + +```text +Licora-v5.1.0.zip +Licora-v5.1.0.zip.sha256 +``` + +## 8. Publish the GitHub Release + +From Command Prompt, adjust the asset paths if your repository parent differs: + +```bat +gh release create v5.1.0 ^ + ..\Licora-v5.1.0.zip ^ + ..\Licora-v5.1.0.zip.sha256 ^ + --verify-tag ^ + --title "Licora v5.1.0 - Smart Installer and First-Run Wizard" ^ + --notes-file RELEASE_NOTES_v5.1.0.md ^ + --latest +``` + +## 9. Verify the published release + +```bat +gh release view v5.1.0 --web +git status +git tag --sort=-v:refname +``` + +Download the published ZIP, verify its checksum, extract it into a disposable folder, run `bash scripts/validate.sh`, and complete one fresh installation plus one authenticated `/api/verify.php` test. + +## Recovery of the preserved branch work + +The original v5.1.1 work remains in the stash and its remote branch. To inspect the stash later: + +```bat +git stash list +``` + +Do not apply that stash onto the v5.1.0 release branch. diff --git a/RELEASE_NOTES_v5.1.0.md b/RELEASE_NOTES_v5.1.0.md index 04a0f14..28dfd03 100644 --- a/RELEASE_NOTES_v5.1.0.md +++ b/RELEASE_NOTES_v5.1.0.md @@ -1,5 +1,6 @@ # Licora v5.1.0 — Smart Installer & First-Run Wizard +**Release date:** 2026-08-06 **Release type:** Backward-compatible installer feature release **Stable base:** `v5.0.1.1` **Database migration:** None @@ -111,6 +112,21 @@ Demo records can be removed with: php scripts/remove-demo-data.php ``` + +## Release hardening included + +Before publication, the v5.1.0 line received additional release-readiness fixes without changing the database schema, license engine, or API contracts: + +- Public exception responses no longer expose exception messages, source paths, or line numbers. +- Installer requirement output no longer exposes absolute server paths. +- Base URLs containing embedded credentials, query strings, or fragments are rejected. +- Mail From Name rejects CR/LF characters. +- Installer-generated application and security secrets are validated before finalization. +- Runtime release identity resolves before preserved private configuration, preventing version pinning during future upgrades. +- Cached filesystem status is refreshed before checking whether `includes/` is writable. +- Database `TRIGGER` permission denial is mapped to an actionable, non-secret installer message. +- Release packaging uses a validated Git ref and produces a SHA-256 checksum. + ## Compatibility guarantees This release does not change: @@ -164,6 +180,11 @@ The repository validation suite covers: - JavaScript syntax - Public-release marker scanning - SQL seed-scope validation +- Release-version consistency +- Safe public-error mapping +- Base URL and mail-header validation +- Installer permission-state ordering +- Git-ref release packaging markers ## Documentation @@ -172,4 +193,19 @@ The repository validation suite covers: - `docs/FIRST_RUN_GUIDE.md` - `docs/UPGRADE_GUIDE.md` - `docs/DEMO_DATA.md` +- `docs/FAQ.md` +- `docs/COMPATIBILITY_MATRIX.md` +- `docs/RELEASE.md` +- `RELEASE_COMMANDS_v5.1.0.md` - `PHASE2_INSTALLER_SUMMARY.md` + +## Hosting privilege note + +The unchanged Licora schema contains MySQL/MariaDB triggers. The standard installer therefore requires a database account with the privileges needed to create those triggers. Some free shared hosts deny `TRIGGER`; v5.1.0 reports that condition safely but does not silently remove schema objects. + +## Release assets + +The official release should include: + +- `Licora-v5.1.0.zip` +- `Licora-v5.1.0.zip.sha256` diff --git a/REPOSITORY_METADATA.md b/REPOSITORY_METADATA.md index 15e1882..1299b43 100644 --- a/REPOSITORY_METADATA.md +++ b/REPOSITORY_METADATA.md @@ -4,7 +4,7 @@ - **Name:** `Licora` - **Title:** Licora — Open-Source Central License Management System -- **Description:** Licora is an open-source, self-hosted PHP and MySQL/MariaDB central license management system with license validation, API-key binding, device controls, administration, audit logs, and backups. +- **Description:** Licora is an open-source, self-hosted PHP and MySQL/MariaDB central license management system with authenticated license validation, API-key/application binding, device controls, administration, audit logs, and backups. - **Homepage:** `https://vib.tools/` - **Visibility:** Public - **Default branch:** `main` @@ -14,10 +14,15 @@ `php`, `mysql`, `mariadb`, `license-management`, `license-server`, `license-key`, `api-key`, `device-management`, `admin-dashboard`, `self-hosted`, `php8`, `csrf-protection`, `rate-limiting`, `audit-log`, `vibtools` -## Release title +## v5.1.0 release -`v5.0.1 — Branding and Company Information Hotfix` +- **Tag:** `v5.1.0` +- **Target:** reviewed `main` commit containing the final installer-lock and release-readiness fixes +- **Title:** `Licora v5.1.0 — Smart Installer & First-Run Wizard` +- **Latest release:** Yes, after all release-gate checks pass +- **Release notes:** `RELEASE_NOTES_v5.1.0.md` +- **Assets:** `Licora-v5.1.0.zip` and `Licora-v5.1.0.zip.sha256` ## Release summary -Documentation-only hotfix that applies the Licora product identity, corrects Vib Tools company information, updates repository links, and replaces the private support contact without changing application behavior. +Licora v5.1.0 adds a ten-step first-run installer, delimiter-aware schema execution, secure administrator and secret generation, atomic private configuration, installation locking, optional demo data, database-port support, safe installer diagnostics, and expanded regression validation. The release preserves the existing license engine, API contracts, database schema, routes, admin UI, cron entry points, and encrypted-data compatibility. diff --git a/SECURITY.md b/SECURITY.md index 3a1655c..c695c6c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -41,3 +41,17 @@ An installed deployment returns HTTP 403 from `install.php`. An intentional reco ### Remaining compatibility limitation `/api/check_license.php` remains unauthenticated by default. Mandatory authentication is not introduced in v5.0.1.1 because it would break existing integrations and violate the frozen API contract. Deployments should prefer `/api/verify.php` with `X-API-Key` or `Authorization: Bearer TOKEN`, restrict the legacy path at the web server or network layer where possible, and retain rate limiting. + +## v5.1.0 quality and stability hardening + +Licora v5.1.0 removes verified technical-detail disclosure from unhandled exception responses and installer requirement output. + +- Public unhandled-error JSON remains `{"error":"Internal Server Error"}`. +- Exception messages, SQL text, credentials, stack traces, and full server paths are not returned. +- Internal logs record a diagnostic reference, exception class, file basename, and line number. +- Installer Base URLs cannot contain embedded credentials, query parameters, or fragments. +- Mail From Name rejects CR/LF control characters. +- Installer-generated secrets are validated before activation. +- Existing legacy configuration and secret compatibility are unchanged. + +Historical logs are not deleted or rewritten by this release. diff --git a/audit/V5.1.0_DELTA_FILE_SHA256SUMS.txt b/audit/V5.1.0_DELTA_FILE_SHA256SUMS.txt new file mode 100644 index 0000000..2328dee --- /dev/null +++ b/audit/V5.1.0_DELTA_FILE_SHA256SUMS.txt @@ -0,0 +1,24 @@ +d086d37b5ed4649c0ea8a1d8b49b392e554fb05e398ad45ea27ea933821ea871 .github/workflows/ci.yml +74413792534cab96a9488c923524bf1043fdfafccd10c38f09e22e6d57e5e1f8 CHANGELOG.md +eed31a28241bd24780bb59b482e16e1d1e8da960c86478af8ed110a93c5602eb PHASE2_INSTALLER_SUMMARY.md +6527bec483e6e5783cef22ea4fbbe7f25a5f67a33cf31e6f36f054ce88417eb0 README.md +4c516eb6c5e868c7c2f4cbed5e239f316a5a618167675066d5a6d651765a3e7d RELEASE_COMMANDS_v5.1.0.md +6cbfad108bacdd4fb8e1e3b52088ff16b57d7c30abb446b791fb5b17dbc39af2 RELEASE_NOTES_v5.1.0.md +ec2dadc3d493a73d7f60ae1c95f008fd0efcb65ecd18c067133d7e2a629c27dd REPOSITORY_METADATA.md +5de5aafc989e3a6acf3a3d28edf83d11e0357dee9db76b3a6405a8c300b17cd0 SECURITY.md +a73d67a65213c1d0774b39c54c82d4600b58ce09e3b38b4288d2d3aa71d2f016 audit/V5.1.0_RELEASE_DELTA_AUDIT.md +c824efc265a83e117a6cda50923bf8bf2d7c76d1e6e0e2c9dad63bdd441b2abf audit/V5.1.0_DELTA_PATCH_MANIFEST.txt +6c0613273286dbac1d2e43f860dbcb4cab654d38fe9c5e558d7d8a63c08aa781 docs/COMPATIBILITY_MATRIX.md +867cefc7753f82ef1ba8f23a6b730b50095e28e671e5e5bdfab3d45c933eafeb docs/CONFIGURATION.md +3fdbde294b5f9f080f22af99a2721cba5ad1f2614e3a580ca4c06bbd361b0f32 docs/FAQ.md +be2921578decdb662262526207086794ecbffaaaf98ba8e7314f7efdb8d01376 docs/INSTALLATION.md +3ec2265fd1131108dfceeab57d895e256fc5a9033f8a3fa12e37dc043b0a6471 docs/RELEASE.md +33a5367c1916994cafc3600ebb04341635e1118816b6856e0f97753d1446f33d docs/TROUBLESHOOTING.md +0d31cbe6e920ab36c6a4e579bd8e37e55c05b5218f74ea7b78859073fc8c14ae docs/UPGRADE_GUIDE.md +ecb9e7203ace6b13a352268073ad0447c4206c34d9cf7e88410f2d63bbce8580 includes/config.php +2d4f4f6aefc91fea6350059675e4e0a83d45b06d24154029a6de16deabfd2611 includes/installation.php +e953fb47cb8a7543cffcdc2e8a6b09494e7b61e6bd89f77a4489613a73ebd2a8 install.php +0d89e47b05104a2b5408baaa21236def3a00b50b7a3d171274920448eeeb32cd scripts/package-release.sh +0db7a58c3368745660f81236507fcebffc63b95f7d3db4aefbebf39e3a94be1b scripts/validate.sh +42ac805c023ec97d0af0a9d05b1c083731fa811c4769feb0ebf444194a2fa3ce tests/installer_smoke.php +0606fccf457e53847b853c61ab5b6dd1c7aceea33f5dfa10de9190b5979c9140 tests/release_readiness.php diff --git a/audit/V5.1.0_DELTA_PATCH_MANIFEST.txt b/audit/V5.1.0_DELTA_PATCH_MANIFEST.txt new file mode 100644 index 0000000..98dffc6 --- /dev/null +++ b/audit/V5.1.0_DELTA_PATCH_MANIFEST.txt @@ -0,0 +1,35 @@ +Licora v5.1.0 Final Release Delta Patch +Base commit: 372fb35f602b16f6dd684b59a36114b7b941c3fe +Target branch: release/v5.1.0-final created from main +Audit date: 2026-08-06 + +Modified files: +.github/workflows/ci.yml +CHANGELOG.md +PHASE2_INSTALLER_SUMMARY.md +README.md +RELEASE_NOTES_v5.1.0.md +REPOSITORY_METADATA.md +SECURITY.md +docs/CONFIGURATION.md +docs/INSTALLATION.md +docs/RELEASE.md +docs/TROUBLESHOOTING.md +docs/UPGRADE_GUIDE.md +includes/config.php +includes/installation.php +install.php +scripts/package-release.sh +scripts/validate.sh +tests/installer_smoke.php + +Added files: +RELEASE_COMMANDS_v5.1.0.md +audit/V5.1.0_RELEASE_DELTA_AUDIT.md +audit/V5.1.0_DELTA_PATCH_MANIFEST.txt +docs/COMPATIBILITY_MATRIX.md +docs/FAQ.md +tests/release_readiness.php + +Application rule: +Extract into the repository root on a clean release/v5.1.0-final branch created from main, and allow overwrite/replace. Do not tag the v5.1.1 feature branch as v5.1.0. diff --git a/audit/V5.1.0_RELEASE_DELTA_AUDIT.md b/audit/V5.1.0_RELEASE_DELTA_AUDIT.md new file mode 100644 index 0000000..fd4421d --- /dev/null +++ b/audit/V5.1.0_RELEASE_DELTA_AUDIT.md @@ -0,0 +1,115 @@ +# Licora v5.1.0 Final Release Delta Audit + +## Audit basis + +- Source supplied as `github_release.zip`. +- Supplied working branch: `feature/v5.1.1-quality-stability`. +- Supplied branch HEAD: `6887fe0d3e19e5899e5c96f43361288cd8ce3561`. +- Published stable tag present in the supplied repository: `v5.0.1.1`. +- v5.1.0 installer line used as the delta base: `372fb35f602b16f6dd684b59a36114b7b941c3fe`. +- Intended final release: `v5.1.0`. +- Audit date: `2026-08-06`. + +This delta is intended for a clean release branch created from `main`, not for tagging the existing v5.1.1 feature branch. + +## Release blockers found + +1. The supplied working tree contained six uncommitted v5.1.1 files. +2. The unpublished v5.1.0 source exposed exception messages, source paths, and line numbers outside production mode. +3. Installer requirement output exposed the absolute `includes` directory path. +4. Installer Base URL validation accepted embedded credentials, query strings, and fragments. +5. Mail From Name validation did not reject CR/LF control characters. +6. Installer-generated secrets were not revalidated at finalization. +7. Installer-generated private configuration could pin the displayed source version during a future upgrade. +8. The permission regression test failed in root-based CI/container environments because `chmod(0500)` does not necessarily make a directory non-writable for root. +9. A shared host denying `TRIGGER` produced only a generic failure despite the standard schema requiring triggers. +10. `scripts/package-release.sh` packaged the live directory and excluded too few private/runtime file patterns. +11. Configuration, release, metadata, README, FAQ, troubleshooting, and upgrade documentation contained stale or inconsistent version information. +12. CI did not run on version-tag pushes. + +## Corrections included in this delta + +### Installer and runtime + +- Resolves `APP_VERSION=5.1.0` before loading private local configuration. +- Uses `Licora` as the runtime application-name default. +- Returns generic public unhandled-exception JSON in all environments. +- Logs only a diagnostic reference, exception class, file basename, and line number. +- Adds safe installer public-error mapping. +- Provides a specific non-secret diagnostic when MySQL/MariaDB denies `TRIGGER`. +- Removes absolute server paths from installer requirement output. +- Refreshes cached file status before evaluating `includes/` writability. +- Rejects unsafe Base URLs containing credentials, query strings, or fragments. +- Rejects CR/LF in Mail From Name. +- Validates generated 64-hex application, encryption, CSRF, and JWT secrets before finalization. +- Preserves the existing schema, API contracts, license engine, routes, admin UI, and cron entry points. + +### Validation + +- Makes the restricted-directory smoke test portable in root-based CI. +- Adds `tests/release_readiness.php`. +- Verifies release-version consistency, safe error output, URL/mail validation, secret validation, path redaction, stat-cache ordering, trigger diagnostics, documentation presence, and Git-ref packaging markers. +- Keeps PHP 8.0 through 8.4 CI validation. +- Enables CI on `v*` tag pushes. + +### Release packaging + +- Replaces live-directory ZIP creation with `git archive` from an explicit Git ref. +- Refuses tracked dirty working trees. +- Validates the selected Git ref in a temporary extracted tree. +- Creates `Licora-v5.1.0.zip` and a matching `.sha256` file. +- Packages tracked files only, excluding untracked private configuration and runtime secrets by construction. + +### Documentation + +- Corrects the publication date to `2026-08-06`. +- Aligns runtime, installer, release, configuration, and metadata version at `5.1.0`. +- Separates wizard installation from manual-import credentials. +- Documents the database `TRIGGER` requirement. +- Adds a compatibility matrix, FAQ, Windows release commands, release gate, and post-release verification. +- Removes outdated claims that Bearer parsing is broken in the current release. + +## Validation performed on the completed delta + +The completed delta was applied to a clean local branch created from the v5.1.0 main-line commit and committed in a disposable Git clone. + +Results: + +- Required-file validation: PASS +- PHP syntax validation: PASS +- Security smoke test: PASS +- Compatibility regression test: PASS +- Installer smoke test: PASS +- Release readiness test: PASS +- JavaScript syntax validation: PASS +- Public-release marker scan: PASS +- SQL seed-scope validation: PASS +- `git archive` release packaging: PASS +- SHA-256 generation: PASS +- Release ZIP inspection: PASS +- Private/runtime path inspection: no `.git`, `config.local.php`, `.licora-encryption.key`, `.licora-installed`, or deployment `.env` files found + +The test release archive contained 136 tracked entries under the `Licora-5.1.0/` prefix. + +## Remaining limitations intentionally not changed + +1. `/api/check_license.php` remains unauthenticated for backward compatibility. Prefer `/api/verify.php`. +2. The standard database schema still requires `TRIGGER`. Hosts that deny it cannot complete the standard schema unchanged. +3. InfinityFree-style external API blocking is a hosting limitation, not a Licora validation defect. +4. Desktop applications cannot keep a shared embedded API key permanently secret. +5. Several admin mutations still use CSRF-protected GET actions. They are retained for compatibility but should move to POST in a future version. +6. Public CDN dependencies remain unless deployment operators vendor the assets. +7. Some stored settings remain informational or only partially enforced. +8. Database-backed browser/API/cron regression testing remains a manual release gate. +9. Nginx and LiteSpeed deny rules remain operator-managed. + +## Required application procedure + +1. Stash the current v5.1.1 branch work. +2. Create `release/v5.1.0-final` from updated `main`. +3. Extract the delta ZIP into the repository root and overwrite matching files. +4. Run `bash scripts/validate.sh`. +5. Complete the manual release gate in `docs/RELEASE.md`. +6. Commit, push, review, merge, tag, package, and publish using `RELEASE_COMMANDS_v5.1.0.md`. + +Do not tag the current `feature/v5.1.1-quality-stability` history as v5.1.0. diff --git a/docs/COMPATIBILITY_MATRIX.md b/docs/COMPATIBILITY_MATRIX.md new file mode 100644 index 0000000..1561287 --- /dev/null +++ b/docs/COMPATIBILITY_MATRIX.md @@ -0,0 +1,48 @@ +# Compatibility Matrix + +## Status terminology + +- **Automated:** Covered by repository or CI tests. +- **Manual:** Must be confirmed in a disposable deployment before release. +- **Configuration review:** Server rules remain operator-managed. + +## PHP + +| Runtime | Status | Notes | +|---|---|---| +| PHP 8.0 | Automated compatibility retention | Existing support retained | +| PHP 8.1 | Automated target | v5.1.0 target | +| PHP 8.2 | Automated target; local XAMPP coverage recommended | v5.1.0 target | +| PHP 8.3 | Automated target | v5.1.0 target | +| PHP 8.4 | Automated forward-compatibility check | Not the minimum target | + +Required extensions: `pdo`, `pdo_mysql`, `openssl`, and `json`. + +## Web servers + +| Server | Status | Notes | +|---|---|---| +| Apache | Manual | Included `.htaccess` | +| Nginx | Configuration review + manual | Reproduce deny rules | +| LiteSpeed/OpenLiteSpeed | Configuration review + manual | Verify `.htaccess` compatibility or equivalent rules | + +## Databases + +| Database | Status | Notes | +|---|---|---| +| MariaDB | Manual | Verify schema, triggers, and advisory locks | +| MySQL | Manual | Verify schema, triggers, and advisory locks | + +## Installation scenarios + +| Scenario | Automated coverage | Manual release gate | +|---|---|---| +| Fresh production installation | Validation helpers | Required | +| Fresh DEMO installation | Demo helper and smoke markers | Required | +| Existing installation | Lock and compatibility tests | Required | +| Interrupted installation | Cleanup/static assertions | Required | +| Invalid database credentials | Safe error contract | Required | +| Invalid administrator credentials | Validation tests | Required | +| Read-only includes directory | Requirement test | Required | +| Existing lock file | Lock decision tests | Required | +| Legacy upgrade path | Compatibility assertions | Required | diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 2df9b54..c91885b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -2,50 +2,77 @@ ## Loading order -1. `includes/config.php` starts the session. +1. `includes/config.php` starts the secured session and resolves the source release version. 2. `includes/config.local.php` is loaded when present. -3. Environment variables supply missing constants. +3. Environment variables supply constants not already defined by private configuration. 4. Safe or empty defaults are used last. -Environment variables are preferred in managed hosting and containers. Keep local configuration outside the public web root when possible. +The release version is resolved before private configuration so an installer-generated `APP_VERSION` value cannot pin a future source upgrade. An explicit `APP_VERSION` environment variable remains supported. + +Environment variables are preferred in managed hosting and containers. On shared hosting, protect `includes/config.local.php` with the supplied `includes/.htaccess` or equivalent web-server rules. ## Variables | Constant | Environment variable | Default | Notes | |---|---|---|---| -| `DB_HOST` | `LICENSE_DB_HOST`, fallback `DB_HOST` | `localhost` | Database hostname; installer also accepts `host:port`. | +| `DB_HOST` | `LICENSE_DB_HOST`, fallback `DB_HOST` | `localhost` | Database hostname or IP address. | +| `DB_PORT` | `LICENSE_DB_PORT`, fallback `DB_PORT` | `3306` | MySQL/MariaDB TCP port. | | `DB_NAME` | `LICENSE_DB_NAME`, fallback `DB_NAME` | empty | Required. | -| `DB_USER` | `LICENSE_DB_USER`, fallback `DB_USER` | empty | Use least privilege. | -| `DB_PASS` | `LICENSE_DB_PASS`, fallback `DB_PASS` | empty | Required where the DB user has a password. | -| `APP_NAME` | `APP_NAME` | `License System` | Application label. | -| `APP_URL` | `APP_URL` | `http://localhost` | Used for CORS fallback and key derivation fallback. | -| `APP_VERSION` | `APP_VERSION` | `2.0` | Returned by the verification API. This historical runtime value differs from repository release numbering. | -| `ENVIRONMENT` | `APP_ENV` | `production` | `development` enables verbose output and debug logging. | -| `ENCRYPTION_KEY` | `LICENSE_ENCRYPTION_KEY` | empty | Set a random high-entropy value. Empty invokes a deterministic fallback and is not recommended. | -| `CSRF_SECRET` | `LICENSE_CSRF_SECRET` | empty | Present for compatibility; current CSRF tokens are session-random and do not consume this constant. | -| `JWT_SECRET` | `LICENSE_JWT_SECRET` | empty | Reserved; current code does not issue JWTs. | -| `API_RATE_LIMIT` | `API_RATE_LIMIT` | `1000` | Global hourly IP limit used by both APIs. | +| `DB_USER` | `LICENSE_DB_USER`, fallback `DB_USER` | empty | Use a database-specific least-privilege account. | +| `DB_PASS` | `LICENSE_DB_PASS`, fallback `DB_PASS` | empty | Required when the database account has a password. | +| `APP_NAME` | `APP_NAME` | `Licora` | Application label. | +| `APP_URL` | `APP_URL` | `http://localhost` | Public application root, without a query string or fragment. | +| `APP_VERSION` | `APP_VERSION` | `5.1.0` | Source release identity returned by the verification API. | +| `APP_TIMEZONE` | `APP_TIMEZONE` | `Asia/Dhaka` | PHP runtime timezone. | +| `APP_LOCALE` | `APP_LOCALE` | `en` | Application locale metadata. | +| `MAIL_FROM_NAME` | `MAIL_FROM_NAME` | `APP_NAME` | Mail display name. CR/LF characters are rejected by the installer. | +| `ENVIRONMENT` | `APP_ENV` | `production` | Controls PHP error reporting; public exception responses remain generic. | +| `APP_KEY` | `LICENSE_APP_KEY`, fallback `APP_KEY` | empty | Application secret generated by the installer. | +| `ENCRYPTION_KEY` | `LICENSE_ENCRYPTION_KEY` | empty | High-entropy encryption secret. Required for reliable encrypted-value recovery. | +| `CSRF_SECRET` | `LICENSE_CSRF_SECRET` | empty | Compatibility secret; session CSRF tokens remain random. | +| `JWT_SECRET` | `LICENSE_JWT_SECRET` | empty | Reserved for compatible future use. | +| `API_RATE_LIMIT` | `API_RATE_LIMIT` | `1000` | Global hourly IP limit used by the API endpoints. | | `API_VERSION` | `API_VERSION` | `v1` | Compatibility metadata. | -| Allowed origin | `LICENSE_ALLOWED_ORIGIN` | `APP_URL` | Exact-origin match for the full API endpoint. | +| Allowed origin | `LICENSE_ALLOWED_ORIGIN` | `APP_URL` | Exact-origin match used by the full API endpoint. | ## Example environment ```dotenv LICENSE_DB_HOST=127.0.0.1 +LICENSE_DB_PORT=3306 LICENSE_DB_NAME=license_system LICENSE_DB_USER=license_app LICENSE_DB_PASS=replace-with-a-secret -APP_URL=https://licenses.example.com +APP_NAME=Licora +APP_URL=https://licenses.example.com/licora APP_ENV=production -LICENSE_ENCRYPTION_KEY=replace-with-at-least-32-random-bytes +APP_TIMEZONE=Asia/Dhaka +APP_LOCALE=en +MAIL_FROM_NAME=Licora +LICENSE_APP_KEY=replace-with-a-random-64-character-secret +LICENSE_ENCRYPTION_KEY=replace-with-a-random-64-character-secret +LICENSE_CSRF_SECRET=replace-with-a-random-64-character-secret +LICENSE_JWT_SECRET=replace-with-a-random-64-character-secret API_RATE_LIMIT=1000 LICENSE_ALLOWED_ORIGIN=https://app.example.com ``` +## Installer-generated private configuration + +A fresh v5.1.0 wizard installation writes `includes/config.local.php` atomically and then creates `includes/.licora-installed`. Preserve both files, together with `includes/.licora-encryption.key` when present, during backup or upgrade operations. + +Never commit or publish: + +- `includes/config.local.php` +- `includes/.licora-encryption.key` +- `includes/.licora-installed` +- temporary `*.installing.*` files +- `.env` or `.env.*` deployment files + ## Stored settings -The `settings` table and admin page store system name, timezone, default license hours, default device limit, API rate limit, retention, two-factor toggle, maintenance mode, license prefix, and API base URL. The audit found that several stored values are not connected to runtime enforcement. Consult [FEATURE_MATRIX.md](FEATURE_MATRIX.md) before relying on a toggle. +The `settings` table and admin page store system name, timezone, default license hours, default device limit, API rate limit, retention, two-factor toggle, maintenance mode, license prefix, and API base URL. Some stored settings remain informational or only partially connected to runtime behavior. Review [FEATURE_MATRIX.md](FEATURE_MATRIX.md) before relying on a toggle. -## Time zone +## Database privileges -The PHP configuration currently sets `Asia/Dhaka` directly. The stored timezone setting is not applied by the runtime. +Fresh installation executes the existing `database.sql`, including tables, indexes, foreign keys, and triggers. The database account therefore needs the privileges required by that schema. Shared hosts that deny `TRIGGER` will not complete the standard installer. v5.1.0 returns a non-secret diagnostic when that denial is detected. diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..792dc37 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,45 @@ +# Frequently Asked Questions + +## Does v5.1.0 require a database migration? + +No. The schema and every migration file remain unchanged. + +## Should an existing v5.0.1 or v5.0.1.1 installation run the v5.1.0 installer? + +No. The wizard is for fresh installations. Preserve private configuration and encryption-key material, replace source files, and follow `UPGRADE_GUIDE.md`. + +## Why does the installer reject a URL containing `?query` or `#fragment`? + +The Base URL represents the application root. Query parameters and fragments are request-specific and can retain tokens or produce invalid generated URLs. + +## Are subdirectory installations supported? + +Yes. Values such as `https://example.com/licora` remain valid. + +## Why is an absolute directory path no longer shown? + +Production paths reveal server layout. The installer reports only whether the required location is writable. + +## Does v5.1.0 change API JSON or license keys? + +No. Primary and legacy API contracts, license format, generation, validation, encryption compatibility, and device behavior remain unchanged. + +## Which PHP versions are targeted? + +PHP 8.1, 8.2, and 8.3. CI also retains PHP 8.0 and 8.4 checks. + +## Are Apache, Nginx, and LiteSpeed supported? + +The PHP application is server-agnostic. Apache uses the included `.htaccess` rules. Nginx and LiteSpeed require equivalent deny rules documented in the deployment guides. + +## Which database servers are supported? + +Licora uses PDO MySQL and is designed for MySQL and MariaDB. + +## What directories must be writable? + +For a fresh wizard installation, `includes/` must be writable temporarily. Licora currently defines no separate upload, cache, or storage directory. + +## Can I delete historical logs during upgrade? + +No. v5.1.0 does not delete or rewrite historical application, API, authentication, license, or audit logs. diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 05892e4..ccc2cd7 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -108,3 +108,17 @@ After installation: ## Upgrade installations Existing v5.0.1 and v5.0.1.1 deployments must not run the first-run wizard. Preserve private configuration and encrypted-key material, replace application source, and follow `UPGRADE_GUIDE.md`. + +## v5.1.0 production-readiness checks + +Before public exposure: + +- Use PHP 8.1, 8.2, or 8.3 with required extensions. +- Keep the application read-only where practical. +- Grant temporary write access only to `includes/` during first installation. +- Confirm private configuration, installation flags, cron paths, audit files, and backups are not web-accessible. +- Use a Base URL without credentials, query parameters, or fragments. +- Restore restrictive permissions after installation. +- Review `COMPATIBILITY_MATRIX.md` for server-specific validation. + +Licora defines no dedicated upload, cache, or storage directory. v5.1.0 does not introduce one. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 089fa89..54ee78d 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -2,28 +2,76 @@ ## Versioning -Use semantic tags for public releases. Repository release numbers and the historical `APP_VERSION` API value are separate until a compatibility decision explicitly aligns them. +Licora uses semantic version tags. For this release, repository version, installer version, installation flag, stored installed version, and runtime `APP_VERSION` are aligned at `5.1.0`. -## Checklist +The release tag must point to a reviewed commit on `main`. Do not tag a dirty working tree or package uncommitted files. -- [ ] All changes are reviewed and feature-preserving or explicitly versioned. -- [ ] `bash scripts/validate.sh` passes. -- [ ] Disposable-database manual validation passes. +## Release gate + +- [ ] `git status --short` is empty. +- [ ] Pull-request review is complete. +- [ ] GitHub Actions passes for PHP 8.0, 8.1, 8.2, 8.3, and 8.4. +- [ ] `bash scripts/validate.sh` passes locally. +- [ ] Fresh production installation passes with Demo Data unchecked. +- [ ] Fresh demonstration installation passes with Demo Data checked. +- [ ] Existing v5.0.1.1 upgrade passes without running the installer. +- [ ] Admin login, password change, roles, API keys, licenses, devices, logs, settings, exports, backups, and cron entry points are tested. +- [ ] `/api/verify.php` is tested with valid, invalid, expired, suspended, and device-limit scenarios. - [ ] `database.sql` contains no operational data. -- [ ] No private config, API key, password, license key, device identifier, IP address, or backup is tracked. -- [ ] Migrations and rollback steps are documented. -- [ ] `CHANGELOG.md` and release notes are updated. -- [ ] Security findings are resolved or accurately disclosed. -- [ ] Screenshots are redacted. -- [ ] Archive contents and checksums are verified. +- [ ] No private configuration, API key, password, license key, device identifier, IP address, log, or backup is tracked. +- [ ] `CHANGELOG.md`, `RELEASE_NOTES_v5.1.0.md`, and repository metadata are current. +- [ ] The release ZIP and SHA-256 checksum are inspected. + +## Build the release archive -## Package +Run from the repository root after committing and tagging: ```bash -bash scripts/package-release.sh v5.0.0 +bash scripts/package-release.sh v5.1.0 v5.1.0 +``` + +The packager: + +1. validates the repository; +2. requires a clean tracked working tree; +3. packages only files tracked by the selected Git ref using `git archive`; +4. writes a SHA-256 checksum next to the ZIP. + +Default output: + +```text +../Licora-v5.1.0.zip +../Licora-v5.1.0.zip.sha256 ``` -The script creates a ZIP outside the source directory, excluding Git metadata, private configuration, caches, and generated archives. +## Tag and publish + +```bash +git switch main +git pull --ff-only origin main +bash scripts/validate.sh +git tag -a v5.1.0 -m "Licora v5.1.0 - Smart Installer and First-Run Wizard" +git push origin v5.1.0 +bash scripts/package-release.sh v5.1.0 v5.1.0 +gh release create v5.1.0 \ + ../Licora-v5.1.0.zip \ + ../Licora-v5.1.0.zip.sha256 \ + --verify-tag \ + --title "Licora v5.1.0 - Smart Installer and First-Run Wizard" \ + --notes-file RELEASE_NOTES_v5.1.0.md \ + --latest +``` + +Windows Command Prompt equivalents are provided in `RELEASE_COMMANDS_v5.1.0.md`. + +## Post-release verification + +- Confirm the GitHub Release is marked Latest. +- Download the published ZIP and compare its SHA-256 checksum. +- Extract it into a disposable directory. +- Confirm private/runtime files are absent. +- Run `bash scripts/validate.sh` from the extracted source. +- Perform one clean browser installation and one API smoke test from the published asset. ## Suggested release metadata diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index cfb3c7f..825e30e 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -14,7 +14,7 @@ For the sanitized schema, use the temporary local credentials and change them im ## API key is rejected -Use `X-API-Key`, remove whitespace, confirm the key is active and not expired, and ensure the stored hash corresponds to the original plaintext key. Bearer parsing is a known issue in this release. +Use `X-API-Key` or `Authorization: Bearer TOKEN`, remove whitespace, confirm the key is active and not expired, and ensure the stored hash corresponds to the original plaintext key. ## License is valid but verification fails @@ -35,3 +35,26 @@ Run it from the project context with the same environment variables as the web p ## Protected files are visible Apache may not be honoring `.htaccess`, or the server may be Nginx. Apply explicit deny rules from [SECURITY_DEPLOYMENT.md](SECURITY_DEPLOYMENT.md) immediately. + +## v5.1.0 quality and stability diagnostics + +### Installer reports that the private configuration location is not writable + +Grant the web-server process temporary write access to `includes/`. Do not make the complete application tree globally writable. The installer intentionally does not display the absolute path. + + +### Installer reports missing TRIGGER privilege + +The standard schema contains MySQL/MariaDB triggers. Use a database account that has the required `TRIGGER` privilege or move the deployment to hosting that permits the complete schema. Licora does not silently remove schema objects during a standard installation. + +### Base URL is rejected + +Use only the application root, such as `https://licenses.example.com`, `https://licenses.example.com/licora`, or `http://localhost/licora`. Do not include embedded credentials, query parameters, or fragments. + +### Installer displays a generic completion error + +Review the server error log using the same timestamp. Public output intentionally excludes SQL text, credentials, stack traces, and server paths. + +### Nginx or LiteSpeed exposes a protected path + +Reproduce the deny rules documented in `SECURITY_DEPLOYMENT.md`. Licora does not edit web-server configuration automatically. diff --git a/docs/UPGRADE_GUIDE.md b/docs/UPGRADE_GUIDE.md index aa865c0..21e28c8 100644 --- a/docs/UPGRADE_GUIDE.md +++ b/docs/UPGRADE_GUIDE.md @@ -59,3 +59,9 @@ A configured deployment with a temporarily unavailable database retains the exis - Settings - Installer lock - Legacy encrypted values + +## Release-version precedence + +The preserved `includes/config.local.php` file may contain the version recorded by an older installer. v5.1.0 resolves runtime release identity from the source before loading private configuration, so a preserved local `APP_VERSION` definition does not pin future source upgrades. Existing database values, application settings, security secrets, and explicit environment-based version overrides remain supported. + +Existing installation flags may continue recording the original installation version; they are installation records, not an upgrade ledger. diff --git a/includes/config.php b/includes/config.php index d9ec0fa..e959f03 100644 --- a/includes/config.php +++ b/includes/config.php @@ -19,12 +19,6 @@ header('Referrer-Policy: same-origin'); } -// Optional private local override. Keep this file outside public web root where possible. -$localConfig = __DIR__ . '/config.local.php'; -if (file_exists($localConfig)) { - require_once $localConfig; -} - if (!function_exists('env_value')) { function env_value($key, $default = '') { $value = getenv($key); @@ -32,6 +26,17 @@ function env_value($key, $default = '') { } } +// Resolve release identity before private local configuration is loaded. +// This prevents an installer-generated local configuration from pinning +// future source upgrades while retaining the APP_VERSION environment override. +if (!defined('APP_VERSION')) define('APP_VERSION', env_value('APP_VERSION', '5.1.0')); + +// Optional private local override. Keep this file outside public web root where possible. +$localConfig = __DIR__ . '/config.local.php'; +if (file_exists($localConfig)) { + require_once $localConfig; +} + // ডেটাবেস কনফিগারেশন if (!defined('DB_HOST')) define('DB_HOST', env_value('LICENSE_DB_HOST', env_value('DB_HOST', 'localhost'))); if (!defined('DB_PORT')) define('DB_PORT', (int)env_value('LICENSE_DB_PORT', env_value('DB_PORT', 3306))); @@ -40,9 +45,8 @@ function env_value($key, $default = '') { if (!defined('DB_PASS')) define('DB_PASS', env_value('LICENSE_DB_PASS', env_value('DB_PASS', ''))); // এপ্লিকেশন সেটিংস -if (!defined('APP_NAME')) define('APP_NAME', env_value('APP_NAME', 'License System')); +if (!defined('APP_NAME')) define('APP_NAME', env_value('APP_NAME', 'Licora')); if (!defined('APP_URL')) define('APP_URL', env_value('APP_URL', 'http://localhost')); -if (!defined('APP_VERSION')) define('APP_VERSION', env_value('APP_VERSION', '5.1.0')); if (!defined('APP_TIMEZONE')) define('APP_TIMEZONE', env_value('APP_TIMEZONE', 'Asia/Dhaka')); if (!defined('APP_LOCALE')) define('APP_LOCALE', env_value('APP_LOCALE', 'en')); if (!defined('MAIL_FROM_NAME')) define('MAIL_FROM_NAME', env_value('MAIL_FROM_NAME', APP_NAME)); @@ -90,18 +94,13 @@ function env_value($key, $default = '') { set_exception_handler(function($exception) { http_response_code(500); - error_log('Uncaught exception: ' . $exception->getMessage() . ' in ' . $exception->getFile() . ':' . $exception->getLine()); + $reference = substr(hash('sha256', microtime(true) . get_class($exception) . $exception->getLine()), 0, 12); + error_log( + 'Licora exception [' . $reference . '] [' . get_class($exception) . '] at ' + . basename((string)$exception->getFile()) . ':' . (int)$exception->getLine() + ); header('Content-Type: application/json; charset=utf-8'); - if (ENVIRONMENT === 'production') { - echo json_encode(['error' => 'Internal Server Error']); - } else { - echo json_encode([ - 'error' => 'Internal Server Error', - 'message' => $exception->getMessage(), - 'file' => $exception->getFile(), - 'line' => $exception->getLine() - ]); - } + echo json_encode(['error' => 'Internal Server Error']); exit; }); ?> diff --git a/includes/installation.php b/includes/installation.php index 2ef47c0..a14d14e 100644 --- a/includes/installation.php +++ b/includes/installation.php @@ -57,6 +57,50 @@ function licora_installation_secret_is_usable($value): bool } } +if (!function_exists('licora_installer_generated_secret_is_valid')) { + function licora_installer_generated_secret_is_valid($value): bool + { + return preg_match('/^[a-f0-9]{64}$/i', trim((string)$value)) === 1; + } +} + +if (!function_exists('licora_installer_public_error')) { + function licora_installer_public_error(Throwable $error): string + { + $message = trim($error->getMessage()); + $normalized = strtolower($message); + + $allowed = [ + 'Licora is already installed.', + 'The target database already contains Licora tables.', + 'Generated installation secrets are invalid.', + 'Database schema is unavailable.', + 'The database account lacks the TRIGGER privilege required by the Licora schema.', + 'The database credentials or required database privileges were rejected.', + 'Installation request could not be completed. Review the server log and try again.', + ]; + if (in_array($message, $allowed, true)) { + return $message; + } + + // Shared hosts frequently permit tables but deny triggers. Return a + // useful non-secret diagnostic instead of exposing the SQL exception. + if (strpos($normalized, 'trigger command denied') !== false + || (strpos($normalized, 'trigger') !== false && strpos($normalized, 'permission') !== false) + || (strpos($normalized, 'trigger') !== false && strpos($normalized, 'privilege') !== false)) { + return 'The database account lacks the TRIGGER privilege required by the Licora schema.'; + } + + if (strpos($normalized, 'access denied') !== false + || strpos($normalized, 'permission denied') !== false + || strpos($normalized, 'command denied') !== false) { + return 'The database credentials or required database privileges were rejected.'; + } + + return 'Installation request could not be completed. Review the server log and try again.'; + } +} + if (!function_exists('licora_installation_environment_configured')) { function licora_installation_environment_configured(): bool { @@ -288,13 +332,20 @@ function licora_installer_is_locked(?string $root = null): bool function licora_installer_requirements(?string $root = null): array { $root = licora_installation_root($root); + $includesPath = $root . '/includes'; + + // PHP caches stat-family results. Refresh this path before evaluating + // permissions so persistent web-server workers cannot report stale ACL state. + clearstatcache(true, $includesPath); + $includesWritable = is_writable($includesPath); + $requirements = [ ['label' => 'PHP 8.0 or newer', 'status' => version_compare(PHP_VERSION, '8.0.0', '>='), 'required' => true, 'detail' => PHP_VERSION], ['label' => 'PDO extension', 'status' => extension_loaded('pdo'), 'required' => true, 'detail' => extension_loaded('pdo') ? 'Loaded' : 'Missing'], ['label' => 'PDO MySQL extension', 'status' => extension_loaded('pdo_mysql'), 'required' => true, 'detail' => extension_loaded('pdo_mysql') ? 'Loaded' : 'Missing'], ['label' => 'OpenSSL extension', 'status' => extension_loaded('openssl'), 'required' => true, 'detail' => extension_loaded('openssl') ? 'Loaded' : 'Missing'], ['label' => 'JSON extension', 'status' => extension_loaded('json'), 'required' => true, 'detail' => extension_loaded('json') ? 'Loaded' : 'Missing'], - ['label' => 'Writable includes directory', 'status' => is_writable($root . '/includes'), 'required' => true, 'detail' => $root . '/includes'], + ['label' => 'Writable includes directory', 'status' => $includesWritable, 'required' => true, 'detail' => $includesWritable ? 'Writable' : 'Not writable'], ['label' => 'Readable database schema', 'status' => is_readable($root . '/database.sql'), 'required' => true, 'detail' => 'database.sql'], ['label' => 'HTTPS transport', 'status' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'required' => false, 'detail' => 'Required for production'], ]; @@ -431,11 +482,20 @@ function licora_installer_validate_application(array $input): array if (!preg_match('/^[A-Za-z]{2,3}(?:[_-][A-Za-z]{2})?$/', $locale)) { $errors[] = 'Locale must use a value such as en or en_US.'; } - if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('#^https?://#i', $url)) { - $errors[] = 'Base URL must be a valid HTTP or HTTPS URL.'; - } - if (strlen($mailFrom) < 2 || strlen($mailFrom) > 120) { - $errors[] = 'Mail From Name must be between 2 and 120 characters.'; + $urlParts = parse_url($url); + if (!filter_var($url, FILTER_VALIDATE_URL) + || !preg_match('#^https?://#i', $url) + || !is_array($urlParts) + || empty($urlParts['host'])) { + $errors[] = 'Base URL must be a valid HTTP or HTTPS application URL.'; + } elseif (isset($urlParts['user']) + || isset($urlParts['pass']) + || isset($urlParts['query']) + || isset($urlParts['fragment'])) { + $errors[] = 'Base URL must not contain credentials, query parameters, or fragments.'; + } + if (strlen($mailFrom) < 2 || strlen($mailFrom) > 120 || preg_match('/[\r\n]/', $mailFrom)) { + $errors[] = 'Mail From Name must be 2-120 characters without line breaks.'; } return $errors; } @@ -641,6 +701,12 @@ function licora_installer_seed_demo(PDO $pdo, int $adminId, array $data): array function licora_installer_finalize(?string $root, array $data): array { $root = licora_installation_root($root); + foreach (['app_key', 'encryption_key', 'csrf_secret', 'jwt_secret'] as $secretName) { + if (!isset($data['secrets'][$secretName]) + || !licora_installer_generated_secret_is_valid($data['secrets'][$secretName])) { + throw new RuntimeException('Generated installation secrets are invalid.'); + } + } $db = $data['db']; $databaseCreated = false; $snapshot = []; @@ -777,8 +843,9 @@ function licora_installer_finalize(?string $root, array $data): array licora_installer_cleanup_new_tables($pdo, $snapshot); } } - error_log('Licora installer finalization failed [' . get_class($e) . '].'); - throw new RuntimeException('Installation could not be completed. Verify database permissions and writable directories, then try again.'); + $reference = substr(hash('sha256', microtime(true) . get_class($e) . $e->getLine()), 0, 12); + error_log('Licora installer finalization failed [' . $reference . '] [' . get_class($e) . '].'); + throw new RuntimeException(licora_installer_public_error($e), 0, $e); } } } diff --git a/install.php b/install.php index 41de91c..fa627c7 100644 --- a/install.php +++ b/install.php @@ -228,7 +228,7 @@ function installer_render_locked(): void } } catch (Throwable $e) { error_log('Licora installer request failed [' . get_class($e) . '].'); - $error = $e->getMessage() !== '' ? $e->getMessage() : 'Installation request failed.'; + $error = licora_installer_public_error($e); } } } diff --git a/scripts/package-release.sh b/scripts/package-release.sh index f30faa8..0bc42e1 100644 --- a/scripts/package-release.sh +++ b/scripts/package-release.sh @@ -2,27 +2,75 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -NAME="${1:-vibtools-license-management-system-v5.0.0-github-ready}" -OUT="${2:-$ROOT/../${NAME}.zip}" +VERSION="${1:-v5.1.0}" +REF="${2:-$VERSION}" +ARCHIVE_ROOT="Licora-${VERSION#v}" +OUT="${3:-$ROOT/../${ARCHIVE_ROOT}.zip}" +CHECKSUM_OUT="${OUT}.sha256" cd "$ROOT" -bash scripts/validate.sh -python3 - "$ROOT" "$OUT" "$NAME" <<'PY_PACKAGE_RELEASE' +resolve_python_command() { + if command -v python3 >/dev/null 2>&1; then + PYTHON_CMD=(python3) + elif command -v python >/dev/null 2>&1; then + PYTHON_CMD=(python) + elif command -v py >/dev/null 2>&1; then + PYTHON_CMD=(py -3) + else + echo "Python 3 is required for release validation and packaging." >&2 + exit 1 + fi + + if ! "${PYTHON_CMD[@]}" -c 'import sys; raise SystemExit(0 if sys.version_info.major == 3 else 1)'; then + echo "The selected Python command is not Python 3." >&2 + exit 1 + fi +} + +resolve_python_command + +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "Release packaging requires a Git working tree." >&2 + exit 1 +fi + +if ! git rev-parse --verify "${REF}^{commit}" >/dev/null 2>&1; then + echo "Unknown release ref: ${REF}" >&2 + exit 1 +fi + +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "Tracked working-tree changes are present. Commit or restore them before packaging." >&2 + exit 1 +fi + +TEMP_DIR="$(mktemp -d)" +cleanup() { + rm -rf "$TEMP_DIR" +} +trap cleanup EXIT + +mkdir -p "$TEMP_DIR/source" +git archive "$REF" | tar -x -C "$TEMP_DIR/source" +( + cd "$TEMP_DIR/source" + bash scripts/validate.sh +) + +mkdir -p "$(dirname "$OUT")" +rm -f "$OUT" "$CHECKSUM_OUT" +git archive --format=zip --prefix="${ARCHIVE_ROOT}/" -o "$OUT" "$REF" + +"${PYTHON_CMD[@]}" - "$OUT" "$CHECKSUM_OUT" <<'PY_CHECKSUM' from pathlib import Path -import sys, zipfile -root = Path(sys.argv[1]).resolve() -out = Path(sys.argv[2]).resolve() -archive_root = Path(sys.argv[3]) -exclude_parts = {'.git', '.idea', '.vscode', '__pycache__', 'vendor', 'node_modules'} -exclude_names = {'includes/config.local.php', '.env'} -with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as archive: - for path in sorted(root.rglob('*')): - if not path.is_file(): - continue - rel = path.relative_to(root) - if any(part in exclude_parts for part in rel.parts) or rel.as_posix() in exclude_names: - continue - archive.write(path, archive_root / rel) -print(out) -PY_PACKAGE_RELEASE +import hashlib +import sys + +archive = Path(sys.argv[1]).resolve() +checksum_file = Path(sys.argv[2]).resolve() +digest = hashlib.sha256(archive.read_bytes()).hexdigest() +checksum_file.write_text(f"{digest} {archive.name}\n", encoding="ascii") +print(archive) +print(checksum_file) +PY_CHECKSUM diff --git a/scripts/validate.sh b/scripts/validate.sh index 2d981f2..602af1e 100644 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -4,42 +4,70 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" -echo "[1/8] Checking required files" +resolve_python_command() { + if command -v python3 >/dev/null 2>&1; then + PYTHON_CMD=(python3) + elif command -v python >/dev/null 2>&1; then + PYTHON_CMD=(python) + elif command -v py >/dev/null 2>&1; then + PYTHON_CMD=(py -3) + else + echo "Python 3 is required for release validation and packaging." >&2 + exit 1 + fi + + if ! "${PYTHON_CMD[@]}" -c 'import sys; raise SystemExit(0 if sys.version_info.major == 3 else 1)'; then + echo "The selected Python command is not Python 3." >&2 + exit 1 + fi +} + +resolve_python_command + +echo "[1/9] Checking required files" required=( README.md LICENSE SECURITY.md CONTRIBUTING.md database.sql + CHANGELOG.md RELEASE_NOTES_v5.0.1.1.md RELEASE_NOTES_v5.1.0.md + PHASE2_INSTALLER_SUMMARY.md REPOSITORY_METADATA.md RELEASE_COMMANDS_v5.1.0.md + audit/V5.1.0_RELEASE_DELTA_AUDIT.md audit/V5.1.0_DELTA_PATCH_MANIFEST.txt audit/V5.1.0_DELTA_FILE_SHA256SUMS.txt includes/config.php includes/installation.php install.php install/index.php - tests/security_smoke.php tests/compatibility_regression.php tests/installer_smoke.php - RELEASE_NOTES_v5.0.1.1.md RELEASE_NOTES_v5.1.0.md PHASE2_INSTALLER_SUMMARY.md - docs/INSTALLER_ARCHITECTURE.md docs/FIRST_RUN_GUIDE.md docs/UPGRADE_GUIDE.md docs/DEMO_DATA.md + scripts/package-release.sh + tests/security_smoke.php tests/compatibility_regression.php tests/installer_smoke.php tests/release_readiness.php + docs/CONFIGURATION.md docs/RELEASE.md docs/INSTALLER_ARCHITECTURE.md + docs/FIRST_RUN_GUIDE.md docs/UPGRADE_GUIDE.md docs/DEMO_DATA.md + docs/FAQ.md docs/COMPATIBILITY_MATRIX.md docs/TROUBLESHOOTING.md ) for path in "${required[@]}"; do test -f "$path" || { echo "Missing required file: $path" >&2; exit 1; } done -echo "[2/8] PHP syntax" +echo "[2/9] PHP syntax" while IFS= read -r -d '' file; do php -l "$file" >/dev/null done < <(find . -type f -name '*.php' -not -path './vendor/*' -print0 | sort -z) -echo "[3/8] Security smoke test" +echo "[3/9] Security smoke test" php tests/security_smoke.php -echo "[4/8] Compatibility regression test" +echo "[4/9] Compatibility regression test" php tests/compatibility_regression.php -echo "[5/8] Installer smoke test" +echo "[5/9] Installer smoke test" php tests/installer_smoke.php -echo "[6/8] JavaScript syntax" +echo "[6/9] Release readiness test" +php tests/release_readiness.php + +echo "[7/9] JavaScript syntax" if command -v node >/dev/null 2>&1; then node --check admin/assets/js/admin-ui.js else echo "Node.js not installed; JavaScript syntax check skipped." fi -echo "[7/8] Public-release marker scan" -python3 - <<'PY_PUBLIC_MARKER_SCAN' +echo "[8/9] Public-release marker scan" +"${PYTHON_CMD[@]}" - <<'PY_PUBLIC_MARKER_SCAN' from pathlib import Path import base64 @@ -67,8 +95,8 @@ if violations: raise SystemExit('Private deployment marker detected in: ' + ', '.join(sorted(violations))) PY_PUBLIC_MARKER_SCAN -echo "[8/8] SQL seed scope" -python3 - <<'PY_VALIDATE_SQL' +echo "[9/9] SQL seed scope" +"${PYTHON_CMD[@]}" - <<'PY_VALIDATE_SQL' from pathlib import Path import re text = Path('database.sql').read_text(encoding='utf-8') diff --git a/tests/installer_smoke.php b/tests/installer_smoke.php index f6a34f6..831a754 100644 --- a/tests/installer_smoke.php +++ b/tests/installer_smoke.php @@ -52,6 +52,21 @@ ]; $assert(licora_installer_validate_application($validApplication) === [], 'valid application configuration accepted'); +$unsafeUrl = $validApplication; +$unsafeUrl['base_url'] = 'https://user:password@licenses.example.com/licora'; +$assert(licora_installer_validate_application($unsafeUrl) !== [], 'base URL credentials rejected'); +$unsafeUrl['base_url'] = 'https://licenses.example.com/licora?token=secret'; +$assert(licora_installer_validate_application($unsafeUrl) !== [], 'base URL query rejected'); +$unsafeUrl['base_url'] = 'https://licenses.example.com/licora#fragment'; +$assert(licora_installer_validate_application($unsafeUrl) !== [], 'base URL fragment rejected'); + +$unsafeMail = $validApplication; +$unsafeMail['mail_from_name'] = "Licora\r\nBcc: attacker@example.com"; +$assert(licora_installer_validate_application($unsafeMail) !== [], 'mail-from line break rejected'); + +$assert(licora_installer_generated_secret_is_valid(str_repeat('a', 64)), 'generated secret validation accepted'); +$assert(!licora_installer_generated_secret_is_valid('replace-me'), 'invalid generated secret rejected'); + $sql = "CREATE TABLE sample (id INT);\nDELIMITER $$\nCREATE TRIGGER sample_trigger BEFORE INSERT ON sample FOR EACH ROW SET NEW.id = 1$$\nDELIMITER ;\nINSERT INTO sample (id) VALUES (1);\n"; $statements = licora_installer_sql_statements($sql); $assert(count($statements) === 3, 'schema parser handles custom delimiters'); @@ -86,6 +101,44 @@ $tempRoot = sys_get_temp_dir() . '/licora-installer-test-' . bin2hex(random_bytes(5)); mkdir($tempRoot . '/includes', 0700, true); +$tempIncludesPath = $tempRoot . '/includes'; + +$assert(is_writable($tempIncludesPath), 'temporary includes directory starts writable'); + +if (DIRECTORY_SEPARATOR === '/') { + $permissionsChanged = @chmod($tempIncludesPath, 0500); + clearstatcache(true, $tempIncludesPath); + + // Root containers can remain writable even after chmod(0500). Only assert + // the restricted state when the current runtime actually loses write access. + if ($permissionsChanged && !is_writable($tempIncludesPath)) { + try { + $restrictedRequirements = licora_installer_requirements($tempRoot); + $restrictedIncludesRequirement = null; + + foreach ($restrictedRequirements as $requirement) { + if (($requirement['label'] ?? '') === 'Writable includes directory') { + $restrictedIncludesRequirement = $requirement; + break; + } + } + + $assert( + is_array($restrictedIncludesRequirement) + && empty($restrictedIncludesRequirement['status']) + && ($restrictedIncludesRequirement['detail'] ?? '') === 'Not writable', + 'installer requirements refresh and report restricted includes permissions' + ); + } finally { + @chmod($tempIncludesPath, 0700); + clearstatcache(true, $tempIncludesPath); + } + } else { + @chmod($tempIncludesPath, 0700); + clearstatcache(true, $tempIncludesPath); + } +} + $assert(licora_installation_write_flag($tempRoot, '5.1.0'), 'installation flag written atomically'); $flag = json_decode((string)file_get_contents($tempRoot . '/includes/.licora-installed'), true); $assert(($flag['product'] ?? '') === 'Licora', 'installation flag identifies Licora'); diff --git a/tests/release_readiness.php b/tests/release_readiness.php new file mode 100644 index 0000000..0f70e37 --- /dev/null +++ b/tests/release_readiness.php @@ -0,0 +1,140 @@ + '5.1.0'") !== false, 'generated installer configuration targets v5.1.0'); +$assert(strpos($installer, 'Professional installation wizard for Licora v5.1.0') !== false, 'installer branding targets v5.1.0'); + +foreach ([ + "'message' => \$exception->getMessage()", + "'file' => \$exception->getFile()", + "'line' => \$exception->getLine()", +] as $unsafeOutput) { + $assert(strpos($config, $unsafeOutput) === false, 'exception response hides technical detail: ' . $unsafeOutput); +} +$assert(strpos($config, "echo json_encode(['error' => 'Internal Server Error']);") !== false, 'unhandled exception response is generic'); +$assert(strpos($installer, 'licora_installer_public_error($e)') !== false, 'installer exceptions use safe public mapping'); + +$requirements = licora_installer_requirements($root); +foreach ($requirements as $requirement) { + $detail = (string)($requirement['detail'] ?? ''); + $assert(strpos($detail, $root) === false, 'installer requirement detail hides server paths'); +} + +$clearStatMarker = 'clearstatcache(true, $includesPath);'; +$writableMarker = '$includesWritable = is_writable($includesPath);'; +$clearStatPosition = strpos($installation, $clearStatMarker); +$writablePosition = strpos($installation, $writableMarker); +$assert(substr_count($installation, $clearStatMarker) === 1, 'installer refreshes cached includes status exactly once'); +$assert(substr_count($installation, $writableMarker) === 1, 'installer evaluates includes writability exactly once'); +$assert( + $clearStatPosition !== false && $writablePosition !== false && $clearStatPosition < $writablePosition, + 'installer clears cached status before checking writability' +); + +$validApplication = [ + 'app_name' => 'Licora', + 'timezone' => 'Asia/Dhaka', + 'locale' => 'en', + 'base_url' => 'https://licenses.example.com/licora', + 'mail_from_name' => 'Licora', +]; +$assert(licora_installer_validate_application($validApplication) === [], 'valid application configuration remains accepted'); + +foreach ([ + 'https://user:password@licenses.example.com/licora', + 'https://licenses.example.com/licora?token=secret', + 'https://licenses.example.com/licora#fragment', +] as $unsafeUrl) { + $candidate = $validApplication; + $candidate['base_url'] = $unsafeUrl; + $assert(licora_installer_validate_application($candidate) !== [], 'unsafe base URL rejected: ' . $unsafeUrl); +} + +$unsafeMail = $validApplication; +$unsafeMail['mail_from_name'] = "Licora\r\nBcc: attacker@example.com"; +$assert(licora_installer_validate_application($unsafeMail) !== [], 'mail header control characters rejected'); + +$assert(licora_installer_generated_secret_is_valid(str_repeat('a', 64)), 'generated 64-character hexadecimal secret accepted'); +$assert(!licora_installer_generated_secret_is_valid('replace-me'), 'placeholder secret rejected'); + +$technical = new RuntimeException('SQLSTATE[HY000] password=secret /private/server/path'); +$public = licora_installer_public_error($technical); +foreach (['SQLSTATE', 'password', 'secret', '/private/server/path'] as $sensitive) { + $assert(strpos($public, $sensitive) === false, 'public installer error redacts: ' . $sensitive); +} +$triggerError = new RuntimeException("SQLSTATE[42000]: TRIGGER command denied to user 'demo'"); +$assert( + licora_installer_public_error($triggerError) === 'The database account lacks the TRIGGER privilege required by the Licora schema.', + 'trigger privilege denial receives a safe actionable diagnostic' +); + +foreach ([ + 'RELEASE_NOTES_v5.1.0.md', + 'PHASE2_INSTALLER_SUMMARY.md', + 'RELEASE_COMMANDS_v5.1.0.md', + 'docs/FAQ.md', + 'docs/COMPATIBILITY_MATRIX.md', +] as $path) { + $assert(is_file($root . '/' . $path), 'release documentation exists: ' . $path); +} + +$assert(strpos($releaseNotes, 'Licora v5.1.0') !== false, 'release notes identify v5.1.0'); +$assert(strpos($changelog, '## [5.1.0] - 2026-08-06') !== false, 'changelog contains the publication date'); +$assert(strpos($configuration, '`APP_VERSION` | `APP_VERSION` | `5.1.0`') !== false, 'configuration reference matches runtime version'); +$assert(strpos($releaseGuide, 'scripts/package-release.sh v5.1.0 v5.1.0') !== false, 'release guide uses the v5.1.0 packager command'); +$assert(strpos($packager, 'git archive --format=zip') !== false, 'release package is created from a Git ref'); +$assert(strpos($packager, 'git diff --quiet') !== false, 'release packager rejects tracked working-tree changes'); + +foreach ([ + 'includes/config.php', + 'includes/installation.php', + 'install.php', + 'config.sample.php', + 'CHANGELOG.md', + 'RELEASE_NOTES_v5.1.0.md', +] as $path) { + $content = $read($path); + $assert(strpos($content, '5.1.1') === false, 'v5.1.0 release file does not contain future version marker: ' . $path); +} + +if ($failures !== []) { + fwrite(STDERR, "Release readiness test failed:\n- " . implode("\n- ", $failures) . "\n"); + exit(1); +} + +echo "Release readiness checks passed.\n";