# SRP — Full Clean Pack (2026-06-29)

Complete, production-ready source of the Smart Redirect Proxy application.
Unzip at the deployment root. `composer check` is green (PSR-12, PHPStan level
max baseline-aware, 64 tests / 128 assertions).

## What's included
All three modules (`public/`, `statistics/`, `redirect/`), shared bootstrap
(`connection_pdo.php`, `env.php`, …), schema + migrations, quality-gate config,
the full `tests/` suite, assets, and docs.

## Deliberately excluded (regenerate / never ship)
| Excluded | Why / how to restore |
|---|---|
| `vendor/` | Third-party libs — run `composer install --no-dev --optimize-autoloader` |
| `.env`, `.env.lock` | **Secrets** — create from your own values (see `.env.example` if present / `INSTALL.md`) |
| `redirect/databases/GeoLite2-Country.mmdb` | 9.6 MB GeoIP data — run `php redirect/update-geoip.php` |
| `redirect/logs/cache-cleanup.log`, `statistics/temp/*.json` | Runtime output (`.gitkeep` retained) |
| `.phpunit.cache/`, prior `*.zip` | Build/test cache and earlier packs |

## First deploy
```bash
composer install --no-dev --prefer-dist --optimize-autoloader
# create .env from your secrets (DB, CF, admin/API keys) — see INSTALL.md
mysql < schema.sql                                  # fresh install
php migrations/003_add_cf_cert_installed.php        # + any pending migrations
php migrations/004_clickrecord_unique_click_date.php --dry-run   # verify prefix-collision=0, then run without --dry-run
php redirect/update-geoip.php                       # fetch GeoIP DB
# cron: setup-cron.sh installs cache cleanup + GeoIP refresh
```

## Verify (dev)
```bash
composer install        # incl. dev tools
composer check          # cs + stan + 64 tests
```

## Notes
- This pack reflects all changes from the 2026-06-22 session (addon-domain CF
  toggle, multi-domain add 1–5, auto CF-status poll + one-time auto Origin-SSL,
  the `cf_cert_installed` column/migration, the GeoResolver handler-leak fix,
  and the quality-gate scaffolding). See `CLAUDE.md`.
- 2026-06-23 hardening session: fixed the unresolvable-token infinite redirect
  loop (`redirect/index.php` host-guard), closed the fail-open auth on
  `public/api/ixg.php` (now session-or-bearer, fail-closed), and consolidated the
  duplicated image proxy into a single `imgp_handler.php` (root) with thin shims
  at `public/imgp.php` + `redirect/imgp.php` (blocked from direct web access via
  `.htaccess`). `composer check` green (cs + stan + 55 tests).
- Migrations and browser runtime were not executed in the build environment.
- 2026-06-25 hotfix session (all baked into this pack): click-log writer
  `redirect/_meetups/clicks.php` rewritten to O(1) splice-append (was O(n^2)/day),
  same on-disk JSON-array format + `id` contract; `imgp_handler.php` hard 5 MB
  byte cap (holds for length-less/chunked responses); `redirect/index.php`
  rate-limiter made atomic under a single `flock` (TOCTOU fix) + filter-mode
  misconfig logging; `redirect/FilterEngine.php` guards `cycleSeconds <= 0`.
  Added regression tests (`tests/FilterEngineTest.php` cycle case,
  `tests/MeetupClickLogTest.php`). `composer check` green (62 tests / 115 assertions).
- 2026-06-25 tooling/type session: Composer scripts switched to portable
  `@php vendor/bin/<tool>` (run on Windows + Linux without per-OS bin proxies);
  resolved all 8 out-of-baseline PHPStan findings via typed `@param`/`@return`
  contracts (no runtime change) in `env-config.php` + `public/api/cf_create_token.php`;
  regenerated `phpstan-baseline.neon` (1049 → 859 locked legacy findings).
  `composer check` green (cs + stan + 62 tests).
