Skip to content

Root Folder Tidy

Ed Mozley edited this page Jul 21, 2026 · 3 revisions

Root folder tidy

The application root used to hold fifteen loose PHP files β€” login, logout, password pages, OAuth callbacks, background workers, a CSAT survey and a stray test script. In July 2026 they were moved into purposeful folders, leaving the root with just index.php, config.php and db_config.sample.php, and the login page gained a single canonical pretty URL: /login. This page documents every move, the URL contract that keeps old links working, and the rules to follow when adding new root-level pages.

The problem this solved

The same login page was reachable at three URLs, one of them broken:

URL What happened
/login.php worked
/login worked by accident β€” Apache's MultiViews content-negotiation silently mapped it onto login.php
/login/index.php half-broken β€” MultiViews served login.php with /index.php as PATH_INFO, so the browser resolved relative assets against a phantom /login/ folder and the logo 404'd

MultiViews came from the WAMP vhost (Options +MultiViews). The Docker image never had it β€” so /login 404'd in Docker until this change. Explicit rewrites now replace the accident in both environments, and MultiViews is switched off (Options -MultiViews in the root .htaccess).

The URL contract

/login is the one canonical login URL. Everything else funnels into it:

Request Result
/login βœ… 200 β€” the login page (canonical)
/login.php (GET) 301 β†’ /login (query string preserved)
/login.php (POST) served internally β€” a redirect would drop the form body
/login/, /login/index.php, /login/anything 301 β†’ /login
/auth/login.php (the file's real location) 404 β€” direct access is blocked so a third URL can never exist

The same pattern applies to /forgot-password, /reset-password and /csat. The ~100 in-app references to login.php were deliberately not rewritten β€” they keep working through the 301 and can be migrated to /login opportunistically.

What moved where

Key: πŸ” auth page Β· 🌐 external contract (URL must never change) Β· βš™οΈ CLI worker Β· πŸ§ͺ test Β· πŸ“Š module page

🎨 Old (root) New location URL behaviour
πŸ” login.php auth/login.php canonical /login; .php GET 301s, POST internally rewritten
πŸ” forgot-password.php auth/forgot-password.php canonical /forgot-password; .php 301s
πŸ” reset-password.php auth/reset-password.php canonical /reset-password; .php 301s β€” emailed reset links keep working (1-hour tokens, query preserved)
πŸ” force_password_change.php auth/force_password_change.php internal rewrite only, URL unchanged (POST-heavy interstitial; no pretty URL needed)
πŸ” analyst_logout.php auth/analyst_logout.php internal rewrite, URL unchanged; also aliased as /logout
🌐 oauth_callback.php auth/oauth_callback.php internal rewrite only β€” never redirected. This exact URL is registered in Azure app registrations and stored (encrypted) per mailbox
🌐 google_oauth_callback.php auth/google_oauth_callback.php same β€” registered in Google Cloud Console
πŸ“Š csat.php tickets/csat/survey.php canonical /csat; csat.php?token=… 301s with the token intact β€” survey links in already-sent emails keep working forever
βš™οΈ intune_worker.php scripts/intune_worker.php CLI only, spawned by intuneSpawnGenericWorker() in includes/intune.php (path updated) β€” never a URL
βš™οΈ intune_app_worker.php scripts/intune_app_worker.php same
πŸ§ͺ test_email_thread.php tests/test_email_thread.php dev script, zero references β€” never a URL
πŸ” logout.php auth/logout.php ⚠️ legacy corpse: it references a TOKEN_STORAGE_FILE constant that no longer exists, so it has fatalled on every call for a long time, and nothing references it (the real logout is analyst_logout.php). Moved rather than deleted only pending a deliberate decision β€” candidate for deletion

What deliberately stayed in root: index.php (the landing page β€” the web server's default document), config.php (its path is hardcoded in every require across the app) and db_config.sample.php (documented in the install guide as a root file to copy). Plus the non-PHP infrastructure: .htaccess, web.config, Dockerfile, docker-compose.yml, LICENSE, README.md.

How the compatibility layer works

Everything is driven by the root .htaccess (heavily commented in place):

  • Options -MultiViews -Indexes β€” kills the accidental URL mapping and directory listings.
  • Pretty URLs are internal rewrites (the address bar keeps the pretty form).
  • Legacy .php URLs: GET β†’ 301 to the pretty URL; POST β†’ internal rewrite (a 301 would discard the body). Redirect targets are built from a REQUEST_URI capture (%1), never a bare relative substitution β€” a relative substitution in an R= rule leaks the filesystem path into the Location header. This also makes the rules deployment-agnostic: they work at /freeitsm-app/ (WAMP) and / (Docker) unchanged.
  • Guard files in auth/ and tickets/csat/ return 404 on direct access: internal rewrites arrive with REDIRECT_STATUS set, direct requests don't. One page, one URL.
  • All rewrite rules sit inside <IfModule mod_rewrite.c> (house style, matching api/v1/.htaccess) so a server without mod_rewrite serves the rest of the app instead of 500ing β€” but note that on such a server the auth pages are unreachable: mod_rewrite is effectively required since this change. The Docker image enables it (a2enmod rewrite); WAMP's vhost has AllowOverride All.

Things that did not change

  • SSO/OIDC β€” its callback was already api/auth/oidc_callback.php; untouched.
  • The self-service portal β€” has its own self-service/login.php; untouched.
  • Mailbox OAuth registrations β€” nothing to re-register in Azure or Google; the callback URLs are byte-identical externally.
  • The mailbox save validation (api/tickets/save_mailbox.php) matches the callback filename with an any-prefix pattern ((^|/)google_oauth_callback\.php$), so it was already tolerant of the file's location.

Rules for future root-level pages

  1. Don't put new PHP pages in the root. Auth-flow pages go in auth/; module pages go in their module.
  2. Give a public page a pretty URL via a rewrite pair in the root .htaccess (internal rewrite for the canonical URL + guarded 301 for any legacy name), copying the existing REQUEST_URI %1 pattern for redirects.
  3. Never rely on MultiViews β€” it is off, deliberately.
  4. Never move or redirect an OAuth callback URL β€” it is an external contract with the IdP; if a callback file must move, add an internal rewrite so the URL survives.
  5. URLs embedded in sent emails (CSAT, password reset, verification links) must keep working indefinitely β€” 301 with the query string preserved is the minimum.

Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally