-
Notifications
You must be signed in to change notification settings - Fork 15
LMS Authoring
How FreeITSM lets you write a course, rather than only upload a SCORM package authored elsewhere. Companion to the module hub LMS; for the AI helpers see LMS AI Authoring.
Before this, publishing a single page of text meant buying Articulate or Rise, exporting a zip and uploading it β a strange front door for a free, self-hosted ITSM. Now you press Create and write the course in the app.
A tempting alternative is to author in a database but compile to a SCORM zip and play it in the existing iframe. FreeITSM deliberately does not do this, for two reasons that outweigh the portability a zip would give:
- You'd write the player twice. A SCORM package is static files; something inside it must still render the lessons, collect answers, grade, and call the SCORM API β the same player code, plus a compiler and a manifest generator. Native is strictly less code, not more.
- The answer key would ship to the browser. If grading happens inside the package, the correct answers sit in a JS file the learner can open in devtools β exactly why SCORM quizzes are trivially cheatable. Native keeps grading on the server (see below).
The database is the source of truth either way, so "Export as SCORM" remains a viable future feature β generated on demand for portability to another LMS β without being the runtime.
A course is a tree, three tables deep, all cascading on delete:
lms_courses (content_type='native', pass_mark)
ββ lms_lessons (ordered; body = TinyMCE HTML)
ββ lms_questions (question_type: single | multiple | truefalse)
ββ lms_answers (answer_text, is_correct β the key)
-
Lessons are written in TinyMCE, the same editor and same trust level as a Knowledge article β including inline base64 images, so a lesson carries its own media with nothing on disk to lose or leak. Reorder by dragging;
display_orderis the position. - Questions hang off a lesson and are asked at its end. Three types: choose-one, choose-several, true/false. Each has an optional explanation shown to the learner after they answer.
-
Answers are a question's options.
is_correctis the answer key and never leaves the server (see Grading).
Gated by lms.manage. Left: the lessons, draggable. Right: the selected lesson's body in TinyMCE, and the questions that follow it.
- Course settings β title, description, and the pass mark (0β100, or blank for "no pass mark").
- Lessons β add, rename, write, reorder, delete. A lesson save writes only the body; questions have their own endpoint.
- Questions β a modal per question: the text, the type, the answers (radio for single/true-false, checkboxes for multiple β the control tells the author the rule), which are correct, and the explanation.
Question validation happens at the door, server-side in api/lms/questions.php, and refuses author mistakes rather than discovering them in the results:
- at least two answers, each with text;
- at least one correct answer (a question with none would silently mark everyone wrong);
- a
single/truefalsequestion may not have two correct answers (a contradiction) β the error nudges you to "Choose several".
An authored course has no package and no launch URL, so player.php detects content_type = 'native' and hands off to the native player instead of the SCORM iframe. It renders lesson-by-lesson from the database, so it inherits the app's theme and dark mode rather than being a foreign white box.
- A contents list, a progress bar, and resume (the last lesson is the bookmark).
- At the end, an answer review: what you got wrong, the correct answer, and the explanation.
- Content is fetched from
api/lms/course_content.phpβ the one endpoint that returns a course to a learner, and the one that never selectsis_correct. There is no filtering step to forget: the answer key is simply never on the wire.
api/lms/native_progress.php grades a submission by re-reading the correct answers from lms_answers and comparing. The client posts only which answer ids were chosen; it has no key to check against and cannot be made to lie about the result.
- Right = exactly the correct set, no more and no less (ticking every box is marked wrong).
- Score = percentage of questions correct across the whole course.
- With a pass mark, the learner is
passed/failedagainst it and may retake; without one, finishing iscompleted; a course with no questions iscompletedon reaching the last lesson.
The clever bit that avoided a second admin UI: the grader writes each result as SCORM CMI rows into lms_cmi_data using the real element names β cmi.interactions.N.id, .learner_response, .result, .correct_responses.0.pattern, and cmi.score.raw. Because the admin Progress tab and the Learner Data drill-down already read those, an authored course lights them up with no new admin code at all. See LMS.
The LMS is the pilot module for Roles (RBAC "Layer 2"). The whole surface is divided by one capability, lms.manage:
| Surface | Gate | Who |
|---|---|---|
| Admin dashboard, editor, all management APIs | lms.manage |
Managers + admins |
| My Courses page + its feed | the lms module |
Every learner |
| Playback, learner content / progress / SCORM-runtime APIs |
assigned-to-me or lms.manage
|
Learners (their courses) + managers (any, = Preview) |
Enforcement lives in one helper file, includes/lms_access.php, so the rule can't drift between the pages, the APIs and the player:
-
lmsCanManage($conn, $analystId)β thin wrapper overanalystHasCapability('lms.manage')(sois_adminbypasses). -
lmsCourseAssignedTo($conn, $analystId, $courseId)β is the course assigned to a group the analyst is in? -
lmsCanAccessCourse(...)β manage or assigned. The playback gate. -
requireLmsCourseAccessJson($conn, $courseId)β the 403 twin for learner APIs. -
lmsMyCourses($conn, $analystId)β the assigned-to-me feed behind My Courses.
lms/ (the dashboard) routes a non-manager to my-courses.php, and the header nav adapts β learners see My Courses + Help, managers additionally get Dashboard + Settings.
Manager β admin. A manager is a non-administrator who holds an
lms.manageRole. This is exactly the point of the Roles system: delegate running the LMS without handing someone the whole System module.
Uploaded packages extract into lms/content/<id>/ β a web-served directory Apache can execute from β so an unguarded ZipArchive::extractTo() would let anyone with the LMS ship a .php shell and run code as the web server. includes/lms_package.php vets every entry before anything is written (and before the course row is created, so a rejected upload leaves nothing behind):
- an allowlist of course file types (HTML/CSS/JS/images/media/fonts/PDF) β never
.php, never an extension-less file; - rejection of any path that escapes the folder (zip-slip:
../β¦, absolute,C:\β¦); - zip-bomb caps β total uncompressed size, entry count, and per-entry compression ratio.
Only the vetted entries are extracted, never the whole archive. As defence in depth, a committed .htaccess in lms/content/ disables code execution for the whole tree β and .htaccess is itself on the deny list, so a package can't ship one to undo it.
| File | Role |
|---|---|
lms/editor.php Β· assets/js/lms-editor.js
|
The authoring surface (gated by lms.manage) |
lms/native-player.php Β· assets/js/lms-native-player.js
|
The learner's player for authored courses |
lms/my-courses.php Β· assets/js/lms-my-courses.js
|
The learner landing |
api/lms/lessons.php Β· questions.php
|
Author-side content APIs (return the key) |
api/lms/course_content.php |
Learner content feed (never returns the key) |
api/lms/native_progress.php |
Server-side grading + progress |
api/lms/my_courses.php |
The assigned-to-me feed |
includes/lms_access.php |
The learner/manager access rules |
includes/lms_package.php |
Safe SCORM extraction |
- LMS β the module hub
- LMS AI Authoring β the three AI helpers in the editor
-
Roles & Permissions β the
lms.managecapability - Knowledge β the article source for AI-drafted lessons
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)