- 2026-06-25 security/perf audit session (all baked into this pack): added
  `migrations/004_clickrecord_unique_click_date.php` + `schema.sql` UNIQUE
  `(click_id(64), click_date)` on `clickrecord` (closes the concurrent
  double-count gap the postback/meetup write paths assumed); removed the
  per-request `information_schema` table-existence probe from the click hot path
  (`redirect/_meetups/index.php`) and the shorten path (`public/api/generate_api.php`);
  `public/api/checkdomain.php` Facebook token now POST-only (no token in
  access/Referer logs); fail-open env-tunable per-IP rate limit on
  `redirect/api/shorten.php` (`SRP_SHORTEN_RATE_PER_MIN`, default 300, 0=off);
  removed dead label in `public/api/cf_create_token.php`. `composer cs` now
  `--ignore`s 4 HTML-mixed templates (phpcs ScopeIndent vs php-cs-fixer conflict;
  still covered by `composer fix` + PHPStan). `composer check` green
  (cs + stan + 62 tests / 115 assertions). **Run migration 004 on the host
  before serving traffic** (backup DB first; confirm prefix-collision=0).
- 2026-06-25 feature: added a **"Long URL"** option to the SELECT-SHORTURL
  dropdown (`public/index.php`). When chosen, generation posts `noshorten=1` to
  `public/api/generate_api.php`, which returns the raw long URL
  (`https://<domain>/<base64url-payload>`) and skips the built-in + external
  shorteners. Default and all other shortener options are unchanged.
  `composer check` green.
- 2026-06-25 anti-probe: redirect engine now flags requests that spoof a MOBILE
  UA but carry automation / HTTP-client / link-checker-validator-monitor
  signatures (`srp_is_suspicious_mobile_probe()` in `redirect/functions.php`).
  Such "check redirect / tracker / preview / validator" probes are served OG meta
  tags (HTTP 200) like preview bots — they skip the rate limit, geo block, and
  timed filter and never see the real offer redirect. Real mobile visitors
  (normal browser UA) are unaffected. Wired into `redirect/index.php` via the
  combined `$isPreviewProbe` flag; regression tests added in
  `tests/FunctionsTest.php`. `composer check` green (64 tests / 124 assertions).
- 2026-06-26 redirect-decision hardening (`public/redirect-decision.php`):
  login now uses the shared per-IP file throttle (`login_throttle.php`,
  scope `rd_login`) in addition to the per-session counter, so clearing cookies
  no longer resets the brute-force lockout; `rdHttps()` now honours
  `X-Forwarded-Proto` / `CF-Visitor` so the session cookie `Secure` flag is set
  correctly behind a TLS-terminating edge; live/tracker stats `SUM(leads)` now
  `CAST(... AS UNSIGNED)` (leads is a TEXT column). Removes `is.gd` from the
  generator shortener dropdown (`public/index.php`). `composer check` green.
- **2026-06-29 init + UI consistency** (this pack): project init — regen
  `phpstan-baseline.neon` (866 entries) + fix `support_form_result()` return
  type + PSR-12 spacing fix (`index.php`); all pages across `public/`,
  `statistics/`, `redirect/` now use `html,body{width:100%;height:100dvh;margin:0;}`
  for consistent full-viewport layout (16 files updated); added subtle full-page
  backdrop `body::before` with `assets/login-backdrop.webp` at opacity .08
  across all pages (19 files). `composer check` green
  `composer check` green (cs + stan + 64 tests / 128 assertions).
- **2026-06-29 cf_sync spinner + bg.png** (this pack): `public/api/cf_sync.php`
  UI now shows animated SVG spinner during Cloudflare zone sync (replaces static
  `...` text); badge + sync button both spin via `SVG_SPINNER` + `.btn-spin`
  keyframes; auto-opens nameserver modal on success. Added `assets/bg.png` as
  muted full-viewport background (`body::after` opacity .06 layered with favicon
  logo). `composer check` green (cs + stan + 64 tests / 128 assertions).
- **2026-06-29 gradient overlay + font tokens** (this pack): `body::after` now
  layers `linear-gradient(rgb(255 255 255/66%),rgb(255 255 255/20%))` over
  favicon logo + `bg.png` cover; added `--font-cursive:cursive` and
  `--font-mono:monospace` to all `:root` blocks. `composer check` green
  (cs + stan + 64 tests / 128 assertions).
  image overlay with brand watermark `NGIX•XCTD` at top-left (`body::before`)
  + favicon logo at bottom-right (`body::after`) across all pages (19 files).
  `composer check` green (cs + stan + 64 tests / 128 assertions).
