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.

Moodle track details

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:

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:

  1. app renders the question and understands the mechanisms
  2. bridge stores the id in cmi.comments and writes the data for scorm using scormApi calls
  3. 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
  4. 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