About Scorm
Scorm is a system supported by many LMSs that offers a way to store and retrieve data using a common javascript API, so if your content is authored to communicate using a Scorm standard, it should work on any LMS that also supports that version.
Scorm comes in two ancient versions - 1.2, and 2004. (2004 has v2, v3 and v4 slight variations). After Scorm came the more complicated xAPI, but that's a whole other kettle of fish. Scorm 2004 is generally considered to be more capable and easier to manipulate user data, but is also over-engineered and more difficult to fully implement on a server. Scorm 1.2 is easy to implement, which makes it reliable and common, but is more restrictive and has some bizarre API choices (such as write-only properties, or only allow 4 kilobytes of storage in total). The documentation is hard to follow and fails to clarify many details such as data sizes. Some LMS's extend or do not validate data sizes because they are so restrictive.
cmi.interactions.n
We are concerned with storing long-form text, which is best done using the cmi.interactions collection. This is often exposed through a management interface in the LMS and allows teachers to drill down to examine learner responses, as seen in a snippet from Moodle below. We will want to preserve this ability.

When you want to store longer text entries, it's easy enough with Scorm 2004. It allows an interaction to read AND write various field values (so you can look up previously stored values). Scorm 1.2 had many write-only properties, so it wasn't easy to know what had previously been saved. The id property is a good example - it can be used to identify or find a given response, but in Scorm 1.2 you can only save it, not read it back. You need to look it up in order to save properly.
| Property | 1.2 | 2004 | Useful | Quirk |
|---|---|---|---|---|
| id | 255 bytes | 4000 bytes | y | Write-Only in Scorm 1.2 |
| type | fill-in | long-fill-in | y | Only 255 bytes in Scorm 1.2, |
| student_response | 255 bytes | - | ~ | write only, can't read it back |
| learner_response | - | 4096 bytes | y | most LMS's allow more data (usually 64k) |
| description | - | 250 bytes | y | can be used to store a question for context |
Overcoming Scorm 1.2's restrictive properties.
The main problems with Scorm 1.2 are:
- Most properties are write only - you can't look up what you've previously saved
- Most properties are only allow a quarter of a a kilobyte of storage (perhaps 30 words)
- There's no good way to store a question/context for a response
To be able to read and write interactions in Scorm 1.2, we need to store data somewhere else, in a way that is compatible with Articulate Rise. Rise already uses the cmi.suspend_data field to remember page completions, so that's not an option. Another candidate field is:
cmi.comments (CMIString (SPM: 4096), RW) Textual input from the learner about the SCO
It's both read and write, is 4k long and is not used by Rise. That is 4k in total, so if you have multiple questions/responses to store, you run out of room pretty fast (even with data compression). We will use cmi.comments as a dictionary to look up interaction ids, and store public urls for the learners responses. The public url will be abstract so there will be no way to figure out the course, the user, the domain or other contextual clues; the main benefit being that a url is generally less than 250 bytes and the page can store much more than 4 kilobytes per response. Teachers will be able to use the LMS tracking tools to see the URLs and examine the learner responses.
Implementation
Our code will run in an iframe, to be injected into Articulate Rise using Mighty or other means. The code only communicates when there is a SCORM implementation available. It is compatible with both Scorm 1.2 and 2004 and will use the best method for storing and retrieving data based on the version.
This site contains server routines for storing and loading student responses. Its a fairly naive implementation, relying on disk-based storage (no database) and simple scripts. Javascript code within the iframe communicates to the server to both read and write users responses, and the server makes data available through public urls with unguessable names.
Here is the core implementation for storing data and looking up the URL that will be saved into the scorm interaction data.
function getInteractionUrl(courseId, learnerId, interactionId, key = "") {
key = key ?? ""; // an optional extra parameter to salt the hashing function
return fetch(DATA_ENDPOINT, { // call the server to save the data
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}) // add a Bearer (hash)
},
body: JSON.stringify({
course: courseId, // identify the course
learner: learnerId, // identify the learner
interaction: interactionId, // identify the interaction
key, // add salt if required
content: __latestTextToSave, // the text to save
question: questionText // adds context to the text
})
})
.then(res => res.json())
.then(data => data.url || null); // returns a public url that shows the question and answer
}
And a breakdown of what the variables mean:
| Variable | Meaning |
|---|---|
| DATA_ENDPOINT | The url of the server that stores the value. You can implement your own and configure it when you add the interaction. |
| token | An authorization token that the server uses to ensure the data is acceptable. If you use this site, you can generate a token on the next page. |
| courseId | the unique id that Rise assigns to your course. It's random letters and numbers. |
| learnerId | the cmi.core.student_id, which whatever your LMS makes available through the Scorm API. I don't know what kind of data this is. |
| interactionId | the identifier for the scorm interaction, usually made from the question text or a quasi-random value or the value you specifically set. |
| questionText | The text set by you to identify the response. |
| key | A salt that can be used to make the hash more entropic |
The function returns a URL (for example https://rise.frumbert.org/dce4093991be9a8acb23b524eb561263.html). There is nothing in the file that can be used to identify the course, learner, interaction or other private detail. Here is the specific code my server uses to generate the filename:
$hashInput = $course . "|" . $learner . "|" . $interaction . "|" . $key;
$hash = substr(hash('sha256', $hashInput), 0, 32); // safe hash
$filename = DATA_DIR . "/{$hash}.html";
Getting the code & Generating tokens
The token generator and server code are available here.
The entire source code to the live server (and this whole site) is available on my GitHub.
iframe-only
in order to support iframe only mode you would still need a way to inject the token and initialisation code and there would, for a time, be a problem with domain boundaries and scorm. You would have to link an iframe like ≤iframe src="https://rise.frumbert.org/embed/(guid-guid-guid)/"≥ and that code would know a) the token, b) the interaction type and c) the initialisation script. You would then have a separate tool where you upload your finished Rise course and it injects all the iframes and downloads the course with everything patched.
scorm differences
as this is based on the long-text-entry system, where scorm 2004 is leveraged to do everything itself, and scorm 1.2 is bypassed and used as a recording system, much of the same logic can still apply, however rise.frumbert.org's internals need to be adapted to understand the different data structures and to be able to load/store data. only fill-in and performance types make sense to store long-form data in html files, other question types should persist into plain text that follows the scorm data size rules.
The process for scorm 1.2 will be:
- app renders the question and understands the mechanisms
- bridge stores the id in cmi.comments and writes the data for scorm using scormApi calls
- bridge stores the persistence data in html file on the server, and reads it back on load in order to rebuild the object cache for the next write
- the html on the server needs to be done in such a way that it can easily store/return the format for the interaction type. can look up persisted data as hash.html or hash.json
updates to api endpoints
endpoints /generate and /view can accept a 'type' field, which can be 'json' or 'html'.
endpoint /hash.html returns text/html
endpoint /hash.json returns application/json