Repository navigation
Portal Was Down For Everyone Signed In
Reported by email Β· Fixed in #1441
The self-service portal accepted your password, and then gave you a page with nothing on it. Not an error, not a broken layout β the browser tab had the right title, the styling loaded, and the page was empty below it.
Every page behind the sign-in did the same thing: the dashboard, your tickets, the service catalogue, raising a ticket, the help centre and help. Six of them.
The only way to see what had happened was to view the page source, where the page stopped mid-attribute:
<div class="portal-header">
<div class="portal-brand">
<img src="<br />
<b>Fatal error</b>: Uncaught Error: Call to undefined function brandingLogoUrl()
in .../self-service/includes/header.php:100
The sign-in page itself was fine. So was registration. That is the part worth holding on to β from outside, the portal looked completely healthy.
One missing line.
Update #1421 taught the portal to display a logo an administrator had configured, rather than the bundled one. The portal's shared page header stopped hardcoding the file and asked for it instead:
<img src="<?php echo htmlspecialchars(brandingLogoUrl()); ?>" alt="">brandingLogoUrl() lives in includes/branding.php. The header never loaded that file. PHP looks a function name up when it reaches the call, not when it parses the file, so the page rendered perfectly until the exact byte where the function was needed β which is why you get a page with a <head>, a <body>, and then nothing.
The fix is the line that was missing:
require_once __DIR__ . '/../../includes/theme.php';
require_once __DIR__ . '/../../includes/branding.php'; // β this
require_once __DIR__ . '/../../includes/timezone.php';self-service/login.php and self-service/register.php show the logo too, and both of them load the file themselves. They were changed in the same commit, correctly.
So the portal's entire public face β the only part you can reach without a password β went on working exactly as before. The failure began at the first page after sign-in and stopped there, in the one part of the portal that nobody can check without an account.
It is also the reason a reviewer would not spot it by reading the directory. Two of the three files that call the function sit right there in self-service/, both with the require at the top. The third is the one page you cannot see from the outside.
Nine hardcoded paths became nine calls to one helper. Here is what that commit did inside the portal:
self-service/includes/header.php | 2 +-
self-service/login.php | 3 ++-
self-service/register.php | 3 ++-
Two files gained a line. One only swapped a line.
That is not a coincidence, it is the mechanism. In self-service/login.php and self-service/register.php the logo is set up near the top of the file, in among the requires β so replacing it put the author's eye on the require block, and the require went in. In self-service/includes/header.php the logo is a tag buried a hundred lines down in the markup, nowhere near the top of the file, and nothing about editing that line prompts you to look at what the file loads.
A find-and-replace across nine call sites asks "did every hardcoded path get replaced?". The question that needed asking was "can every file that now calls this reach it?" β and those are not the same audit.
Four separate safety nets did not apply, and each one is worth knowing about on its own.
| Check | Why it missed this |
|---|---|
php -l |
An undefined function is a runtime error. The broken file lints clean β verified against the exact broken revision. |
| The HTTP status code | A PHP fatal is served as HTTP 200. Anything checking only the status reports a dead page as a pass. |
| Loading the portal to look at it | The pages you can load without an account were the pages that worked. |
| The test suite | There is no test that signs into the portal and loads a page. There is no test that loads any page and fails on a fatal. |
The first two are general. A page can be completely dead and still answer 200 with a syntactically perfect file β so if you are checking pages by script, read the response body and treat Fatal error or Uncaught as a failure, whatever the status line says.
The convention on these write-ups is to sweep for other instances of the same fault before writing anything, and this time the sweep is the reassuring half rather than the alarming one. Every call site in the product was checked:
| File | Loads includes/branding.php? |
|---|---|
auth/login.php |
β |
index.php |
β |
forms/edit/index.php |
β |
forms/fill.php |
β |
forms/settings/index.php |
β |
morning-checks/index.php |
β |
system/branding/index.php |
β |
api/system/save_branding.php |
β |
self-service/login.php |
β |
self-service/register.php |
β |
self-service/includes/header.php |
β β this bug |
Ten of eleven were already right. (includes/services/network_mapper.php has a brandingSlot() of its own, which is an unrelated class method that happens to share the prefix β worth naming so the next person grepping for branding does not chase it.)
π read Β· π₯οΈ UI
| π¨ | File | What changed |
|---|---|---|
| π₯οΈ | self-service/includes/header.php |
one require_once, with a comment saying why it belongs here rather than in the six pages |
A forged portal session against a real installation, then all six signed-in pages fetched and their bodies read:
index ok 88,717 bytes
tickets ok 106,808 bytes
help ok 84,937 bytes
help-centre ok 77,522 bytes
catalogue ok 85,556 bytes
new-ticket ok 113,753 bytes
No Fatal error and no Uncaught in any of them.
And a positive control, which is the part that actually proves something. "No fatal error" would also be true if the require had pointed at an empty file, or if the function had failed and been swallowed. So the check was that the header renders the configured logo, not the bundled fallback:
<img src="/freeitsm-app/system/uploads/branding/d972b01e3866a83dfed62f3f3d244b65.png" alt="">That is an uploaded file, read out of system_settings β so the file really loaded, the function really ran, and it really reached the database. Asserting the absence of an error proves much less than it appears to.
-
If your portal users can sign in and then see a blank page, this is it, and you are on update #1421 or #1422. Take #1441, or add the single
require_onceabove toself-service/includes/header.phpyourself. - Nothing was lost or corrupted. The pages could not be drawn; no data was touched, and no ticket raised through the portal before the upgrade went anywhere it should not have.
- The analyst side was never affected, nor was portal sign-in, nor registration.
-
There is still no smoke test. Nothing in the suite loads every page in the product and fails on a fatal, and nothing at all exercises the portal behind its sign-in. A test that forges a session, fetches each page and greps the body for
Fatal errorwould have caught this in seconds, and would catch the whole family β any shared include that calls a function it does not load. It is the obvious next thing to build and it has not been built. - The general fault is not designed out. PHP resolving function names at call time is exactly what makes a helper like this pleasant to use everywhere, and also what lets a caller forget to load it with no warning until somebody visits the page. There is no static check in the project that would object.
- Login Screen Designer β the feature this arrived with
- Self-Service Portal Β· Self-Service Developer Guide
- The portal dashboard showed the wrong time β the other one found in the portal's shared chrome
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
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
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- 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: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- 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
- β³ π’ One board across every company
- β³ 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)
- What this is
-
π Bugs resolved
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96