Portal Privacy - edmozley/freeitsm GitHub Wiki
Portal privacy: what requesters see of their own ticket
Where: Tickets β Settings β Privacy Default: Hide them from the requester Affects: the self-service portal only. Nothing changes for analysts.
The problem in one sentence
A ticket is not only your conversation with the person who raised it β and until this setting existed, the portal showed them all of it.
When an analyst forwards a ticket to a supplier, or copies in a colleague, that message is filed on the same ticket. So is the supplier's reply. That's exactly what you want internally: one ticket, one complete history, nothing scattered across mailboxes.
But the self-service portal was showing the requester that entire history. So correspondence about them was visible to them:
"Hi Dave β sending this one over, the customer's being difficult about the renewal. Pricing sheet attached."
β¦appearing in the customer's own portal, with the pricing sheet downloadable.
The three options
| Option | The message | Its attachments |
|---|---|---|
| Hide them from the requester (default) | Not shown at all | Not downloadable |
| Show the message, but not its attachments | Shown | Not downloadable |
| Show everything | Shown | Downloadable |
Show everything is exactly how FreeITSM behaved before this setting existed. If your team relies on the requester seeing forwards β some desks deliberately work "in the open" with their customers β choose it after updating.
What the setting does not touch
Three things are decided elsewhere and are unaffected by whichever option you pick:
- Internal notes are never shown in the portal. They never were. That's what the Internal flag on a note is for, and it remains the right place for anything you don't want the customer reading.
- The requester's own messages are always shown. Anything they sent you, and any reply addressed to them.
- WhatsApp and web chat conversations are always shown in full. Those exchanges are between the requester and your desk by definition β there is no "forward" on those channels, so there is nothing to hide. (See How the rule works for why this needs stating explicitly.)
- A portal manager sees exactly what the requester would. This setting applies to them too: privacy is measured against the ticket's requester, not whoever is reading.
How the rule works
Here is the part worth understanding, because it explains the edge cases.
FreeITSM does not record that a message was a forward. A forward and an ordinary reply are stored identically β same table, same columns, same everything. Adding a "this was a forward" marker would only work for mail sent after the update, leaving every forward already in your system visible forever.
So instead of asking "was this a forward?", FreeITSM asks the question that actually matters:
Was this message to, or from, the person who raised the ticket?
If yes, it's their correspondence and they see it. If no, it's third-party and the setting decides.
That single question is better than a forward flag in three ways: it catches CCs to colleagues as well as forwards, it catches the supplier's reply coming back in, and it works on every ticket you already have with no migration.
When FreeITSM can't tell
Some messages can't be judged by that question, and every one of them is shown:
| Situation | Why it's shown |
|---|---|
| WhatsApp / web chat | Addressed by phone number or visitor id β the requester's email matches nothing, so an address check would hide their whole conversation from their own portal |
| A message with no recipient recorded | Nothing to compare against |
| A requester whose email address is unknown (recorded as blank) | Nothing to compare against β but this is not the same as a requester who genuinely has no mailbox; see below |
This is deliberate, and it's the one place the design leans away from privacy on purpose:
Hiding an analyst's genuine reply is worse than showing a forward. A customer who can't see the answer concludes nobody helped them, opens a second ticket, and phones you. Ambiguity therefore resolves to visible, and the strict direction is something an admin chooses per option β never something FreeITSM guesses at.
One case that is deliberately not ambiguous
A requester who genuinely has no mailbox β a directory account that signs in to the portal by username (see Staff with no email address) β is a different thing from one whose address is merely unknown (the "no email address on file" row above). No email can ever have been addressed to them, so every email on their ticket is third-party by definition, and the setting applies in full. Treating their blank address as "can't judge β show it" would have shown those requesters every forward and every copied-in reply whatever option was chosen β aimed at exactly the people least able to notice, since they cannot be emailed. portalEmailInvolvesRequester() (in includes/portal_visibility.php) therefore distinguishes has no address (NULL) from address unknown (''), and its callers preserve that difference rather than casting it away.
Worked examples
Assume Sarah Chen ([email protected]) raised ticket ABC-123-45678 about a broken laptop, and your service desk mailbox is [email protected].
Example 1 β an ordinary reply
An analyst replies to Sarah.
| From | [email protected] |
| To | [email protected] |
Sarah is a recipient β her correspondence β always visible, under all three options. Her attachments download normally.
Example 2 β forwarding to a supplier
The laptop is under warranty, so an analyst forwards the ticket to the vendor, attaching the diagnostic report.
| From | [email protected] |
| To | [email protected] |
Sarah is neither sender nor recipient β third-party.
- Hide β Sarah sees nothing. Her thread jumps from the analyst's last reply to whatever comes next.
- Show without attachments β Sarah sees "we've raised this with the vendor" and the message body, but cannot download the diagnostic report.
- Show everything β Sarah sees the message and can download the report.
Example 3 β the vendor replies
The vendor answers, and their reply lands on the same ticket.
| From | [email protected] |
| To | [email protected] |
Still neither to nor from Sarah β third-party, and handled exactly like Example 2. This is the case a "was it a forward?" rule would have missed entirely β the reply isn't a forward, it's an ordinary inbound email.
Example 4 β a colleague copied in
An analyst replies to Sarah and CCs a network engineer.
| From | [email protected] |
| To | [email protected] |
| Cc | [email protected] |
Sarah is a recipient β visible. Being copied to someone else doesn't make it private from her β she's still being written to. The setting is about correspondence that isn't hers, not about who else is on it.
Example 5 β Sarah messages you on WhatsApp instead
Sarah's ticket came in over WhatsApp, addressed by her phone number.
Always visible in full. Her email address doesn't appear anywhere on those messages, so a strict address check would have hidden her own conversation from her. Chat channels are exempt for exactly this reason.
Example 6 β Sarah replies in the portal
Always visible. She wrote it.
An important detail about attachments
The setting is enforced twice β once when building the conversation, and again when a file is actually requested.
That matters more than it sounds. Attachments are fetched by a numeric id in the URL, and those ids are sequential and easy to guess:
/api/self-service/get_attachment.php?id=39
Simply leaving the link out of the page would be decoration, not protection β anyone could type the URL. So the download endpoint applies the same rule independently and returns a plain 404 when it doesn't pass.
Two related guarantees, both verified:
- You can only ever reach attachments on your own tickets. The check walks attachment β message β ticket and requires you to be the person who raised it. A wrong id and someone else's id both return the same bare 404, so the portal never even confirms that another customer's file exists.
- The portal is never told who else was on an email. The sender and recipient fields are used to make the decision server-side and then removed from the response.
Choosing an option
| If your desk⦠| Choose |
|---|---|
| Treats forwarding to suppliers as internal business (most desks) | Hide them |
| Wants customers to see that work is happening, without exposing what was sent | Show the message, not attachments |
| Works transparently with customers and relies on them seeing the full trail | Show everything |
If you're unsure, keep the default. It matches what most people assume is happening, and it's the only option where forwarding a ticket can't surprise you.
Under the hood
Colour key: βοΈ shared logic Β· π API Β· π₯οΈ UI Β· π permissions Β· ποΈ storage
| π¨ | File | Role |
|---|---|---|
| βοΈ | includes/portal_visibility.php |
The rule, the policy reader, and the three constants. One place, so the listing and the download can't disagree |
| π | api/self-service/get_ticket_detail.php |
Filters the conversation; strips the routing fields from the response |
| π | api/self-service/get_attachment.php |
Enforces the same rule on download, independently |
| π₯οΈ | tickets/settings/index.php |
The Privacy tab UI |
| π | tickets/settings/manifest.php |
Declares the tab, its capability and its setting key |
| π | includes/capabilities.php |
Cap::TICKETS_PRIVACY |
| π | lang/en/tickets.php + lang/pt-BR/tickets.php |
The tab's strings |
| ποΈ | system_settings.portal_third_party_visibility |
The stored choice β absent means hide |
Stored as the portal_third_party_visibility system setting (hide / no_attachments / show). Absent means hide, so a fresh or upgrading install is protected before anyone finds the screen. No database change is needed for this feature.
See also
- Self-Service Portal β the portal itself
- Tickets β forwarding, replies and internal notes
- Admin Access Control β the capability that controls who can change this