Repository navigation
Module Availability Developer Guide
How switching a whole module off for the installation works (#217). Users: Switching modules off. Design and next steps: Module availability β design.
The list of switched-off modules lives in one setting. analystCanAccessModule() says "no" for a switched-off module, and almost everything β every module page, ~700 endpoint gates, the launchers, most cross-module buttons β already asks that function, so they follow for free. The rest of this page covers the places that don't ask it: the REST API, workflows, cron jobs, the self-service portal, device agents and public links, plus a handful of screens that used raw grants. Each of those has its own small check, and a test fails if a new REST resource or workflow trigger is added without a module.
system_settings.modules_disabled β a JSON list of the module keys that are off. Everything else is on.
// includes/module_availability.php
const MODULES_ALWAYS_ON = ['tickets', 'system'];
function disabledModules(PDO $conn): array // cached per request; [] if unreadable
function moduleIsEnabled(PDO $conn, string $key): bool
function moduleDisabledMessage(string $key): string // "Tasks is switched off for this installation."
function requireModuleOnJson(PDO $conn, string $key): void // 403 + module_disabled, for non-analyst endpoints
function portalModuleOn(string $key): bool // no connection in hand
function cronModuleSwitchedOff(PDO $conn, string $key, string $job, ?string $trigger): boolThree decisions, all in the file's header:
- Only the off list is stored. An upgrade, Database Verification or a fresh install means "everything on" with nothing seeded β and a module added in a later release is on everywhere without anybody adding it to a row.
-
It fails open. An unreadable setting means everything on. This is a tidiness switch, not a security control: access grants and
is_adminare the security, and they still apply. - Tickets and System are never off, even if the stored list names them.
There is no schema change: system_settings already exists, so database/freeitsm.sql and DB Verify are untouched.
Saved by api/system/save_module_availability.php (admins, CSRF, refuses Tickets/System with 422). Shown in System β Modules β Availability (system/modules/index.php), which also tags switched-off rows in the access table; api/system/get_effective_module_access.php gives Switched off for the whole installation as the reason.
function analystCanAccessModule(PDO $conn, int $analystId, string $moduleKey): bool {
if ($analystId <= 0) return false;
if ($moduleKey === 'system') return analystIsAdmin($conn, $analystId);
// A module switched off for the install is off for everyone, administrators
// and all-modules analysts included (GH #217). ...
if (!moduleIsEnabled($conn, $moduleKey)) return false;That one line covers requireModuleAccess() (pages redirect to ?denied=), requireModuleAccessJson(), requireModuleWriteJson(), analystModuleLevel(), the MCP tools, record previews, the Recent trail, report packs, and every page flag built from it (TICKETS_SHOW_DOMAINS, TICKETS_SHOW_PROJECTS, the new TICKETS_MODULE_ON, and so on).
The JSON gates say switched off, not "no access" or "read-only", and add module_disabled: '<key>' to the body.
It deliberately does not subtract switched-off modules: System β Modules shows grants, and an all-modules analyst is still "all modules" there. Anything that decides what an analyst may open from that list goes through:
modulesWithoutSwitchedOff(PDO $conn, ?array $allowed): ?array // null only when nothing is offanalystAllowedModulesNow() (so analystVisibleModules(), the waffle, home page, sidebar and βK) uses it, as do the four callers that used raw grants: includes/search/search.php, includes/case_report.php, includes/services/analysts.php (the effective modules) and tasks/index.php (now analystCanAccessModule()).
"Any of these modules" outlived a switched-off module: Knowledge's articles answered anyone with Tickets after Knowledge was off. When the endpoint's data belongs to one module, pass it:
requireAnyModuleAccessJson(['knowledge', 'tickets', 'lms'], null, 'knowledge');Set on Knowledge's article/tag/AI-chat endpoints, Calendar's events and categories, Tasks' list and contractors, and the assetβCI link. A shared lookup β cost centres, analysts, departments, settings β has no owner and passes none.
| Area | Where | What it does |
|---|---|---|
| REST API v1 |
api/v1/lib/modules.php, called from api/v1/index.php after apiRequirePermission()
|
API_V1_RESOURCE_MODULES maps every permission resource to its module(s) β all must be on. reference lookups map by path (API_V1_REFERENCE_PATH_MODULES). Two routes add a second module by path (/projects/{id}/tasks, /problems/{id}/changes). Answers 403 module_disabled. |
| Workflow triggers |
WorkflowEngine::TRIGGER_MODULES (by the event's first segment), checked in dispatch() and run()
|
Nothing fires while Workflows, or the event's module, is off. run() covers Test fire and the REST fire: an aborted run with the reason. The check sits after notifications and search indexing, which are not workflows. |
| Workflow actions |
WorkflowEngine::ACTION_MODULES, checked in executeActionList()
|
create_task, create_project and attach_checklist are skipped (status skipped, reason in the result), and the run continues β like a confidential ticket's email. Covers stored workflows and forms' own action lists. |
| Workflow editors |
WorkflowEngine::withoutSwitchedOff() in workflow/editor.php and forms/edit/index.php
|
Only switched-on triggers and actions are offered, plus whatever the workflow or form already uses, so it still opens and saves. availableTriggers() / availableActions() stay complete on purpose β api/workflow/save.php validates against them. |
| Cron | cronModuleSwitchedOff() |
lms_reminders (lms), domains, task_recurrence (tasks), webhook_deliveries (workflow), proxmox_sync / vcloud_sync (assets). Recorded as a JobRun skip. workflow_scheduled gates per part instead: contract expiries (contracts), warranty expiries (assets), planned maintenance (service-status), project alerts (projects). |
| Job health | includes/job_registry.php |
Those jobs' needed closures are false while their module is off, and pushed Hyper-V sources vanish with Assets β so nothing reads as overdue. |
| Calendar sync |
includes/calendar_sync/pull.php, api/tickets/schedule_feed.php
|
No task rows while Tasks is off. |
| Calendar writers | asset warranty, software licence, domains, projects, leases | Return synced => 0 and touch nothing while Calendar is off. |
| Portal |
self-service/includes/header.php ($portalNavModules), the endpoints, self-service/index.php
|
Help Centre (knowledge), Training (lms), My equipment (assets) and the Catalogue (forms) lose their tab and redirect home; their endpoints call requireModuleOnJson(); the dashboard drops Service Status, requests and popular articles. LMS portal learners are stopped in one place: LmsLearner::user() returns null. |
| Device agents | api/external/{system-info,device-manager,software-inventory}/submit |
Refused after the key check β nothing recorded, nothing queued. |
| Public links |
api/lms/test_public.php, lms/test.php
|
A test link reads as not valid. |
| Inbound integrations |
api/webchat/send.php, includes/integrations/zabbix.php
|
No AI answer without Knowledge; no CI lookup/link without the CMDB. |
| Notifications | NotificationsService::switchedOffClause() |
Not listed or counted, not deleted β they return with the module. |
| Watchtower | includes/watchtower_queries.php |
A card whose wtCardRegistry() module is off is hidden. |
| Checklists |
ChecklistsService::assertClosureAllowed(), checklists/ticket_view.js
|
A "block" closure rule cannot trap tickets while nobody can attach a checklist. |
tickets/index.php builds $ticketsModuleOn once β analystCanAccessModule() for problems, changes, assets, cmdb, tasks, checklists, knowledge β and uses it for the right-click menu and as window.TICKETS_MODULE_ON. In assets/js/inbox.js, ticketModuleOn(key) drives Link toβ¦, the problem/change pills, Ask AI, and the asset/CMDB/task loaders. The endpoints behind them (api/tickets/get_ticket_assets.php and friends) checked only Tickets, so they now also call requireModuleOnJson() β availability only, so who may link an asset to a ticket is unchanged.
Where the target module's own API already refused an analyst without access, the screen uses analystCanAccessModule(). Where today's screen deliberately works without the grant (Problems linking a change, Assets' Software and Contracts tabs, Leases' contract picker, an LMS author's KB article picker, Intune reporting), it uses moduleIsEnabled(), so not granted behaves exactly as before and only switched off hides it.
TICKET_FEATURES in includes/module_availability.php β csat, sla, time-tracking, rota β share the modules' switched-off list, so moduleIsEnabled($conn, 'sla'), requireModuleOnJson() and cronModuleSwitchedOff() work for them unchanged. A feature switch is availability only: who may use the feature is untouched. System β Modules lists them under Ticket features; api/system/get_module_access.php returns them as features.
| Feature | The checks |
|---|---|
| SLA |
sla_get_state() returns enabled=false first β that one line silences the pane, workspace summary, list pill, batch endpoint and breach check. cron/sla_breach_check.php skips (recorded in both histories); job health maps sla_breach. Settings writes, rules and cron-run endpoints refuse; get_sla_settings.php still answers, because the calendars stay for web chat. REST ticket_sla needs ['tickets','sla']; workflow segment sla maps to the feature; bell rows sla.warning / sla.breached are hidden; Watchtower's paused count is not run. |
| Time tracking |
timeTrackingUiOn() / timeTrackingApiOn() return false first, so every existing per-company gate follows. TicketsService::create/update/deleteTimeEntry() refuse with module_disabled β deleting was never gated before. REST ticket_time_entries needs ['tickets','time-tracking']. AI write-up and gap analysis stop reading minutes. |
| CSAT |
sendCsatSurvey() returns CSAT_RESULT_OFF first (covers every close path and in-chat sends). request_csat.php refuses. tickets/csat/survey.php shows no longer collecting feedback and writes nothing. In includes/messaging/ingest.php a digit falls through as a message; a button press is swallowed (Telegram still gets answerCallbackQuery). |
| Duty rota |
tickets/rota.php redirects; the 13 rota endpoints refuse; the War Room / MCP on_call tool carries 'feature' => 'rota' and is dropped from the tool list; includes/projects/capacity.php treats desk hours as off. |
The ticket page adds the features to $ticketsModuleOn (via portalModuleOn() β availability, not access), so ticketModuleOn('csat') and friends work in inbox.js; the workspace hides its Time tab when both SLA and Time tracking are off.
sla_run_breach_check($conn, true) walks the same tickets and triggers but sends nothing: it writes each warning and breach into sla_notifications_sent (recipients (not sent: SLA switched back on)) and, only where a workflow is listening, into workflow_scheduled_emissions (sla_mark_workflow_event()). api/system/save_module_availability.php runs it after an offβon flip of sla unless send_missed is true; it re-reads the list first with disabledModules($conn, true). Proven on a throwaway install with a genuinely breached ticket: four marks, nothing in email_send_log, and the next normal run silent for it.
git grep -n "GH #217" # every change this made
-
TRAP: a new REST resource for a switchable module goes in
API_V1_RESOURCE_MODULES, or it keeps answering while its module is off. -
TRAP: a new trigger family goes in
TRIGGER_MODULES(orTICKET_TRIGGER_SEGMENTS), or its workflows keep firing. -
TRAP: pass
$ownertorequireAnyModuleAccessJson()when the data belongs to one module. -
TRAP: never decide what someone may open from
getAnalystAllowedModules()β wrap it inmodulesWithoutSwitchedOff(), or callanalystCanAccessModule(). - TRAP: it fails open. Don't "fix" that into failing closed: a database hiccup would look like half the product vanishing.
| Test | Needs | Checks |
|---|---|---|
tests/module-availability.php |
Nothing | 16 β every REST resource and workflow trigger has a module (control: remove one and it fails); the maps and Watchtower cards name real modules or Tickets features; a ticket's SLA and time entries need their feature; no feature key collides with a module; the setting reader (no row = all on, a list, Tickets/System never off, a broken value fails open), one process per case on SQLite |
The end-to-end proof was run against a throwaway install, not committed (it switches a module off on the install it runs against). 20 checks, all passing: Tasks off through the real endpoint β its page redirects, its endpoint and the REST API answer module_disabled, REST Tickets still answers, the home page and the ticket pane stop offering it, the access checker gives the reason, Test fire is aborted saying why, Tickets refuses to be switched off; then back on and everything answers again.
- Add its key to
getModuleRegistry()and the waffle β it is on everywhere automatically. - Its pages and endpoints use
requireModuleAccess()/requireModuleAccessJson()as always β that's availability done for them. - Its REST resources go in
API_V1_RESOURCE_MODULES; its trigger prefixes inTRIGGER_MODULES; any action that writes into it inACTION_MODULES; its cron job usescronModuleSwitchedOff(). - Run
php tests/module-availability.php.
See also: Module Access β developer guide Β· REST API Β· Workflow engine β developer guide Β· Code traps
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π€ Contributing (read before a pull request)
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π§© Preference templates
- π Display order (modules, Watchtower cards) β Developer Guide
- 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: Files
- β³ π 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
- β³ π Switching modules off
- β³ π οΈ Module availability β Developer Guide
- β³ π Module availability β design (#217)
- 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
- β³ π Proposal: custom fields, inventory import and change evidence (#194)
- β³ π οΈ Custom fields, evidence + inventory - review (#195-#197)
- β³ π οΈ Long forms in a task's Work view - review (#198)
- β³ π« 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: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π οΈ Inbox.js map β Developer Guide
- β³ π οΈ Tickets settings β Developer Guide
- β³ ποΈ The ticket workspace
- β³ π The ticket table
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ 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
- β³ π Case reports - a ticket and its work as one PDF
- β³ π 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
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π§© Asset type layouts
- β³ π Inventory freshness
- β³ π Linked configuration item
- β³ π Connected assets
- β³ π Asset review
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ πͺ Hyper-V and Failover Clusters
- β³ π 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
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- π‘ Zabbix (monitoring)
- π¬ Microsoft Teams alerts
- π 3CX (phone system)
- 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
- β³ βοΈ #214 A "contains" condition did not match capitals
- β³ βοΈ #216 A workflow email from a chosen mailbox always failed
- β³ π³ The Docker image was missing PHP extensions
- β³ π A failed DNS lookup read as a missing record
- β³ π A link could change data
- β³ π Access outlived its removal
- β³ π Knowledge articles could run scripts
- β³ π Knowledge writes ignored permissions
- β³ π Company boundaries (Oct 2026)
- β³ π Smaller gaps (Oct 2026)
- β³ π Escaped text could leave its attribute
- β³ π Task descriptions were not cleaned
- β³ π Task documents ignored company scope
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ 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