Путеводитель по SCORM или увлекательное приключение с фреймами во фронтенде

Я работаю фронтенд - программистом и мы разрабатываем систему управления обучением или learning management system (LMS). Наша LMS система позволяет работать как с конечными клиентами (B2C), так и с корпоративными (B2B).
Например, легко можно загрузить видео, аудио, pdf, всё это разбить на разделы и в конце организовать тест и менеджер клиента может посмотреть отчёт о том, кто с какой попытки прошёл этот тест и с каким результатом.
Кроме того, наша LMS система интеллектуальная на основе нейросетей и генетических алгоритмов, она рекомендует пользователю, какой бы ещё похожий курс купить и пройти. Определяет шаблоны поведения пользователя, если пользователь прошёл курс за пару минут, явно что-то пошло не так и сообщает об этом менеджеру. Или это видео пользователи слишком часто перелистывают, с видео явно что-то не то.
Появился крупный клиент, который сообщил, что он не хочет создавать курсы, курсов у него уже созданных вагон и маленькая тележка, но эти курсы в формате SCORM 2004. А можно так сделать, чтобы они проигрывались в нашей системе?
Клиенту ответили, а почему бы и не да? Начали разбираться, что за зверь по имени SCORM? Если по-простому SCORM (Sharable Content Object Reference Model — модель ссылок на совместно используемые объекты содержимого) - это такой формат обучающих курсов, который создаётся в одной программе, упаковывается в zip-архив и может воспроизводиться в любой системе, где есть SCORM-плейер. Конкретно у этого клиента большинство SCORM-курсов очень древние и были созданы в PowerPoint + специфическое расширение для работы с этим форматом.

Путеводитель по SCORM или увлекательное приключение с фреймами во фронтенде

На дворе 21 век SPA приложения начинают мигрировать на Islands Architecture, нейронные сети и искусственный интеллект делают решительный шаг в захвате рынка.
SCORM-курс же в своей технической реализации представляет собой просто набор файлов с xml-манифестом, который взаимодействует с исходной системой при помощи определенных API-команд. В целом, это классический многостраничный html-сайт с js-кодом на страницах. При чём сайт где-то 20 летней давности, если судить по JS-коду. Про стандарты ES5, ES6, разработчики даже не слышали.
Если кратко взаимодействие со SCORM-курсом выглядит вот так:

  1. Браузер находит объект API_1484_11(хорошо запоминающееся название), который располагается в одном из родительских окон браузера или в iframe.
  2. Затем вызвать метод Initialize("") этого объекта.
  3. После успешной инициализации можно запрашивать у системы данные посредством метода GetValue(«название_элемента_данных») или посылать данные в систему посредством метода SetValue(«название_элемента_данных», «значение»).
  4. Для принудительного сохранения данных, отправленных в систему, должен вызываться метод Commit("").
  5. Также API позволяет отслеживать возможные ошибки, возникающие в процессе взаимодействия, посредством методов GetLastErrorGetErrorString и GetDiagnostic.
  6. В конце своей работы нужно вызвать метод Terminate("")

Бекендер долго ругался, но написал свой веб-сервер, который реализует эти команды, я через асинхронный цикл в ReactJS подгрузил необходимые файлы библиотеки для реализации этого SCORM API. И SCORM-курс даже стал отображаться, на это занимательное занятие ушло где-то 1 месяц, но там в основном рутина.

Путеводитель по SCORM или увлекательное приключение с фреймами во фронтенде

Самое интересное пошло дальше. Результаты тестов по этому SCORM-курсу должны появляться в отчётах менеджера. Например, такой-то пользователь с такой-то попытки прошёл тест с таким-то результатом.
Наш SCORM-курс открывается в отдельном фрейме при помощи тега iframe.
Как думаете что может сделать в ReactJS с iframe. Да, ничего! Размер поменять, и установить некоторые атрибуты отображения.
В настоящее время iframe является очень надежным средством защиты, замкнутая песочница, поскольку родительский сайт не имеет доступа к информации, которая передается внутри дочернего документа, и наоборот. Их работа происходит независимо друг от друга.
А если нет возможности получить информацию из фрейма, но очень хочется, что делать?
Посидели и обсудили с бекендером - техлидом проблему, как получить результаты итогового теста. SCORM-курс собирает данные теста куда бы вы думали? В store, localstorage? Нет, в скрытую форму на отдельном iframe и по команде commit он отправляет данные этой формы.

Путеводитель по SCORM или увлекательное приключение с фреймами во фронтенде

SCORM-курс открывает корневой iframe, который открывает ещё 2 iframe: сам курс и скрытая форма.

Путеводитель по SCORM или увлекательное приключение с фреймами во фронтенде