- 2026-06-29 invalid sub_id → 404.php (`public/index.php`): when `sub_id` is absent
  or not found in the DB, the portal now routes to the shared WrongURL page via
  `require __DIR__ . '/404.php'; exit` (HTTP 404). The previous inline "Invalid or
  missing sub_id." HTML block is removed. `404.php` patched to guard `function e()`
  with `function_exists()` (prevents redeclare fatal when included from pages that
  already define `e()`) and to respect a pre-set ≥400 response code instead of
  overriding it with 200. No DB or auth change; `composer check` green.
- 2026-06-29 animated button spinner loader + disable image right-click: all AJAX
  buttons in `public/index.php` dashboard (`#genurl`, `#addondom`, `#cf-detect-account`,
  `#cf-config-save`, `#cf-config-clear`, `#cf-create-token`, `#cf-sync-all`,
  `#confirm_delete_domain`) now show a `@keyframes btn-spin` SVG arc spinner while
  in-flight and restore original HTML on completion via `btnLoad()`/`btnReset()` helpers.
  Login submit buttons in `public/index.php` (tracker portal) and `public/login.php`
  (admin) show the spinner on form submit via a vanilla-JS interceptor. Image
  right-click (`contextmenu`) is blocked on the login pages (dashboard already had it).
  No PHP change; `composer check` green.
- 2026-06-29 add-domain CF loader + inline NS (`public/addondomain/index.php`): when
  admin submits an add-domain form with CF enabled, the form and cPanel NS blockquote
  are hidden and a spinner replaces them ("CF Sync: domain.com…") while the
  `cf_sync.php` zone_add request runs; on success the spinner is replaced inline by
  the Cloudflare nameserver list (per-domain, accumulated for batches); the modal
  stays open so the admin can note the NS before closing. On CF failure the loader
  hides and the modal auto-closes as before. New helpers: `resetAddModal()` restores
  the form on `hidden.bs.modal`. No PHP change; `composer check` green (64 tests / 128 assertions).
- 2026-06-28/29 `404.php` redesign: the WrongURL 404 page was fully rewritten with
  new helper functions (`request_path()`, `is_https_request()`, `safe_host()`,
  `canonical_url()`), dynamic HTTP status (200 at root, 404 elsewhere), Schema.org
  JSON-LD, revised CSS/HTML layout, and brand assets served as `/favicon.ico` +
  `/assets/apple-touch-icon.png` (no `/__brand/` or data URI dependencies). The
  `img-src 'self' data:` and `font-src 'self' data:` CSP directives are retained.
  `composer check` green (64 tests / 128 assertions).
- 2026-06-28 delete-domain UX (`public/addondomain/index.php`): when admin
  confirms domain deletion the confirm dialog closes immediately; the table row
  enters a loader state (spinner + 50% opacity, all row buttons disabled); DB
  delete runs first, then cPanel wildcard + CF zone removal run in parallel
  (best-effort); when all settle the row fade-outs (180 ms) and is removed from
  the DOM, followed by a single "Deleted" toast and a `loadData()` refresh to
  update pagination counts. On DB-delete failure the row is restored and
  `loadData()` is called. No PHP change; `composer check` remains green
  (cs + stan + 64 tests / 128 assertions).
- 2026-06-27 shared 404 error document: the standalone WrongURL landing
  (`404.php`, now emits a real HTTP 404) is wired as `ErrorDocument 404 /404.php`
  across all four scopes — root + `public/`, `redirect/`, `statistics/`. Each
  module gets a thin `404.php` shim (`require __DIR__ . '/../404.php';`, mirrors
  the `imgp_handler.php` shim pattern, no drift); the root `.htaccess` subdomain
  routing remaps `/404.php` into the right module dir. Brand assets load on every
  subdomain via a host-independent `/__brand/*` → root `assets/` passthrough in
  the root `.htaccess` (no per-module asset duplication). Also cleared pre-existing
  gate drift in `index.php` / `redirect/index.php` (whitespace/EOL) and bumped the
  `index.php` `cast.string` baseline count 1→2 for the apex-redirect cast.
  `composer check` green (cs + stan + 64 tests / 128 assertions). The webserver
  `ErrorDocument` + `/__brand/` rewrite are LiteSpeed/Apache-level — verify on the
  host after deploy.
