Skip to content

Preferences Developer Guide

Ed Mozley edited this page Oct 25, 2026 · 1 revision

Preferences - Developer Guide

A map of system/preferences/index.php - each analyst's own Preferences page: its four tabs, the user_preferences key every control writes, the one preference mirrored into a cookie, and how Preference Templates copy those keys to other people. Users' view: Preference templates.

In plain words

Preferences is the one page in System that every analyst can open (system/includes/page_gate.php lets $current_page === 'preferences' past the administrator check). Everything on it belongs to the person signed in.

Nearly every control saves the moment it changes - no Save button - by posting one key and one value to api/system/set_user_preference.php, which upserts a row in user_preferences for the signed-in analyst. Whatever reads that preference elsewhere (the waffle menu, the Tasks board, the inbox) picks it up on the next page it draws.

The exceptions are My details and Signatures, which have their own endpoints in api/myaccount/, and the work calendar, which talks to api/tickets/calendar_enrolment.php.

The tabs and their keys

The tab bar is written directly from a $prefTabs array, not from a settings manifest: a manifest binds each tab to a capability, and there is nothing here to grant. Panels are general-tab, notifications-tab, display-tab and details-tab. Each section has an <h3 id="..."> anchor; the "Section" column below is that id.

General

Section Control Key Values Read by
language languageSelect interface_language a supported locale code; the page reloads after saving I18n::initFromSession()
timezone timezoneSelect timezone an IANA zone id; default is the server zone Tz::init()
date-format dateFormatSelect date_format a DateFmt::DATE_TEMPLATES key, or '' = follow System -> Date and time formats DateFmt (includes/timezone.php)
time-format timeFormatSelect time_format a DateFmt::TIME_TEMPLATES key, or '' DateFmt
money moneyMode, the pickers currency_format, currency_formats (JSON map per currency) '' = follow System -> Currencies Money (includes/money.php)
task-view taskViewSelect tasks_detail_view panel (default) or modal tasks/index.php
search-panel searchDismissToggle search_panel_close_outside on or '' the waffle menu, as window.SEARCH_PANEL_CLOSE_OUTSIDE
cal-subtasks calSubtaskSelect tasks_calendar_subtasks '', both or only; the calendar's own Show control writes the same key tasks/calendar/index.php
board-subtasks boardSubtasksSelect tasks_board_subtasks all, open or none. Unset falls back to the older tasks_hide_done_subtasks (on = open) tasks/index.php
start-page landingSelect default_landing_page analyst, portal, or '' = the administrator's choice. Mirrored into a cookie - see below includes/landing.php
watchtower wtImpersonalToggle watchtower_impersonal show or hide includes/watchtower_settings.php
working-hours workCalToggle and the panels below it mode and task options go to api/tickets/calendar_enrolment.php; the feed's detail level is tickets_schedule_feed_detail (full or ref) includes/calendar_sync/calendar_sync.php

The work calendar section is only drawn when scheduleFeedAllowed() says the install permits it; otherwise the section explains why not. Its subscribe dialogue is the shared renderSubscribeModal('workCal', ...) from includes/subscribe_modal.php, the same one the Calendar module opens. See Ticket calendar sync - Developer Guide.

Notifications

Section Control Key Values Read by
notifications one .notif-type switch per type notification_types one JSON object, type => 1 or 0 includes/services/notifications.php
sound notifSoundSelect notification_sound off (default), chime, ping, knock; choosing one plays it the waffle menu, as window.NOTIFICATION_SOUND
desktop desktopNotifToggle desktop_notifications on or ''. Switching on asks the browser for permission first; if refused, the switch goes back off and nothing is saved the waffle menu, as window.DESKTOP_NOTIFICATIONS
task-email taskEmailToggle task_assigned_email on or '' notificationsTaskAssignedEmail() in includes/notifications_router.php
position toastPositionGrid (nine cells, built in JS) toast_position top-left ... bottom-right; default bottom-right the waffle menu, as window.TOAST_POSITION
animation animToggle toast_animation slide (default) or fade the waffle menu, as window.TOAST_ANIMATION

The notification types are rendered from NotificationsService::effectivePreferences(), so a type added to the service appears here with no second list. See Notifications - Developer Guide.

Display