Решили переписать метод submitForm у библиотеки, чтобы она отправляла напрямую данные формы на наш сервер.
Возникает вопрос. Есть же великолепная функция Window.postMessage, которая отправляет сообщение из iframe и родительское окно. И это сообщение можно перехватить. Думали и над этим вариантом, дело в том, что SCORM-курс находится на нашем сервере. Поэтому мы в одном месте отправляем объект, а в другом месте системы мы его перехватываем. Возникает 2 точки отказа. А когда правим код в одном месте проще искать ошибки. Но не волнуйтесь работа с Window.postMessage нас ещё ждёт, а точнее поджидает.
Всё хорошо, но о том, что есть какое-то React-приложение этот SCORM-курс из своей песочницы даже не подозревает. Я поместил JWT accessToken в localStorage, а в методе submitForm его считал. Всё хорошо.
Но пользователь может проходить тестирование несколько раз для этого у нас есть sessionId, чтобы различать сессию пользователя. Я её тоже поместил в localStorage, но через некоторое время понял, что ошибся. Кроме SCORM-курсов у нас есть самые обычные курсы. По логике клиента пользователь прослушал курс, прошел тест, если неудачно, пользователь ещё раз внимательно прослушал курс и прошёл тест ещё раз. Но подлые пользователи не захотели по несколько раз проходить курс. Они открывали вторую вкладку браузера, копировали ссылку перед прохождением теста, и если проходили неверно, но во второй вкладке проходили тест ещё раз. И так повторяли этот алгоритм до успешной сдачи теста. Но если хранить sessionId в localStorage, когда они проходят второй раз вываливается ошибка с инвалидацией sessionId. Они пугаются и пишут в тех. поддержку о том, что сайт неправильно работает. В результате sessionId переехал в sessionStorage и все счастливы. 🙂
В результате для отправки запроса accessToken берётся из localStorage, а sessionId из sessionStorage. Вот так небольшой модификацией библиотеки решил проблему сохранения результатов теста.
Думаете, это всё? Нет, у нас ведь интеллектуальная система, которая на базе нейросети формирует patterns of user behavior (шаблоны пользовательского поведения). Но она это делает на основе определенных пользовательских действий, например, клику мыши. А как ReactJS может получить данные из iframe. Правильно, никак.

Путеводитель по SCORM или увлекательное приключение с фреймами во фронтенде

Снова пообщался с техлидом. И решили написать небольшой скрипт event.js. Наш внутренний веб-сервер, будет анализировать index.html у scorm-курса и дописывать его в конец файла, перед тегом body. Этот скрипт вешает на событие клика мыши наш обработчик и через API посылает элемент на котором произошёл клик.
Но мы вспоминает, что наш SCORM-курс был получен на основе PowerPoint и если элемент выглядит как кнопка, ведёт себя как кнопка, но это никак не кнопка, а дикий набор тегов div, span, svg. Поэтому я вспомнил, что у функции JSON.stringify есть ещё один параметр, который может фильтровать разный хлам.

const skipKeys = ["style", "innerHTML", "outerHTML", "parentObj", "namespaceURI", "m_playbackController"];  
return JSON.stringify(  
   obj,  
   (key, value) => {  
      if (  
         !value ||  
         value instanceof Node ||  
         value instanceof Window ||  
         value instanceof Array ||  
         typeof value == "function" ||  
         skipKeys.includes(key)  
      )  
         return;  
      return value;  
   },  
   " "  
);

Отправили нашей системе наконец события пользователя.
На самом деле передали ещё событие scroll, SCORM-курс не всегда влезал во фрейм, но это аналогично.
Но и это ещё не всё, пришёл второй крупный клиент. Сказал, что у него тоже полно SCORM-курсов. Но нас как-то насторожил размер представленных курсов, обычно SCORM занимает несколько гигабайт, а здесь какие-то жалкие 50-100 килобайт.
Начали разбираться его SCORM-курсы представляют собой файл манифеста и index.html следующего содержания

<iframe src="https://sait.loc/" title="This is a unique title!" name="html-iframe-01" referrerpolicy="no-referrer" loading="lazy" allowfullscreen=""></iframe>

Т.е. просто iframe ведущий на сервер клиента. На вопрос, а как мы будем сохранять результаты и действия пользователя в нашей системе от менеджера клиента получили текст с нулевым содержанием.
Созвонились с программистами второго клиента и пришли к следующему техническому решению. Ifame может общаться с родительским окном через отправку команды Window.postMessage
Например, он может послать window.top.postMessage("Hello", "https://sait.loc/");
А родительский объект может это сообщение поймать. Вот всё хорошо, но я запустил отладку и понял, чтобы мой браузер просто кипит этими сообщениями, их рассылают расширения браузера. Поэтому их необходимо фильтровать по имени домена. В результате клиент в своих курсах встроил отправку сообщений об результатах теста и кликах пользователя. А наш скрипт, который существует одновременно с ReactJS перехватывает сообщения клиента и отправляет их в нашу LMS-систему.

window.addEventListener("message", postMessageHandler, false);

Но это ещё не всё, к нам опять пришёл первый клиент и предложил реализовать новый формат курсов для дистанционного обучения Tin Can API (также известен как Experience API или xAPI), "убийцу" SCORM. Мы почитали спецификацию. Там было, в том числе:

  • Возможность работы с материалом офлайн.
  • Повышенный уровень безопасности. Так как все курсы страшная коммерческая тайна, то на устройстве пользователя их необходимо шифровать при скачивании и расшифровывать при воспроизведении.
  • Доступ к все возможным сенсорам мобильных устройств от микрофона до датчика наклона.

Подумали и решили клиенту как бы сказать мягко, отказать, а для надёжности посчитали цену и сроки полной реализации этого формата. Клиента полученные результаты поразили и он решил подумать. Думает до сих пор.

Путеводитель по SCORM или увлекательное приключение с фреймами во фронтенде

Вот такой занимательный зверь по имени SCORM и во время работы с ним можно узнать уйму увлекательных методов получения информации из фрейма и получить гарантированный геморрой при негарантированном результате. Но в нашем случае добро победило, но с особой жестокостью.