-
Notifications
You must be signed in to change notification settings - Fork 15
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.phpanddb_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 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).
/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) |
β 200 β the same page, at the path the application itself uses |
The same pattern applies to /forgot-password, /reset-password and /csat.
β οΈ Corrected in August 2026 β issue #68This page previously said two things that turned out to be a bug, and they are struck out above and below. Both are recorded here rather than quietly edited away, because the reasoning was sound and the conclusion was still wrong.
1.
It does return 200 now. Blocking the real path meant the application could not name its own login page β every internal redirect had to go through a URL that only existed if Apache read a rewrite rule./auth/login.phpreturns 404 so a third URL can never exist.2.
The ~100 in-app references toThey have all been rewritten, tologin.phpwere deliberately not rewritten; they keep working through the 301.auth/login.php. "Keeps working through the 301" was only true on Apache withAllowOverride All. On nginx, which never reads.htaccess, a fresh install redirected every logged-out visitor to a 404 β the app was unusable, and the reporter of #68 quite reasonably concluded a file was missing from the repository.The rule now: application code names real file paths. A redirect target must resolve with no rewriting of any kind. Pretty URLs are presentation, layered on top for humans, and nothing internal may depend on them.
The concern that motivated the old rule β relative assets resolving against
/auth/and breaking β was real, and is now fixed at source: the pages inauth/useBASE_URL-absolute paths, so they render identically at either URL.
β οΈ That fix was itself incomplete for about ten minutes, and the way it failed is the most useful thing on this page. The asset paths were made absolute; the redirect targets were not.header('Location: index.php')had always been correct while the page was only served at/login, and resolved to/auth/index.phpβ a 404 β the moment the page became reachable at its real path as well. Signing in succeeded and then landed on nothing.The rule to take away: if a page is reachable at more than one URL depth, every path it emits must be absolute β assets, links, PHP redirects and JavaScript redirects alike. A relative path silently encodes how deep the current URL is, and that assumption holds only until somebody adds a second route to the page.
Written up in full, with the idiom to use and the test that now guards it, in Running on nginx β The trap this created.
See Running on nginx for the config nginx needs, including the nine
.htaccessfiles that are security controls rather than conveniences.
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 |
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.
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
.phpURLs: GET β 301 to the pretty URL; POST β internal rewrite (a 301 would discard the body). Redirect targets are built from aREQUEST_URIcapture (%1), never a bare relative substitution β a relative substitution in anR=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 file in
tickets/csat/returns 404 on direct access: internal rewrites arrive withREDIRECT_STATUSset, direct requests don't.The same guard was inβ removed in #68, because it blocked the path the application's own redirects now use. See the correction notice above.auth/ - All rewrite rules sit inside
<IfModule mod_rewrite.c>(house style, matchingapi/v1/.htaccess) so a server without mod_rewrite serves the rest of the app instead of 500ing. -
mod_rewrite is effectively required since this change.No longer true, and that was the bug. Every internal redirect now names a real file path, so the application works with no rewrite module, no.htaccess, and on a web server that has never heard of either. Rewrites are what make the URLs pretty, not what makes the app function. See Running on nginx.
-
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.
-
Don't put new PHP pages in the root. Auth-flow pages go in
auth/; module pages go in their module. -
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 existingREQUEST_URI%1pattern for redirects. - Never rely on MultiViews β it is off, deliberately.
- 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.
- URLs embedded in sent emails (CSAT, password reset, verification links) must keep working indefinitely β 301 with the query string preserved is the minimum.
- Architecture β the directory layout (updated for these moves)
- Installation β prerequisites, including mod_rewrite
- Security β the login flows themselves
-
Single Sign-On / Mailbox Authentication β the OAuth flows whose callbacks live in
auth/
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)