Section Control Key Values Read by
module-order moduleOrderList (ReorderList) module_order a JSON list of every module key, including ones the analyst cannot open today; Reset saves '' analystModuleOrderSaved() in includes/display_order.php
app-sidebar appSidebarToggle app_sidebar 1 or '' appSidebarPrefs() in includes/app_sidebar.php
appSidebarNames app_sidebar_names icons (default), hover, pinned
appSidebarWaffle app_sidebar_waffle hide, or '' for show
the module ticks app_sidebar_modules a JSON list of module keys; never saved = every module
left-panels one row per entry in SIDEBAR_PANELS knowledge_sidebar_mode, process_mapper_sidebar_mode, contracts_sidebar_mode, calendar_sidebar_mode, tasks_sidebar_mode, cmdb_sidebar_mode, change_management_sidebar_mode, asset_management_sidebar_mode, system_wiki_sidebar_mode always (default) or hover each module's header; the module's own Settings left-panel tab writes the same key
multi-select multiSelectPaneToggle tickets_multiselect_pane summary (default), keep, bar assets/js/inbox.js
morning-checks mcFillToggle mc_chart_fill_style plain (default) or gradient morning-checks/index.php

See Display order - Developer Guide and App sidebar.

My details

Section What Endpoint
my-details Job title, department, phone, mobile api/myaccount/save_profile.php (Save button, saveMyDetails())
signature The analyst's email signatures, edited in place with TinyMCE and a live preview api/myaccount/get_signatures.php, save_signature.php, delete_signature.php

None of these take an analyst id - they act on the signed-in analyst only. See Email signatures - Developer Guide; the "managed" note on My details is explained in SSO JIT and profile sync.

Not on this page: theme

The colour theme is picked from the waffle menu, not here. setTheme() in includes/waffle-menu.php saves the key theme through the same set_user_preference.php and reloads. Theme::active($module) reads theme_<module> first, then theme, then the default. See Theming and dark mode.

Tickets row display (inbox_row_display) is also a per-analyst preference that lives elsewhere: on the Row display tab of Tickets -> Settings, saved by api/tickets/save_inbox_display.php. See Tickets settings - Developer Guide.

How a control saves

async function savePref(key, value)   // POST {key, value} to api/system/set_user_preference.php

savePref() returns true on success and shows an error toast otherwise. On success it also updates window.TOAST_POSITION, window.TOAST_ANIMATION or window.NOTIFICATION_SOUND so the change shows on this page without a reload. A few handlers set their own global the same way (window.DESKTOP_NOTIFICATIONS, window.SEARCH_PANEL_CLOSE_OUTSIDE).

Patterns used:

  • wireToggle(rootId, dataAttr, prefKey, initial, onSaved) - a row of .anim-option buttons, each carrying a data-* value. Used for animation, Morning Checks fill, the multi-select pane and every left-panel row.
  • A <select> or switch with a "Saving..." hint - a change listener calls savePref() and shows the section's .pref-saving-hint.
  • A queue - Module order and Money chain their saves on a promise, one at a time, so two quick changes cannot land out of order. A failed Module order save puts the list back to the last saved order.

watchtower_impersonal is the one control that posts to set_user_preference.php directly rather than through savePref(), and the one that reads its value on load with api/system/get_user_preference.php?key=watchtower_impersonal. It puts the buttons back if the save fails.

The endpoints

  • set_user_preference.php - POST {key, value}. Requires a signed-in analyst and a write method (csrfRequireWriteMethod()). The key must be 1-100 characters; the value is capped at 60,000 bytes. It does not check the key against a list: it writes whatever key it is given, for the caller's own row only. A null or '' value is stored as given - the row stays. Then it calls landingOnPreferenceSaved($key, $value).
  • get_user_preference.php - GET ?key=.... Returns {success, key, value}, with value null when there is no row.

Because any key is accepted, the code that reads a preference is what validates it - Theme::isValid(), landingIsValid(), the in_array() checks in appSidebarPrefs(), and so on. Read a new preference the same way.

Loading

The PHP at the top reads every key in $prefDefaults in one query and passes them to the script as INITIAL_PREFS, so the page opens on the saved values rather than flickering from defaults. A row holding '' counts as not set - the default stands. Keys not in $prefDefaults are read by their own helpers: interface_language (I18n::getLocale()), notification_types (NotificationsService::effectivePreferences()), module_order (analystModuleOrderSaved()), the sidebar keys (appSidebarPrefs()) and the money keys (Money::personalSettings()).

Tabs and links

