Skip to content

Self Service Portal Actions Developer Guide

Ed Mozley edited this page Sep 24, 2026 · 1 revision

What people can do in the portal β€” developer guide

User-facing page: What people can do in the self-service portal.


πŸ”΄ A requester is not an analyst, and the audit trail has to say so

ActorContext.actorId names an analyst. A requester closing their own ticket is not one, so there is no honest value to put there.

Every tempting answer is wrong: passing 1 blames whoever that is; passing the assigned analyst credits somebody who never touched it; passing the requester's id writes a requester id into an analyst column, where it later reads back as whichever analyst holds that number.

ActorContext::fromPortalUser($name) carries actorId: 0 and source: 'portal', and anything writing an actor copes with 0 by storing NULL. ticket_audit.analyst_id is nullable for exactly this reason.

The closure reason goes to ticket_audit, not ticket_notes β€” ticket_notes.analyst_id is NOT NULL, and the column is note_text rather than note. Writing there would have thrown inside a logging-only catch and vanished.

The close itself routes through TicketsService::updateTicket(), the single choke point for ticket writes, so it inherits every rule an analyst close obeys.

πŸ”΄ A guard after the header include does nothing

The My equipment page checked the administrator's switch after including the portal header β€” which reads as the natural place, because that is where every other page does its work.

But the header has already sent output, so header('Location: …') does nothing and the page returns HTTP 200 with the content rendered. The guard must run before any output.

A 200 means a page was reached, never that it was allowed. Test a guard by asking for the URL you should not have.

Per-person settings for people who are not analysts

user_preferences is keyed by analyst_id, so it can hold nothing for a portal user. That gap was first patched by giving the portal's palette its own column on users β€” fine once, wrong as a pattern, since the next preference becomes a third column.

portal_user_preferences is the generic twin. Two things about it:

  • portalPrefSet() does SELECT-then-UPDATE, not ON DUPLICATE KEY. Database Verification creates columns but not indexes, so on an install that gained the table by upgrade there may be no unique key for ON DUPLICATE KEY to fire on β€” and it would silently insert a second row instead of updating.
  • It carries no tenant_id, deliberately. A row is keyed to a person, and users.tenant_id already records which company that person is in; a second copy here could only disagree with the first. How somebody likes their knowledge base laid out is a fact about them, not about the company whose tickets they are reading. (See the multi-tenancy developer guide Β§1 on asking this question before adding a table.)

πŸ”΄ The portal theme and the analyst theme are one session

The portal and the app are one host and one PHPSESSID. Theme::active() used to require analyst_id to be empty before honouring a portal user's choice β€” so an analyst who also has a portal account (every developer, and most admins testing their own portal) carried both ids at once, the condition was false, and the portal rendered the analyst's palette.

The choice saved correctly and was ignored on every render, which from outside is indistinguishable from "it did not save".

What decides it is which product the page belongs to, not who happens to be signed in elsewhere:

$onPortal = defined('FREEITSM_SELF_SERVICE') && FREEITSM_SELF_SERVICE;
if (!empty($_SESSION['ss_user_id']) && ($onPortal || empty($_SESSION['analyst_id']))) { … }

The broken case is the one every developer is permanently in. Reproducing it needs both sessions active β€” a portal-only session renders correctly and proves nothing.

Knowledge layouts

PORTAL_KB_LAYOUTS is the allow-list; api/self-service/preference.php allow-lists both the key and the value. The tree needs an article's folder, which the portal payload did not carry β€” adding it exposes nothing new, because portalKnowledgeScope() has already decided which articles this requester may read, and naming the folder of an article they can already open tells them nothing they could not read off the article itself.


See also

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally