Raiju/ROUTING.md
2026-10-25 06:05:42 +03:00

184 lines
11 KiB
Markdown

# Routing and deployment verification
Read this before changing URLs, redirects, `.htaccess`, `router.php`,
`route.php`, login destinations, or the directory layout of request handlers.
Apply it to site migrations and core upgrades as well as routing fixes.
## Why local success is insufficient
Production routing has multiple layers: the hosting proxy, Apache's rewrite
and directory handling, and Raiju's PHP front controller. The development
server executes `router.php` and does not interpret `.htaccess`. Passing local
PHP tests therefore does not prove that the uploaded installation routes
correctly. Verify affected routes under Apache and on the deployed host.
The concrete regression found in September 2026 was a filesystem collision:
`admin/dashboard.php` is the request handler, while `admin/dashboard/scripts/`
holds implementation fragments. Login redirected to `dashboard`. Apache
redirected `/admin/dashboard` to `/admin/dashboard/` with HTTP 301; that URL
then fell through to `route.php` and returned 404. `/admin/` continued to work
because it has an actual `index.php`. The PHP development server served the
slashless dashboard successfully and concealed the broken login flow.
The fix sends login directly to `dashboard.php` and redirects both clean URL
aliases to it. Do not change the destination back to `dashboard` without
reworking and testing the complete routing contract below.
Site-specific news/event detail routes can have a similar ordering problem
when physical listing templates compete with nested detail URLs. Preserve
the site's explicit detail rules when merging updated core rewrite rules.
## Routing contract
- Use explicit PHP URLs for internal admin endpoints that share their name
with implementation directories. New fragment directories must not silently
change existing public URLs. Check for collisions before adding them.
- Keep `/admin/` as a directory URL. The current dashboard destination is
`/admin/dashboard.php`; `/admin/dashboard` and `/admin/dashboard/` are
compatibility aliases that return HTTP 308 with query parameters preserved.
- `/admin` redirects to `/admin/`; `/login` and `/login/` are aliases for
`/admin/`. Login aliases must not be accepted as post-login return targets.
- Physical directories with an `index.php` get their missing slash. If a PHP
handler shares its name with a directory, both clean aliases resolve to the
explicit `.php` handler. A PHP handler with an extra trailing slash also
resolves to its explicit filename. These HTTP 308 redirects retain POST
method/body and query parameters. They do not change JSON-backed page slugs.
- `includes/routing-helper.php` implements the shared PHP fallback used by
`router.php` and `route.php`; `.htaccess` applies the equivalent rules before
Apache's generic file/directory handling. Update these together and include
the helper in upgrades.
- Canonicalization is route-specific. Do not add a blanket rule to append or
remove slashes across the site, or disable directory handling globally to
conceal one collision. A trailing slash changes how browsers resolve relative
assets, forms, API requests and login redirects.
- Preserve existing security rules before routing shortcuts. Put explicit
collision/detail rules before generic file/directory handling. Retain
`Options -MultiViews` and the existing front-controller rewrite termination;
changing either requires Apache verification.
- Keep Apache and `router.php` behavior aligned for paths they both serve.
`route.php` handles dynamic content; it should not interpret admin fragment
directory names as public page slugs.
- Consider cached permanent redirects. Do not introduce an inverse redirect
that loops with an older cached 301. The explicit `dashboard.php` target
avoids the previous `/admin/dashboard` to `/admin/dashboard/` redirect.
- Merge `.htaccess` deliberately during upgrades and include hidden files in
upload checks. Preserve customer security, redirects and news/event rules.
Uploading only PHP files can leave obsolete production routing in place.
- The bundled Apache substitutions use root-absolute paths. Do not claim
subdirectory hosting works based on a root installation test; verify rewrite
targets, cookies, assets and redirects under the actual deployment prefix.
## Hosting proxies and internal folders
`.htaccess` protects `content/`, `backups/`, `cli/`, `tests/` and the media
trash folders only for requests that reach Apache. Some hosting panels put
nginx in front of Apache and let it deliver static files such as `.json`,
`.txt` and `.zip` directly from disk. Those requests never reach Apache, so
`content/users.json` (password hashes), `content/settings.json` (SMTP password,
web cron token), `content/mails.json` (form submissions) and backup ZIPs become
public. This happened on a HestiaCP staging server in September 2026: a test
JSON file in `content/` returned HTTP 200 from nginx, while a PHP file in the
same folder returned 403 from Apache.
Block the folders in nginx as well. `^~` makes these prefix locations win over
the panel's regular-expression location for static files:
```nginx
location ^~ /content/ { deny all; }
location ^~ /backups/ { deny all; }
location ^~ /cli/ { deny all; }
location ^~ /tests/ { deny all; }
location ^~ /assets/images-trash/ { deny all; }
location ^~ /assets/audio-trash/ { deny all; }
location ^~ /assets/videos-trash/ { deny all; }
location ^~ /assets/documents-trash/ { deny all; }
```
- **HestiaCP:** save the lines as
`/home/<user>/conf/web/<domain>/nginx.ssl.conf_raiju` (for plain HTTP also
as `nginx.conf_raiju`); HestiaCP includes these files in the domain's
server block. Then run `nginx -t && systemctl reload nginx` as root.
- **Plesk:** the default "Smart static files processing" asks Apache first, so
`.htaccess` applies. With "Serve static files directly by nginx" switched on,
requests for the listed extensions never reach Apache. Keep the option
switched off, or add the lines under "Apache & nginx Settings" → "Additional
nginx directives".
- For an installation in a subdirectory, prefix each path with it.
Check this after the first upload and after hosting changes. Dashboard →
System status → Security requests short-lived probe files through the site's
own address and shows these rules for the current installation. A missing file
is no test: nginx passes unknown files to Apache, which answers 403. To check by
hand, request an existing file, for example
`curl -I https://example.com/content/settings.json`, and expect 403 or 404.
`python3 tests/security-check-smoke.py --apache` reproduces the proxy locally
in front of Apache and PHP-FPM, with and without these rules.
## Required verification
For routing or request-handler directory changes, run the focused regression:
```sh
python3 tests/system-smoke.py
python3 tests/routing-smoke.py
python3 tests/routing-smoke.py --apache
```
These suites use disposable accounts and content, never live credentials.
The default routing suite runs on the PHP development server; `--apache` runs
the same contract against Apache and PHP-FPM on temporary loopback ports using
the candidate `.htaccess`. It does not alter the machine's web-server services.
Set `HTTPD`, `PHP_FPM` and `HTTPD_MODULE_DIR` if their paths are not detected.
The separate Apache CI job runs this fixture too. Do not describe PHP-only
results as Apache coverage. Repeat the relevant checks on the deployed host
after an authorized upload; local Apache cannot verify hosting proxy rules.
| Check | Expected result |
| --- | --- |
| Guest opens `/admin/` | Login form, HTTP 200 |
| `/admin`, `/login`, `/login/` | HTTP 308 to `/admin/`; final login form and CSS load |
| Successful login through the form | Redirect reaches the real dashboard, HTTP 200; no intermediate 404 or loop |
| `/admin/dashboard`, `/admin/dashboard/` | HTTP 308 to `/admin/dashboard.php`; query string preserved |
| Dashboard request without a session | Redirect to login; no dashboard data exposed |
| Dashboard request with a valid session | Dashboard HTML, HTTP 200 |
| Actual admin CSS, JS, form/API URLs after navigation | Correct paths and expected content types; authenticated API still works |
| Form POST to directory URL without slash | HTTP 308; followed request retains method/body and query |
| Add fragment directory beside an existing PHP endpoint | Both clean aliases resolve to `.php`; HTML and relative CSS still work |
| Existing language homepage and nested page, with/without slash | Established site behavior, correct assets and links |
| Existing news/event listing and detail URLs in each supported language | Established site behavior; detail request reaches its intended handler |
| Unknown public and admin URLs | HTTP 404, not a successful empty page |
| Config, content and implementation-only resources | Existing access protections remain effective |
| Existing internal files through the public address, behind any hosting proxy (`/content/settings.json`, a backup ZIP) | HTTP 403 or 404; System status → Security reports no public folders |
Inspect the first response **and** the followed redirect chain. A final HTTP
200 alone may be a login or error page, so check the returned content. Use GET
requests, preserve session cookies during login tests, include a query string
such as `?tab=content&probe=a%2Fb`, and check both a fresh browser session and
previously used aliases. Verify relative assets from the final browser URL.
Record the tested code revision or file hashes, server/PHP versions, relevant
hosting prefix/proxy, request paths, statuses, Location headers and final page.
Keep test credentials and session cookies out of the evidence. If Apache or
post-upload checks have not run, state that explicitly as outstanding
verification rather than reporting a production fix as verified.
## Local verification — 2026-09-15
The candidate working tree passed all 19 smoke suites with PHP 8.5.4 and the
additional routing suite under Apache 2.4.67 + PHP-FPM 8.5.4 on macOS. The
installation was at the document root, on temporary loopback ports, without a
hosting proxy. The executable assertions in `tests/routing-smoke.py` record
the request paths, expected first-response statuses/Location values, followed
login/API content, CSS types, and POST payload retention. No live host or
subdirectory installation was tested. The new GitHub Apache job is configured;
a remote CI run has not been triggered from this local check.
Tested SHA-256 values:
```text
.htaccess 1e8a86df0fb6304f7c6cc0189ad2813e0e657c4a49ba4694750d8241a5840d42
includes/routing-helper.php c2d7aa3ff89fbf7ae89d06208e8c94801948d98ea1abfd0f008a7ef1e97b3fc0
router.php 831613fc9e35f2c252d7ebe52a56c8d01e2b8af427ae0c16470de766a6f64e6f
route.php 9709439626c7cbf7716154180f9e8244894a9580815c120fd6c9a799d3312033
```