switchTab(tab) shows #<tab>-tab and writes #<tab> into the address with history.replaceState(). On load, a hash that names a tab opens it (the reply box's "Manage signatures" links to #details); a hash that names a section (#app-sidebar) opens the tab holding it and scrolls the section into view.

The cookie-mirrored preference

default_landing_page decides where / sends someone, and index.php has to know that before anyone has signed in. So it is also kept in a cookie, freeitsm_landing (includes/landing.php):

  • On save - set_user_preference.php calls landingOnPreferenceSaved(). For any other key it does nothing. For this key it writes the cookie with landingSetCookie() if the value is valid (analyst or portal), and clears it otherwise - which is how "use the site default" works.
  • On login - landingRefreshCookieFromPreference() re-issues the cookie from the stored preference, so the choice follows the person to a new browser. It is called from auth/login.php, api/auth/oidc_callback.php and api/myaccount/verify_login_otp.php.
  • On use - landingResolve() trusts the cookie only if landingIsValid() accepts it. The stored value is a key, never a path; landingTargets() is the only place a URL exists.

The cookie is HttpOnly, SameSite=Lax, a year long. Full detail: Landing page - Developer Guide.

Preference Templates

System -> Preference templates (system/preference-templates/index.php, administrators only) saves named sets of these preferences and gives them to people. The endpoint is api/system/preference_templates.php (behind includes/admin_api_guard.php); the logic is includes/preference_templates.php.

Action What it does
GET ?action=list Every template, the sections a template can hold, and the active analysts
POST save name, description, for_new_analysts, sections[], and capture_from (an analyst id)
POST delete Deletes the template; nobody's preferences change
POST apply id and analyst_ids[]; logged to system_logs as preference_template_applied

How it works:

  • PREF_TEMPLATE_SECTIONS maps each section id - the same <h3 id> anchors as the Preferences page - to its heading and its user_preferences keys. board-subtasks carries both tasks_board_subtasks and the older tasks_hide_done_subtasks; app-sidebar carries all four sidebar keys; left-panels all nine *_sidebar_mode keys.
  • Capture - prefTemplateCapture() copies the ticked sections' keys from one analyst's real preferences. A key that analyst never set is stored as null. Editing a template without capture_from keeps what it had, narrowed to the sections still ticked.
  • Apply - prefTemplateApply() writes each key into each chosen analyst's user_preferences, and deletes the row where the template holds null, so the person ends up exactly as the source was. One transaction (it joins one already open). Afterwards everyone can still change their own.
  • For new analysts - one template can be marked; saving that flag clears it on every other template. prefTemplatesApplyForNew() gives it to an analyst when they are created, from api/tickets/save_analyst.php, includes/services/analysts.php, includes/ldap.php and api/auth/oidc_callback.php. It never throws.
  • Not in templates - My details, Signatures, the work calendar, and tickets_schedule_feed_detail (listed in PREF_TEMPLATE_PERSONAL). The theme is not on the Preferences page, so it is not in a template either.

Applying a template writes rows directly; it does not go through set_user_preference.php, so it does not touch the freeitsm_landing cookie. A start page given by a template takes effect in a browser the next time that person signs in there.

Traps

  • TRAP: a new setting on the Preferences page is not in templates until its key is in PREF_TEMPLATE_SECTIONS (includes/preference_templates.php). tests/preference-templates.php fails when a key the Preferences page saves is in neither that list nor PREF_TEMPLATE_PERSONAL. Adding a preference means: a default in $prefDefaults (if the page should open on it), the control and its savePref() call, a section entry for templates, and validation where it is read.
  • '' is a real value for some keys. For date_format, time_format, currency_format and default_landing_page it means "follow the install default", and it is saved as it is. Do not treat it as "nothing chosen" and skip the save.
  • An unloaded control looks like a choice. That is why the page reads values in PHP before it draws, why Watchtower reads its value rather than assuming show, and why a failed signatures load says so instead of showing "no signatures".
  • The work calendar's mode is sent with every task-option change. calendar_enrolment.php takes both on one POST, and leaving the mode out would be read as switching the whole thing off.
  • Left-panel keys have two editors. This page and each module's own Settings left-panel tab write the same *_sidebar_mode key. A new module with a left panel needs an entry in SIDEBAR_PANELS, a default in $prefDefaults, and a key in the left-panels template section.
  • The watchtower namespace is loaded for one section. $translationNamespaces includes it only for the Watchtower "Mine" strings; remove it and those labels render as raw keys.

Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally