-
Notifications
You must be signed in to change notification settings - Fork 15
Asset Handover Developer Guide
Why the designer is blocks and not a canvas, why escaping order is the security of this feature, and why one renderer serves three destinations. The plain-language version is Who holds what, and handover documents.
Asked for in discussion #56. Overview and document in 605811aa, designer in 2aed483e.
| File | Role |
|---|---|
includes/services/assets.php |
usersHoldingAssets(), assetsForUser() β both tenancy-scoped |
api/assets/get_users_with_assets.php |
The people list, with search |
api/assets/get_user_assets.php |
One person and their equipment |
| File | Role |
|---|---|
includes/services/handover_templates.php |
Block catalogue, merge codes, sanitiser, the renderer |
includes/handover_styles.php |
The stylesheet, as a PHP function β see Β§4 |
asset-management/handover.php |
The printable page |
api/assets/email_handover.php |
The emailed copy |
| File | Role |
|---|---|
asset-management/settings/index.php |
The Handover tab and its JavaScript |
asset-management/settings/manifest.php |
Tab declaration, gated on Cap::ASSETS_HANDOVER
|
api/assets/handover_templates.php |
CRUD + preview |
includes/capabilities.php |
ASSETS_HANDOVER |
| Table | Role |
|---|---|
asset_handover_templates |
blocks is JSON; see Β§3 for why that is safe |
users_assets |
Pre-existing. Who holds what, with assignment date, notes, expected return |
The obvious "designer" is a rich-text canvas. It is the wrong tool here for one structural reason:
The middle of this document is a repeating region. One row per asset, count unknown until render time. Every WYSIWYG editor either cannot express that, or expresses it as a loop the administrator has to author β at which point it is not a designer, it is a templating language with a nicer font.
So a document is an ordered list of blocks drawn from a fixed catalogue. The administrator reorders them, disables them, edits the words in the ones that have words, and picks columns on the one that is a table.
What that buys:
- the stored template is always renderable β nothing arbitrary is ever kept
- the equipment table stays a real
<table>rather than hand-written markup - adding a block type to
catalogue()makes it appear in the designer and the renderer, with no UI change
The designer JavaScript fetches ?action=meta and builds itself from the catalogue, so it never holds its own opinion about what a document may contain.
This is the security of the whole feature, and it is the sort of thing that is easy to get backwards.
private static function mergeText(string $text, array $values): string
{
$escaped = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
$from = array_keys($values);
$to = array_map(fn($v) => htmlspecialchars($v, ENT_QUOTES, 'UTF-8'), array_values($values));
return str_replace($from, $to, $escaped);
}- Escaping the finished string would escape our own markup along with it.
- Substituting into unescaped text would let a value containing markup through.
And the values are not ours: {{employee.name}} is a users.display_name, which arrives from the portal, from inbound mail, from an import.
Verified rather than assumed β a user named Alice <script>alert(1)</script> Johnson:
| Check | Result |
|---|---|
Live <script> tags from the name |
0 |
| Correctly escaped occurrences | 2 |
| Positive control (real block still renders) | yes |
sanitiseBlocks() forces anything arriving from the browser into the catalogue's shape:
- an unknown block type is dropped
- an unknown text field on a known block is dropped
- an unknown column is dropped
- a missing field falls back to its default
- a duplicate block type is dropped
- text is capped at 2000 characters and stored as plain text, never markup
That is why renderBlocks() performs no validation of its own: by the time anything is stored, it is already known-good.
Verified with a hostile payload β unknown block absent from the output, a raw <b> in a text field escaped rather than rendered, an invented column not emitted as a header, and the positive control still rendering.
HandoverTemplates::renderBlocks() produces the inside of the document. Three callers wrap it:
| Caller | Wrapper |
|---|---|
handover.php |
Page chrome, toolbar, print CSS |
api/assets/handover_templates.php?action=preview |
The designer's preview pane |
api/assets/email_handover.php |
An HTML email with the CSS inlined |
The preview therefore cannot lie. A designer whose preview is a separate approximation of the output is a designer nobody trusts, and the divergence is always discovered on a document somebody has already signed.
The stylesheet lives in includes/handover_styles.php as a PHP function returning a string, not a .css file, for one reason: the emailed copy has to carry its styles inside the message. A mail client will not fetch an external sheet. Same text, two delivery mechanisms.
email_handover.php passes logo_path => null. The page uses a relative path for the logo, which resolves against the page's URL and would be a dead image in a mail client. Serving it absolutely would mean an authenticated URL a mail client cannot reach either β worth solving properly if anybody wants a logo in the emailed copy.
usersHoldingAssets() INNER JOINs users, on purpose.
users_assets has no foreign key, and older installs carry rows pointing at requesters who no longer exist β the development database used to build this has nine. A LEFT JOIN would list them as nameless people holding real equipment, which reads as data loss rather than as stale rows.
The cost is honest and worth stating: an asset assigned to a deleted requester is invisible on this screen. It is still on the asset record, which is where it can actually be fixed.
handover.php is rendered server-side and printed by the browser. Both are deliberate.
Server-side, because this is the one page in the module somebody signs and files. It must not depend on JavaScript having run, a fetch having succeeded, or a library being present. What the printer puts on paper is what the server sent.
The browser's print dialogue, because it produces a true PDF via Save as PDF on every platform, keeps the text selectable and searchable, and inherits the fonts and logo already on the page. A jsPDF re-implementation would be a second layout to keep in step with this one β and the moment they drift, the PDF and the printout disagree.
Print CSS keeps signature blocks, the declaration and table rows from splitting across pages.
Cap::ASSETS_HANDOVER gates the settings tab and every write action on handover_templates.php. Reads are plain module access.
The split is intentional: designing the document is administration, producing one is daily work. Withholding the capability must not stop a service desk analyst printing a handover for a new starter.
-
A new block: add it to
catalogue()with its text fields, add acaseinrenderBlocks(), add the lang keys. The designer picks it up with no change. -
A new merge code: add it to
mergeCodes()and tomergeValues(). Both the palette and substitution follow. -
A new table column: add it to
assetColumns()and to the$defsmap inrenderAssetTable(), and make sureassetsForUser()selects it.
- No logo in the emailed copy β see Β§4.
- No per-template default per situation. One default for the whole install; an onboarding pack and a leaver's checklist both exist but the button always uses the default. A template picker on the handover screen is the obvious next step.
- Nothing records that a handover happened. The document is produced on demand and not stored, so there is no history of "Alice signed for this on the 3rd". A signed-copy upload against the person would close that loop.
- 23 locales. All new strings are English-only and fall back silently.
- Who holds what, and handover documents β the plain-language version
- Assets Β· Asset QR Labels β Developer Guide
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
- β³ π’ Ticket numbering
- β³ π 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)