From 856df0b583864426868f267f3cae84ede8d87ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9ia=20Bohner?= Date: Wed, 20 May 2026 18:11:18 -0300 Subject: [PATCH 01/16] Import common cartridge initial commit --- dist/filament-lms.css | 102 ++++ resources/css/plugin.css | 102 ++++ .../pages/create-test-entry.blade.php | 2 +- .../livewire/lms-test-form-show.blade.php | 23 + src/Jobs/ImportCommonCartridgeJob.php | 162 +++++ src/Livewire/LmsTestFormShow.php | 104 ++++ ...onCartridgeImportCompletedNotification.php | 40 ++ .../CommonCartridge/ArticulateFrameParser.php | 132 ++++ .../ArticulateSlideContentExtractor.php | 577 ++++++++++++++++++ .../CommonCartridgeImportService.php | 389 ++++++++++++ .../CommonCartridge/FrameResourceEntry.php | 16 + .../CommonCartridge/LessonStructure.php | 22 + .../CommonCartridge/ManifestParser.php | 301 +++++++++ .../CommonCartridge/ParsedManifest.php | 28 + src/Services/CommonCartridge/ResourceData.php | 20 + .../CommonCartridge/StepStructure.php | 19 + tests/Feature/CommonCartridgeImportTest.php | 81 +++ tests/Feature/StepRenderedTextTest.php | 85 +++ .../ArticulateSlideContentExtractorTest.php | 130 ++++ 19 files changed, 2334 insertions(+), 1 deletion(-) create mode 100644 resources/views/livewire/lms-test-form-show.blade.php create mode 100644 src/Jobs/ImportCommonCartridgeJob.php create mode 100644 src/Livewire/LmsTestFormShow.php create mode 100644 src/Notifications/CommonCartridgeImportCompletedNotification.php create mode 100644 src/Services/CommonCartridge/ArticulateFrameParser.php create mode 100644 src/Services/CommonCartridge/ArticulateSlideContentExtractor.php create mode 100644 src/Services/CommonCartridge/CommonCartridgeImportService.php create mode 100644 src/Services/CommonCartridge/FrameResourceEntry.php create mode 100644 src/Services/CommonCartridge/LessonStructure.php create mode 100644 src/Services/CommonCartridge/ManifestParser.php create mode 100644 src/Services/CommonCartridge/ParsedManifest.php create mode 100644 src/Services/CommonCartridge/ResourceData.php create mode 100644 src/Services/CommonCartridge/StepStructure.php create mode 100644 tests/Feature/CommonCartridgeImportTest.php create mode 100644 tests/Feature/StepRenderedTextTest.php create mode 100644 tests/Unit/ArticulateSlideContentExtractorTest.php diff --git a/dist/filament-lms.css b/dist/filament-lms.css index 56a52ab..d91d916 100644 --- a/dist/filament-lms.css +++ b/dist/filament-lms.css @@ -38,3 +38,105 @@ .step-material-container iframe[src*=".pdf"] { min-height: 60vh; } + +/* Knowledge checks: card-style radio options (single-choice Select fields on LMS tests). */ +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio { + counter-reset: lms-mcq-option; + display: flex; + flex-direction: column; + gap: 0.625rem; + width: 100%; +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label { + counter-increment: lms-mcq-option; + position: relative; + display: flex; + align-items: stretch; + gap: 0.75rem; + width: 100%; + margin: 0; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + border: 1px solid rgb(229 231 235); + background-color: rgb(249 250 251); + cursor: pointer; + transition: + background-color 0.15s ease, + border-color 0.15s ease, + box-shadow 0.15s ease; +} + +.dark .lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label { + border-color: rgb(55 65 81); + background-color: rgb(31 41 55 / 0.45); +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label:has(.fi-radio-input:checked) { + border-color: rgb(251 146 60); + background-color: rgb(255 247 237); + box-shadow: 0 0 0 1px rgb(251 146 60 / 0.35); +} + +.dark .lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label:has(.fi-radio-input:checked) { + border-color: rgb(251 146 60); + background-color: rgb(124 45 18 / 0.28); +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label:hover { + border-color: rgb(209 213 219); +} + +.dark .lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label:hover { + border-color: rgb(75 85 99); +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label-text { + display: flex; + align-items: center; + flex: 1; + gap: 0.75rem; + min-width: 0; +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label-text::before { + content: counter(lms-mcq-option, upper-alpha); + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 700; + background-color: rgb(209 213 219); + color: rgb(255 255 255); +} + +.dark .lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label-text::before { + background-color: rgb(75 85 99); +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label:has(.fi-radio-input:checked) .fi-fo-radio-label-text::before { + background-color: rgb(249 115 22); + color: rgb(255 255 255); +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-radio-input { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label-text p { + margin: 0; + font-size: 0.9375rem; + line-height: 1.45; +} diff --git a/resources/css/plugin.css b/resources/css/plugin.css index 56a52ab..d91d916 100644 --- a/resources/css/plugin.css +++ b/resources/css/plugin.css @@ -38,3 +38,105 @@ .step-material-container iframe[src*=".pdf"] { min-height: 60vh; } + +/* Knowledge checks: card-style radio options (single-choice Select fields on LMS tests). */ +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio { + counter-reset: lms-mcq-option; + display: flex; + flex-direction: column; + gap: 0.625rem; + width: 100%; +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label { + counter-increment: lms-mcq-option; + position: relative; + display: flex; + align-items: stretch; + gap: 0.75rem; + width: 100%; + margin: 0; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + border: 1px solid rgb(229 231 235); + background-color: rgb(249 250 251); + cursor: pointer; + transition: + background-color 0.15s ease, + border-color 0.15s ease, + box-shadow 0.15s ease; +} + +.dark .lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label { + border-color: rgb(55 65 81); + background-color: rgb(31 41 55 / 0.45); +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label:has(.fi-radio-input:checked) { + border-color: rgb(251 146 60); + background-color: rgb(255 247 237); + box-shadow: 0 0 0 1px rgb(251 146 60 / 0.35); +} + +.dark .lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label:has(.fi-radio-input:checked) { + border-color: rgb(251 146 60); + background-color: rgb(124 45 18 / 0.28); +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label:hover { + border-color: rgb(209 213 219); +} + +.dark .lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label:hover { + border-color: rgb(75 85 99); +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label-text { + display: flex; + align-items: center; + flex: 1; + gap: 0.75rem; + min-width: 0; +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label-text::before { + content: counter(lms-mcq-option, upper-alpha); + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 700; + background-color: rgb(209 213 219); + color: rgb(255 255 255); +} + +.dark .lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label-text::before { + background-color: rgb(75 85 99); +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label:has(.fi-radio-input:checked) .fi-fo-radio-label-text::before { + background-color: rgb(249 115 22); + color: rgb(255 255 255); +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-radio-input { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio-label-text p { + margin: 0; + font-size: 0.9375rem; + line-height: 1.45; +} diff --git a/resources/views/filament/pages/create-test-entry.blade.php b/resources/views/filament/pages/create-test-entry.blade.php index 86dd868..975cc88 100644 --- a/resources/views/filament/pages/create-test-entry.blade.php +++ b/resources/views/filament/pages/create-test-entry.blade.php @@ -17,6 +17,6 @@ @endif @else - @livewire('tapp.filament-form-builder.livewire.filament-form.show', ['form' => $test->form, 'blockRedirect' => true]) + @livewire('lms-test-form-show', ['form' => $test->form, 'blockRedirect' => true]) @endif \ No newline at end of file diff --git a/resources/views/livewire/lms-test-form-show.blade.php b/resources/views/livewire/lms-test-form-show.blade.php new file mode 100644 index 0000000..d782934 --- /dev/null +++ b/resources/views/livewire/lms-test-form-show.blade.php @@ -0,0 +1,23 @@ +
+
+

+ {{ $this->filamentForm->name }} +

+ @if ($this->filamentForm->description) +
+ {{-- Description is admin-controlled rich text (HTML) --}} + {!! $this->filamentForm->description !!} +
+ @endif +
+ @csrf + {{ $this->form }} + + + Submit + +
+ + +
+
diff --git a/src/Jobs/ImportCommonCartridgeJob.php b/src/Jobs/ImportCommonCartridgeJob.php new file mode 100644 index 0000000..93bfbfb --- /dev/null +++ b/src/Jobs/ImportCommonCartridgeJob.php @@ -0,0 +1,162 @@ +storedPath)) { + throw new RuntimeException('Stored import file not found: '.$this->storedPath); + } + + $extractPath = $this->extractZip($this->storedPath); + + $root = mb_rtrim($extractPath, '/'); + Log::channel('single')->info('CC import: package root', [ + 'context' => 'cc-import', + 'package_root' => $root, + 'imsmanifest_exists' => is_file($root.'/imsmanifest.xml'), + 'frame_xml_exists' => is_file($root.'/story_content/frame.xml'), + 'sample_slide_js_exists' => is_file($root.'/html5/data/js/6FA6ZHMtWms.js'), + ]); + + $result = $importService->import($extractPath, $this->tenantId); + + $user = $this->getUser(); + $user?->notify(new CommonCartridgeImportCompletedNotification( + success: true, + message: "Course \"{$result['course']->name}\" imported: {$result['lessons_created']} lesson(s), {$result['steps_created']} step(s).", + courseName: $result['course']->name, + )); + + $this->deleteStoredFileIfConfigured(); + } catch (Throwable $e) { + Log::error('Common Cartridge import failed', [ + 'path' => $this->storedPath, + 'user_id' => $this->userId, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + $user = $this->getUser(); + $user?->notify(new CommonCartridgeImportCompletedNotification( + success: false, + message: 'Import failed: '.$e->getMessage(), + courseName: null, + )); + + throw $e; + } finally { + if ($extractPath !== null && is_dir($extractPath)) { + $this->deleteDirectory($extractPath); + } + } + } + + private function extractZip(string $zipPath): string + { + $extractPath = storage_path('app/temp/cc-import-'.Str::uuid()->toString()); + if (! is_dir(dirname($extractPath))) { + mkdir(dirname($extractPath), 0755, true); + } + + $zip = new ZipArchive; + if ($zip->open($zipPath, ZipArchive::RDONLY) !== true) { + throw new RuntimeException('Could not open ZIP file.'); + } + $zip->extractTo($extractPath); + $zip->close(); + + return $this->normalizePackageRoot($extractPath); + } + + /** + * When the zip has a single root directory (e.g. "COS_SCORM12") that contains imsmanifest.xml, + * use that subdirectory as the package root so the parser finds frame.xml and html5/data/js. + */ + private function normalizePackageRoot(string $extractPath): string + { + $manifestPath = mb_rtrim($extractPath, '/').'/imsmanifest.xml'; + if (is_file($manifestPath)) { + return mb_rtrim($extractPath, '/'); + } + $entries = @scandir($extractPath); + if ($entries === false) { + return mb_rtrim($extractPath, '/'); + } + $dirs = array_filter($entries, function ($name) use ($extractPath) { + return $name !== '.' && $name !== '..' && is_dir(mb_rtrim($extractPath, '/').'/'.$name); + }); + if (count($dirs) === 1) { + $subDir = mb_rtrim($extractPath, '/').'/'.reset($dirs); + $nestedManifest = $subDir.'/imsmanifest.xml'; + if (is_file($nestedManifest)) { + return $subDir; + } + } + + return mb_rtrim($extractPath, '/'); + } + + private function deleteDirectory(string $path): void + { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($files as $fileinfo) { + if ($fileinfo->isDir()) { + rmdir($fileinfo->getRealPath()); + } else { + unlink($fileinfo->getRealPath()); + } + } + rmdir($path); + } + + private function deleteStoredFileIfConfigured(): void + { + if (config('filament-lms.common_cartridge_import.delete_after_success', true) && is_file($this->storedPath)) { + @unlink($this->storedPath); + } + } + + private function getUser(): ?object + { + $userModel = config('filament-lms.user_model'); + + return $userModel ? $userModel::find($this->userId) : null; + } +} diff --git a/src/Livewire/LmsTestFormShow.php b/src/Livewire/LmsTestFormShow.php new file mode 100644 index 0000000..2728eb3 --- /dev/null +++ b/src/Livewire/LmsTestFormShow.php @@ -0,0 +1,104 @@ +filamentForm->filamentFormFields as $fieldData) { + $componentClass = $fieldData->type === FilamentFieldTypeEnum::SELECT + ? Radio::class + : $fieldData->type->className(); + + $filamentField = $componentClass::make((string) $fieldData->getKey()); + + $filamentField = $this->parseField($filamentField, $fieldData->toArray()); + + if ($fieldData->type === FilamentFieldTypeEnum::SELECT_MULTIPLE) { + $filamentField = $filamentField + ->multiple() + ->live() + ->required() + ->default([]); + } elseif ($fieldData->type === FilamentFieldTypeEnum::CHECKBOX) { + $filamentField = $filamentField + ->default(false); + } elseif ($fieldData->type === FilamentFieldTypeEnum::CHECKBOX_LIST) { + $filamentField = $filamentField + ->default([]); + } elseif ($fieldData->type === FilamentFieldTypeEnum::REPEATER) { + $filamentField = $filamentField + ->schema(function () use ($fieldData) { + $repeaterSchema = []; + foreach ($fieldData->schema ?? [] as $index => $subField) { + $subFieldId = $subField['id'] ?? $fieldData->id.'_'.$subField['type'].'_'.$index; + $subFieldComponent = FilamentFieldTypeEnum::fromString($subField['type'])->className()::make($subFieldId); + + if (isset($subField['label'])) { + $subFieldComponent = $subFieldComponent->label(new HtmlString($subField['label'])); + } + + if (isset($subField['required']) && $subField['required']) { + $subFieldComponent = $subFieldComponent->required(); + } + + if (isset($subField['options'])) { + $subFieldComponent = $subFieldComponent->options(array_combine($subField['options'], $subField['options'])); + } + + if (isset($subField['hint'])) { + $subFieldComponent = $subFieldComponent->hint($subField['hint']); + } + + if (isset($subField['rules'])) { + $subFieldComponent = $subFieldComponent->rules($subField['rules']); + } + + $repeaterSchema[] = $subFieldComponent; + } + + return $repeaterSchema; + }) + ->default([]) + ->live(); + } + + if ($fieldData->type === FilamentFieldTypeEnum::RICH_EDITOR) { + $filamentField = $filamentField->disableToolbarButtons(['attachFiles']); + } + + if ($fieldData->type === FilamentFieldTypeEnum::SELECT) { + $filamentField = $filamentField + ->inline(false) + ->extraFieldWrapperAttributes([ + 'class' => 'lms-knowledge-check-radio-field', + ]); + } + + $schema[] = $filamentField; + } + + return $schema; + } + + public function render() + { + return view('filament-lms::livewire.lms-test-form-show'); + } +} diff --git a/src/Notifications/CommonCartridgeImportCompletedNotification.php b/src/Notifications/CommonCartridgeImportCompletedNotification.php new file mode 100644 index 0000000..3c04d03 --- /dev/null +++ b/src/Notifications/CommonCartridgeImportCompletedNotification.php @@ -0,0 +1,40 @@ + + */ + public function via(object $notifiable): array + { + return ['database']; + } + + /** + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + 'success' => $this->success, + 'message' => $this->message, + 'course_name' => $this->courseName, + ]; + } +} diff --git a/src/Services/CommonCartridge/ArticulateFrameParser.php b/src/Services/CommonCartridge/ArticulateFrameParser.php new file mode 100644 index 0000000..47f7801 --- /dev/null +++ b/src/Services/CommonCartridge/ArticulateFrameParser.php @@ -0,0 +1,132 @@ + outline > links > slidelink (expand="true" = lesson) + * > links > slidelink (expand="false" = step) + */ +final class ArticulateFrameParser +{ + /** + * Parse frame.xml if present. Returns list of lessons with steps, or null if not found/invalid. + * + * @return list|null + */ + public function parse(string $extractedPath): ?array + { + $path = mb_rtrim($extractedPath, '/').'/story_content/frame.xml'; + if (! is_file($path)) { + return null; + } + + $xml = @simplexml_load_file($path); + if ($xml === false) { + return null; + } + + $navData = $xml->nav_data ?? null; + if ($navData === null) { + return null; + } + $outline = $navData->outline ?? null; + if ($outline === null) { + return null; + } + $links = $outline->links ?? null; + if ($links === null) { + return null; + } + + $lessons = []; + $lessonOrder = 0; + foreach ($links->slidelink as $slidelink) { + $expand = (string) ($slidelink['expand'] ?? ''); + $displayText = $this->decodeDisplayText((string) ($slidelink['displaytext'] ?? '')); + + if (mb_strtolower($expand) === 'true') { + $steps = []; + $stepOrder = 0; + $childLinks = $slidelink->links ?? null; + if ($childLinks !== null) { + foreach ($childLinks->slidelink as $child) { + $slideIdRaw = (string) ($child['slideid'] ?? ''); + $slideId = $slideIdRaw !== '' ? $this->extractSlideId($slideIdRaw) : null; + $steps[] = new StepStructure( + title: $this->decodeDisplayText((string) ($child['displaytext'] ?? '')), + resourceIdentifier: null, + order: $stepOrder++, + slideId: $slideId, + ); + } + } + $lessons[] = new LessonStructure( + title: $displayText !== '' ? $displayText : 'Lesson '.($lessonOrder + 1), + steps: $steps, + order: $lessonOrder++, + ); + } + } + + return $lessons; + } + + /** + * Parse frame.xml resource_data if present. Returns list of url/title entries (links and documents). + * + * @return list + */ + public function parseResourceData(string $extractedPath): array + { + $path = mb_rtrim($extractedPath, '/').'/story_content/frame.xml'; + if (! is_file($path)) { + return []; + } + + $xml = @simplexml_load_file($path); + if ($xml === false) { + return []; + } + + $resourceData = $xml->resource_data ?? null; + if ($resourceData === null) { + return []; + } + $resources = $resourceData->resources ?? null; + if ($resources === null) { + return []; + } + + $entries = []; + foreach ($resources->resource as $res) { + $url = mb_trim((string) ($res['url'] ?? '')); + $title = $this->decodeDisplayText((string) ($res['title'] ?? '')); + if ($url !== '') { + $entries[] = new FrameResourceEntry(url: $url, title: $title !== '' ? $title : $url); + } + } + + return $entries; + } + + private function decodeDisplayText(string $text): string + { + $text = html_entity_decode($text, ENT_XML1 | ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + return mb_trim($text); + } + + /** Extract slide id (last segment) from slideid e.g. "_player.6Oszjf3XJTp.6TW1ASrmWOP" -> "6TW1ASrmWOP". */ + private function extractSlideId(string $slideIdAttr): ?string + { + $parts = explode('.', $slideIdAttr); + $last = end($parts); + + return $last !== '' ? $last : null; + } +} diff --git a/src/Services/CommonCartridge/ArticulateSlideContentExtractor.php b/src/Services/CommonCartridge/ArticulateSlideContentExtractor.php new file mode 100644 index 0000000..a78ab62 --- /dev/null +++ b/src/Services/CommonCartridge/ArticulateSlideContentExtractor.php @@ -0,0 +1,577 @@ +|null + */ + public function getSlideData(string $extractedPath, string $slideId): ?array + { + $path = mb_rtrim($extractedPath, '/').'/html5/data/js/'.basename($slideId).'.js'; + if (! is_file($path)) { + return null; + } + + if ($this->nodeExtractorScriptPath !== null && is_file($this->nodeExtractorScriptPath)) { + $data = $this->getSlideDataViaNode($path, $slideId); + if (is_array($data)) { + return $data; + } + } + + $content = @file_get_contents($path); + if ($content === false || $content === '') { + return null; + } + + $content = $this->stripUtf8Bom($content); + + $json = $this->extractJsonFromProvideData($content); + if ($json === null) { + Log::channel('single')->info('CC import: slide JSON extraction returned null', [ + 'context' => 'cc-import', + 'slide_id' => $slideId, + 'slide_js_path' => $path, + ]); + + return null; + } + + $data = $this->decodeSlideJson($json, $slideId, false); + if (is_array($data)) { + return $data; + } + + $json = $this->sanitizeJsonForDecode($json); + $data = $this->decodeSlideJson($json, $slideId, true); + + return is_array($data) ? $data : null; + } + + /** + * Run the Node extractor script and return decoded slide data, or null on failure. + * + * @return array|null + */ + private function getSlideDataViaNode(string $slideJsPath, string $slideId): ?array + { + $nodeBinary = $this->resolveNodeBinary(); + $env = $this->getNodeProcessEnv(); + $pipes = []; + $proc = @proc_open( + [$nodeBinary, $this->nodeExtractorScriptPath, $slideJsPath], + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], + $pipes, + null, + $env, + ); + if (! is_resource($proc)) { + Log::channel('single')->warning('CC import: Node extractor proc_open failed', [ + 'context' => 'cc-import', + 'slide_id' => $slideId, + 'slide_js_path' => $slideJsPath, + ]); + + return null; + } + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exitCode = proc_close($proc); + if ($exitCode !== 0) { + Log::channel('single')->warning('CC import: Node extractor exited non-zero', [ + 'context' => 'cc-import', + 'slide_id' => $slideId, + 'exit_code' => $exitCode, + 'stderr' => $stderr !== false && $stderr !== '' ? mb_substr($stderr, 0, 500) : null, + ]); + + return null; + } + if ($stdout === false || $stdout === '') { + Log::channel('single')->warning('CC import: Node extractor returned empty stdout', [ + 'context' => 'cc-import', + 'slide_id' => $slideId, + ]); + + return null; + } + $data = json_decode($stdout, true); + if (! is_array($data)) { + Log::channel('single')->warning('CC import: Node extractor stdout was not valid JSON', [ + 'context' => 'cc-import', + 'slide_id' => $slideId, + 'json_error' => json_last_error_msg(), + 'stdout_length' => strlen($stdout), + ]); + + return null; + } + + return $data; + } + + /** + * Resolve the node binary path so the subprocess can find it (web server often has minimal PATH). + */ + private function resolveNodeBinary(): string + { + if (function_exists('config')) { + $configured = config('filament-lms.node_binary'); + if (is_string($configured) && $configured !== '' && is_executable($configured)) { + return $configured; + } + } + $candidates = ['/opt/homebrew/bin/node', '/usr/local/bin/node']; + foreach ($candidates as $path) { + if (is_executable($path)) { + return $path; + } + } + + return 'node'; + } + + /** + * Environment for the node subprocess: current env with PATH extended so node is findable. + * + * @return array + */ + private function getNodeProcessEnv(): array + { + $env = getenv() ?: []; + $extraPaths = ['/opt/homebrew/bin', '/usr/local/bin']; + $currentPath = $env['PATH'] ?? ''; + $env['PATH'] = implode(':', array_merge($extraPaths, array_filter([$currentPath]))); + + return $env; + } + + /** + * Decode JSON with UTF-8 substitution and log on failure for investigation. + * + * @return array|null + */ + private function decodeSlideJson(string $json, string $slideId, bool $afterSanitize): ?array + { + $flags = JSON_INVALID_UTF8_SUBSTITUTE; + $data = json_decode($json, true, 512, $flags); + + if (is_array($data)) { + return $data; + } + + $error = json_last_error_msg(); + $excerptStart = mb_substr($json, 0, 200); + $excerptEnd = mb_strlen($json) > 250 ? mb_substr($json, -200) : ''; + + Log::channel('single')->warning('CC import: slide JSON decode failed', [ + 'context' => 'cc-import', + 'slide_id' => $slideId, + 'json_error' => $error, + 'json_error_code' => json_last_error(), + 'json_length' => strlen($json), + 'after_sanitize' => $afterSanitize, + 'excerpt_start' => $excerptStart, + 'excerpt_end' => $excerptEnd, + ]); + + return null; + } + + /** + * Extract HTML from a slide's JS file. Returns null if file missing or invalid. + */ + public function extract(string $extractedPath, string $slideId): ?string + { + $data = $this->getSlideData($extractedPath, $slideId); + if ($data === null) { + return null; + } + + return $this->extractFromSlideData($data); + } + + /** + * Build HTML from already-decoded slide data (e.g. from getSlideData). Use for assessment detection + text without double file read. + * + * @param array $data + */ + public function extractFromSlideData(array $data): string + { + return $this->buildHtmlFromSlideData($data); + } + + /** + * @param array $data + */ + public function getSlideTitle(array $data): ?string + { + $title = $data['title'] ?? null; + + return is_string($title) && $title !== '' ? $title : null; + } + + private function extractJsonFromProvideData(string $content): ?string + { + $jsonStr = $this->extractJsonWithDoubleQuotePattern($content); + if ($jsonStr !== null) { + return $jsonStr; + } + + return $this->extractJsonWithSingleQuotePattern($content); + } + + /** + * Try pattern: globalProvideData("slide", "{...}") + */ + private function extractJsonWithDoubleQuotePattern(string $content): ?string + { + $needle = '("slide", "'; + $start = strpos($content, $needle); + if ($start === false) { + return null; + } + $start += strlen($needle); + $len = strlen($content); + $end = $start; + for ($i = $start; $i < $len; $i++) { + if ($content[$i] === '"') { + $backslashes = 0; + $p = $i - 1; + while ($p >= $start && $content[$p] === '\\') { + $backslashes++; + $p--; + } + if ($backslashes % 2 === 0) { + $end = $i; + } + } + } + $jsonStr = substr($content, $start, $end - $start); + + return $jsonStr !== '' ? $jsonStr : null; + } + + /** + * Try pattern: globalProvideData('slide', '{...}') + */ + private function extractJsonWithSingleQuotePattern(string $content): ?string + { + $needle = "('slide', '"; + $start = strpos($content, $needle); + if ($start === false) { + return null; + } + $start += strlen($needle); + $len = strlen($content); + $end = $start; + for ($i = $start; $i < $len; $i++) { + if ($content[$i] === "'") { + $backslashes = 0; + $p = $i - 1; + while ($p >= $start && $content[$p] === '\\') { + $backslashes++; + $p--; + } + if ($backslashes % 2 === 0) { + $end = $i; + } + } + } + $jsonStr = substr($content, $start, $end - $start); + if ($jsonStr === '') { + return null; + } + $placeholder = "\x00"; + $jsonStr = str_replace("\\\\'", $placeholder, $jsonStr); + $jsonStr = str_replace("\\'", "'", $jsonStr); + $jsonStr = str_replace($placeholder, "\\\\'", $jsonStr); + + return $jsonStr; + } + + private function stripUtf8Bom(string $content): string + { + if (substr($content, 0, 3) === "\xEF\xBB\xBF") { + return substr($content, 3); + } + + return $content; + } + + /** + * Try to fix common issues that cause json_decode to fail. + */ + private function sanitizeJsonForDecode(string $json): string + { + $sanitized = mb_convert_encoding($json, 'UTF-8', 'UTF-8'); + if ($sanitized !== false) { + $json = $sanitized; + } + $json = preg_replace('/,\s*([}\]])/', '$1', $json) ?? $json; + $json = $this->escapeLiteralNewlinesInsideJsonStrings($json); + + return $json; + } + + /** + * Replace literal newline/carriage return inside JSON double-quoted strings with \n and \r + * so that json_decode can succeed (literal U+000A/U+000D inside strings are invalid in JSON). + */ + private function escapeLiteralNewlinesInsideJsonStrings(string $json): string + { + $len = strlen($json); + $result = ''; + $inString = false; + $i = 0; + while ($i < $len) { + $ch = $json[$i]; + if (! $inString) { + $result .= $ch; + if ($ch === '"') { + $inString = true; + } + $i++; + continue; + } + if ($ch === '\\' && $i + 1 < $len) { + $result .= $ch.$json[$i + 1]; + $i += 2; + continue; + } + if ($ch === '"') { + $result .= $ch; + $inString = false; + $i++; + continue; + } + if ($ch === "\n") { + $result .= '\\n'; + $i++; + continue; + } + if ($ch === "\r") { + $result .= '\\r'; + $i++; + continue; + } + $result .= $ch; + $i++; + } + + return $result; + } + + /** + * @param array $data + */ + private function buildHtmlFromSlideData(array $data): string + { + $parts = []; + $slideLayers = $data['slideLayers'] ?? []; + foreach ($slideLayers as $layer) { + $objects = $layer['objects'] ?? []; + foreach ($objects as $obj) { + $textLib = $obj['textLib'] ?? null; + if (! is_array($textLib)) { + continue; + } + foreach ($textLib as $textData) { + $vartext = $textData['vartext'] ?? null; + if (! is_array($vartext)) { + continue; + } + $blocks = $vartext['blocks'] ?? []; + $listItems = []; + foreach ($blocks as $block) { + $text = $this->getBlockText($block); + if ($text === '') { + continue; + } + $listStyle = $block['style']['listStyle'] ?? null; + $isBullet = is_array($listStyle) && ($listStyle['listType'] ?? '') === 'bullet'; + $escaped = nl2br(htmlspecialchars($text, ENT_QUOTES, 'UTF-8')); + if ($isBullet) { + $listItems[] = '
  • '.$escaped.'
  • '; + } else { + if ($listItems !== []) { + $parts[] = '
      '.implode('', $listItems).'
    '; + $listItems = []; + } + $parts[] = '

    '.$escaped.'

    '; + } + } + if ($listItems !== []) { + $parts[] = '
      '.implode('', $listItems).'
    '; + } + } + } + } + + return mb_trim(implode("\n", $parts)); + } + + /** + * @param array $block + */ + private function getBlockText(array $block): string + { + $spans = $block['spans'] ?? []; + $texts = []; + foreach ($spans as $span) { + $t = $span['text'] ?? ''; + if (is_string($t)) { + $texts[] = $t; + } + } + + return mb_trim(implode('', $texts)); + } + + /** + * For Assessment slides: return only intro/directions HTML (exclude question, UI labels, feedback). + * + * @param array $data + */ + public function extractAssessmentIntroFromSlideData(array $data): string + { + $blocks = $this->collectBlocksFromSlideData($data); + $uiBlacklist = [ + 'assessment', + 'session 1:', 'session 2:', 'session 3:', 'session 4:', 'session 5:', 'session 6:', 'session 7:', 'session 8:', + 'question 1 of 4:', 'question 2 of 4:', 'question 3 of 4:', 'question 4 of 4:', + 'question 1 of 3:', 'question 2 of 3:', 'question 3 of 3:', + 'submit', 'try again', 'incorrect', 'continue', 'correct', + "that is incorrect. please try again.", "you did not select the correct response.", + "that's right! you selected the correct response.", + ]; + $parts = []; + $listItems = []; + foreach ($blocks as [$text, $isBullet]) { + $normalized = mb_strtolower(mb_trim($text)); + if ($normalized === '') { + continue; + } + foreach ($uiBlacklist as $black) { + if ($normalized === $black || mb_strpos($normalized, $black) === 0) { + continue 2; + } + } + if (mb_substr(mb_trim($text), -1) === '?') { + break; + } + $escaped = nl2br(htmlspecialchars($text, ENT_QUOTES, 'UTF-8')); + if ($isBullet) { + $listItems[] = '
  • '.$escaped.'
  • '; + } else { + if ($listItems !== []) { + $parts[] = '
      '.implode('', $listItems).'
    '; + $listItems = []; + } + $parts[] = '

    '.$escaped.'

    '; + } + } + if ($listItems !== []) { + $parts[] = '
      '.implode('', $listItems).'
    '; + } + + return mb_trim(implode("\n", $parts)); + } + + /** + * For Assessment slides: return first question text and its option labels (for form name and RADIO field). + * + * @param array $data + * @return array{question: string, options: array}|null + */ + public function getAssessmentFirstQuestionAndOptions(array $data): ?array + { + $blocks = $this->collectBlocksFromSlideData($data); + $question = null; + $options = []; + $foundQuestion = false; + foreach ($blocks as [$text, $isBullet]) { + $t = mb_trim($text); + if ($t === '') { + continue; + } + if (! $foundQuestion) { + if (mb_substr($t, -1) === '?') { + $question = $t; + $foundQuestion = true; + } + continue; + } + if ($isBullet) { + $options[] = $t; + } else { + break; + } + } + if ($question === null || $question === '') { + return null; + } + + return ['question' => $question, 'options' => $options]; + } + + /** + * Collect all text blocks from slide data as [text, isBullet]. + * + * @param array $data + * @return array + */ + private function collectBlocksFromSlideData(array $data): array + { + $out = []; + $slideLayers = $data['slideLayers'] ?? []; + foreach ($slideLayers as $layer) { + $objects = $layer['objects'] ?? []; + foreach ($objects as $obj) { + $textLib = $obj['textLib'] ?? null; + if (! is_array($textLib)) { + continue; + } + foreach ($textLib as $textData) { + $vartext = $textData['vartext'] ?? null; + if (! is_array($vartext)) { + continue; + } + $blocks = $vartext['blocks'] ?? []; + foreach ($blocks as $block) { + $text = $this->getBlockText($block); + if ($text === '') { + continue; + } + $listStyle = $block['style']['listStyle'] ?? null; + $isBullet = is_array($listStyle) && ($listStyle['listType'] ?? '') === 'bullet'; + $out[] = [$text, $isBullet]; + } + } + } + } + + return $out; + } +} diff --git a/src/Services/CommonCartridge/CommonCartridgeImportService.php b/src/Services/CommonCartridge/CommonCartridgeImportService.php new file mode 100644 index 0000000..79e055e --- /dev/null +++ b/src/Services/CommonCartridge/CommonCartridgeImportService.php @@ -0,0 +1,389 @@ +info('CC import: start', [ + 'context' => 'cc-import', + 'extracted_path' => mb_rtrim($extractedPath, '/'), + ]); + + $manifest = $this->parser->parse($extractedPath); + $extractedPath = mb_rtrim($extractedPath, '/'); + + return DB::transaction(function () use ($manifest, $extractedPath, $tenantId) { + $course = $this->createCourse($manifest, $tenantId); + $lessonsCreated = 0; + $stepsCreated = 0; + $isFirstStepOfCourse = true; + $primaryResourceId = $this->getPrimaryWebContentResourceId($manifest->resources); + + foreach ($manifest->lessons as $lessonStructure) { + $lesson = $this->createLesson($course, $lessonStructure); + $lessonsCreated++; + + foreach ($lessonStructure->steps as $stepStructure) { + $this->createStep( + $course, + $lesson, + $stepStructure, + $manifest->resources, + $extractedPath, + $isFirstStepOfCourse ? $primaryResourceId : null, + ); + $isFirstStepOfCourse = false; + $stepsCreated++; + } + + $stepsCreated += $this->createResourcesStepIfNeeded($course, $lesson, $lessonStructure, $manifest->frameResources); + } + + return [ + 'course' => $course, + 'lessons_created' => $lessonsCreated, + 'steps_created' => $stepsCreated, + ]; + }); + } + + private function createCourse(ParsedManifest $manifest, int|string|null $tenantId): Course + { + $slug = $this->uniqueCourseSlug(Str::slug($manifest->courseTitle), $tenantId); + $externalId = $this->uniqueCourseColumn(Str::slug($manifest->courseTitle, '_'), 'external_id', $tenantId); + + $awards = config('filament-lms.awards', ['default' => 'Default']); + $defaultAward = array_key_first($awards); + + $data = [ + 'name' => $manifest->courseTitle, + 'slug' => $slug, + 'external_id' => $externalId, + 'description' => $manifest->courseDescription, + 'is_private' => false, + 'award' => $defaultAward, + ]; + + if (config('filament-lms.tenancy.enabled') && $tenantId !== null) { + $data[TenantHelper::getTenantColumnName()] = $tenantId; + } + + return Course::query()->create($data); + } + + private function createLesson(Course $course, LessonStructure $structure): Lesson + { + $slug = $this->uniqueSlugForLesson($course, Str::slug($structure->title)); + + return Lesson::query()->create([ + 'course_id' => $course->id, + 'order' => $structure->order, + 'name' => $structure->title, + 'slug' => $slug, + ]); + } + + /** + * When using Articulate frame.xml, steps have no resourceIdentifier; we attach the main SCO to the first step. + * + * @param array $resources + */ + private function createStep( + Course $course, + Lesson $lesson, + StepStructure $structure, + array $resources, + string $extractedPath, + ?string $primaryResourceIdForFirstStep = null, + ): void { + $slug = $this->uniqueSlugForStep($lesson, Str::slug($structure->title)); + $materialId = null; + $materialType = null; + $text = null; + + $nodeScriptPath = function_exists('base_path') + ? base_path('scripts/extract-articulate-slide-data.cjs') + : null; + $nodeScriptAvailable = $nodeScriptPath !== null && is_file($nodeScriptPath); + if ($structure->slideId !== null && $structure->order === 0) { + Log::channel('single')->info('CC import: node extractor config', [ + 'context' => 'cc-import', + 'node_script_path' => $nodeScriptPath, + 'node_script_exists' => $nodeScriptPath !== null ? is_file($nodeScriptPath) : false, + 'node_script_will_use' => $nodeScriptAvailable, + ]); + } + $extractor = new ArticulateSlideContentExtractor($nodeScriptAvailable ? $nodeScriptPath : null); + $slideData = $structure->slideId !== null ? $extractor->getSlideData($extractedPath, $structure->slideId) : null; + $isAssessment = $slideData !== null && $extractor->getSlideTitle($slideData) === 'Assessment'; + + $slideJsPath = $structure->slideId !== null + ? mb_rtrim($extractedPath, '/').'/html5/data/js/'.basename($structure->slideId).'.js' + : null; + if ($structure->slideId !== null) { + Log::channel('single')->info('CC import: step slide', [ + 'context' => 'cc-import', + 'step_title' => $structure->title, + 'slide_id' => $structure->slideId, + 'slide_js_path' => $slideJsPath, + 'slide_js_exists' => $slideJsPath !== null && is_file($slideJsPath), + 'slide_data_loaded' => $slideData !== null, + 'slide_title' => $slideData !== null ? $extractor->getSlideTitle($slideData) : null, + 'is_assessment' => $isAssessment, + ]); + } + + $resourceId = $structure->resourceIdentifier ?? $primaryResourceIdForFirstStep; + if (! $isAssessment && $resourceId !== null && isset($resources[$resourceId])) { + $resource = $resources[$resourceId]; + $primaryPath = $extractedPath.'/'.$resource->href; + + // Single entry-point HTML (e.g. index_lms.html in Articulate/SCORM) is stored as a document. + // When shown in an iframe, relative paths inside that HTML (e.g. story_content/, html5/) resolve + // against the media URL, so assets may 404 and the content can appear blank. Full SCO playback + // would require serving the extracted package from a path that preserves the directory structure. + if (in_array(mb_strtolower(mb_trim($resource->type)), ['webcontent', 'associatedcontent'], true) + && $resource->href !== '' + && is_file($primaryPath)) { + $docData = [ + 'name' => $structure->title !== '' ? $structure->title : basename($resource->href), + ]; + if (config('filament-lms.tenancy.enabled') && $course->getAttribute(TenantHelper::getTenantColumnName())) { + $docData[TenantHelper::getTenantColumnName()] = $course->getAttribute(TenantHelper::getTenantColumnName()); + } + $document = Document::query()->create($docData); + $document->addMedia($primaryPath) + ->toMediaCollection('default'); + $materialId = $document->id; + $materialType = 'document'; + } else { + $text = "Imported resource: {$resource->type} (".basename($resource->href).'). Configure material in admin if needed.'; + } + } + + if ($slideData !== null) { + if ($isAssessment) { + $intro = $extractor->extractAssessmentIntroFromSlideData($slideData); + $text = $intro !== '' ? $intro : null; + } else { + $extracted = $extractor->extractFromSlideData($slideData); + if ($extracted !== '') { + $text = $extracted; + } + } + } + // Always persist slide-extracted text when present (text field is the source for step content). + + $firstQuestionAndOptions = $isAssessment && $slideData !== null + ? $extractor->getAssessmentFirstQuestionAndOptions($slideData) + : null; + $formName = $firstQuestionAndOptions !== null && $firstQuestionAndOptions['question'] !== '' + ? $firstQuestionAndOptions['question'] + : ($structure->title !== '' ? $structure->title : 'Assessment'); + + if ($structure->slideId !== null) { + Log::channel('single')->info('CC import: step result', [ + 'context' => 'cc-import', + 'step_title' => $structure->title, + 'text_length' => $text !== null ? strlen($text) : 0, + ]); + } + + if ($isAssessment && class_exists(\Tapp\FilamentFormBuilder\Models\FilamentForm::class)) { + $formData = ['name' => $formName]; + $formClass = \Tapp\FilamentFormBuilder\Models\FilamentForm::class; + if (config('filament-form-builder.tenancy.enabled', false)) { + $formTenantColumn = $formClass::getTenantColumnName(); + $courseTenantValue = $course->getAttribute(TenantHelper::getTenantColumnName()); + if ($courseTenantValue !== null) { + $formData[$formTenantColumn] = $courseTenantValue; + } + } + $form = $formClass::query()->create($formData); + + $fieldClass = \Tapp\FilamentFormBuilder\Models\FilamentFormField::class; + if (class_exists($fieldClass) && $firstQuestionAndOptions !== null && $firstQuestionAndOptions['options'] !== []) { + $options = []; + foreach ($firstQuestionAndOptions['options'] as $label) { + $options[] = ['value' => $label, 'label' => $label]; + } + $fieldClass::query()->create([ + 'filament_form_id' => $form->id, + 'order' => 0, + 'type' => 'Select One', + 'label' => $firstQuestionAndOptions['question'], + 'required' => true, + 'options' => $options, + ]); + } + + $testData = [ + 'name' => $formName, + 'filament_form_id' => $form->id, + ]; + if (config('filament-lms.tenancy.enabled') && $course->getAttribute(TenantHelper::getTenantColumnName())) { + $testData[TenantHelper::getTenantColumnName()] = $course->getAttribute(TenantHelper::getTenantColumnName()); + } + $test = Test::query()->create($testData); + $materialId = $test->id; + $materialType = 'test'; + Log::channel('single')->info('CC import: created test material for assessment', [ + 'context' => 'cc-import', + 'step_title' => $structure->title, + 'test_id' => $test->id, + 'form_id' => $form->id, + ]); + } + + Step::query()->create([ + 'lesson_id' => $lesson->id, + 'order' => $structure->order, + 'name' => $structure->title !== '' ? $structure->title : 'Step '.($structure->order + 1), + 'slug' => $slug, + 'material_id' => $materialId, + 'material_type' => $materialType, + 'text' => $text, + 'retry_step_id' => null, + 'require_perfect_score' => false, + ]); + } + + private function uniqueCourseSlug(string $base, int|string|null $tenantId): string + { + return $this->uniqueCourseColumn($base, 'slug', $tenantId); + } + + private function uniqueCourseColumn(string $base, string $column, int|string|null $tenantId): string + { + $value = $base; + $i = 0; + $query = Course::query(); + if (config('filament-lms.tenancy.enabled') && $tenantId !== null) { + $query->where(TenantHelper::getTenantColumnName(), $tenantId); + } + while ($query->clone()->where($column, $value)->exists()) { + $i++; + $value = $base.'-'.$i; + } + + return $value; + } + + /** + * Returns the first webcontent resource identifier (main SCO entry, e.g. index_lms.html) or null. + * + * @param array $resources + */ + private function getPrimaryWebContentResourceId(array $resources): ?string + { + foreach ($resources as $identifier => $resource) { + if (in_array(mb_strtolower(mb_trim($resource->type)), ['webcontent', 'associatedcontent'], true) + && $resource->href !== '') { + return $identifier; + } + } + + return null; + } + + private function uniqueSlugForLesson(Course $course, string $base): string + { + $slug = $base; + $i = 0; + while ($course->lessons()->where('slug', $slug)->exists()) { + $i++; + $slug = $base.'-'.$i; + } + + return $slug; + } + + private function uniqueSlugForStep(Lesson $lesson, string $base): string + { + $slug = $base; + $i = 0; + while ($lesson->steps()->where('slug', $slug)->exists()) { + $i++; + $slug = $base.'-'.$i; + } + + return $slug; + } + + /** + * Create a "Resources" step for the lesson when frame resources match the lesson title (e.g. "Session 1"). + * + * @param list $frameResources + * @return int Number of steps created (0 or 1) + */ + private function createResourcesStepIfNeeded( + Course $course, + Lesson $lesson, + LessonStructure $lessonStructure, + array $frameResources, + ): int { + if ($frameResources === []) { + return 0; + } + + $lessonTitle = $lessonStructure->title; + $matches = []; + foreach ($frameResources as $entry) { + $t = $entry->title; + if (str_starts_with($t, $lessonTitle.':') || str_starts_with($t, $lessonTitle.' ') || $t === $lessonTitle) { + $matches[] = $entry; + } + } + if ($matches === []) { + return 0; + } + + // Build HTML list only; do not create Link models so we avoid dispatching screenshot jobs + // (Browsershot often fails for Google Drive / external URLs and would spam the log). + $items = []; + foreach ($matches as $entry) { + $items[] = '
  • '.htmlspecialchars($entry->title, ENT_QUOTES, 'UTF-8').'
  • '; + } + $resourcesHtml = '
      '.implode('', $items).'
    '; + $slug = $this->uniqueSlugForStep($lesson, 'resources'); + $order = $lesson->steps()->max('order') + 1; + + Step::query()->create([ + 'lesson_id' => $lesson->id, + 'order' => $order, + 'name' => 'Resources', + 'slug' => $slug, + 'material_id' => null, + 'material_type' => null, + 'text' => $resourcesHtml, + 'retry_step_id' => null, + 'require_perfect_score' => false, + ]); + + return 1; + } +} diff --git a/src/Services/CommonCartridge/FrameResourceEntry.php b/src/Services/CommonCartridge/FrameResourceEntry.php new file mode 100644 index 0000000..e52444a --- /dev/null +++ b/src/Services/CommonCartridge/FrameResourceEntry.php @@ -0,0 +1,16 @@ + $steps + */ +final class LessonStructure +{ + /** + * @param list $steps + */ + public function __construct( + public string $title, + public array $steps, + public int $order = 0, + ) {} +} diff --git a/src/Services/CommonCartridge/ManifestParser.php b/src/Services/CommonCartridge/ManifestParser.php new file mode 100644 index 0000000..29d45a8 --- /dev/null +++ b/src/Services/CommonCartridge/ManifestParser.php @@ -0,0 +1,301 @@ +registerXPathNamespace('imscp', self::NS_IMSCP); + $xml->registerXPathNamespace('imsmd', self::NS_IMSMD); + $xml->registerXPathNamespace('adlcp', self::NS_ADLCP); + + $title = $this->extractCourseTitle($xml); + $description = $this->extractCourseDescription($xml); + $resources = $this->extractResources($xml); + $lessons = $this->extractLessonsAndSteps($xml, $resources); + + $frameParser = new ArticulateFrameParser; + $articulateLessons = $frameParser->parse($extractedPath); + $frameResources = []; + if ($articulateLessons !== null && $articulateLessons !== []) { + $lessons = $articulateLessons; + $frameResources = $frameParser->parseResourceData($extractedPath); + $totalSteps = array_sum(array_map(fn ($l) => count($l->steps), $lessons)); + \Illuminate\Support\Facades\Log::channel('single')->info('CC import: using Articulate frame.xml', [ + 'context' => 'cc-import', + 'lessons_count' => count($lessons), + 'steps_count' => $totalSteps, + 'frame_resources_count' => count($frameResources), + 'sample_steps' => array_slice(array_merge(...array_map(fn ($l) => array_map(fn ($s) => ['title' => $s->title, 'slideId' => $s->slideId], $l->steps), $lessons)), 0, 5), + ]); + } else { + \Illuminate\Support\Facades\Log::channel('single')->info('CC import: Articulate frame.xml not used (missing or empty)', [ + 'context' => 'cc-import', + 'frame_path' => mb_rtrim($extractedPath, '/').'/story_content/frame.xml', + 'frame_exists' => is_file(mb_rtrim($extractedPath, '/').'/story_content/frame.xml'), + ]); + } + + if ($lessons === []) { + throw new InvalidArgumentException('Manifest has no organization or items.'); + } + + return new ParsedManifest( + courseTitle: $title, + courseDescription: $description, + resources: $resources, + lessons: $lessons, + frameResources: $frameResources, + ); + } + + private function extractCourseTitle(SimpleXMLElement $xml): string + { + $metadata = $xml->metadata ?? null; + if ($metadata === null) { + return 'Imported Course'; + } + + $imsmdChildren = $metadata->children(self::NS_IMSMD); + $lom = isset($imsmdChildren[0]) ? $imsmdChildren[0] : null; + if ($lom === null) { + $org = $this->getDefaultOrganization($xml); + if ($org !== null) { + $titleEl = $org->title ?? null; + if ($titleEl !== null && (string) $titleEl !== '') { + return mb_trim((string) $titleEl); + } + } + + return 'Imported Course'; + } + + $general = $lom->general ?? null; + if ($general === null) { + return 'Imported Course'; + } + $title = $general->title ?? null; + if ($title === null) { + return 'Imported Course'; + } + $langstring = $title->langstring ?? $title->children(self::NS_IMSMD)->langstring ?? null; + if ($langstring !== null && (string) $langstring !== '') { + return mb_trim((string) $langstring); + } + + $org = $this->getDefaultOrganization($xml); + if ($org !== null) { + $t = $org->title ?? null; + if ($t !== null && (string) $t !== '') { + return mb_trim((string) $t); + } + } + + return 'Imported Course'; + } + + private function extractCourseDescription(SimpleXMLElement $xml): ?string + { + $metadata = $xml->metadata ?? null; + if ($metadata === null) { + return null; + } + $imsmdChildren = $metadata->children(self::NS_IMSMD); + $lom = isset($imsmdChildren[0]) ? $imsmdChildren[0] : null; + if ($lom === null) { + return null; + } + $general = $lom->general ?? null; + if ($general === null) { + return null; + } + $description = $general->description ?? $general->children(self::NS_IMSMD)->description ?? null; + if ($description === null) { + return null; + } + $langstring = $description->langstring ?? $description->children(self::NS_IMSMD)->langstring ?? null; + if ($langstring === null) { + return null; + } + $text = mb_trim((string) $langstring); + + return $text === '' ? null : $text; + } + + /** + * @return array + */ + private function extractResources(SimpleXMLElement $xml): array + { + $resourcesEl = $xml->resources ?? null; + if ($resourcesEl === null) { + return []; + } + + $out = []; + foreach ($resourcesEl->resource as $res) { + $identifier = (string) ($res['identifier'] ?? ''); + $type = (string) ($res['type'] ?? 'webcontent'); + $href = (string) ($res['href'] ?? ''); + $adlcp = $res->attributes(self::NS_ADLCP); + $scormType = $adlcp !== null && isset($adlcp['scormtype']) ? (string) $adlcp['scormtype'] : null; + + $fileHrefs = []; + foreach ($res->file as $file) { + $h = (string) ($file['href'] ?? ''); + if ($h !== '') { + $fileHrefs[] = $h; + } + } + + if ($identifier !== '') { + $out[$identifier] = new ResourceData( + identifier: $identifier, + type: $type, + href: $href, + fileHrefs: $fileHrefs, + scormType: $scormType, + ); + } + } + + return $out; + } + + /** + * @param array $resources + * @return list + */ + private function extractLessonsAndSteps(SimpleXMLElement $xml, array $resources): array + { + $org = $this->getDefaultOrganization($xml); + if ($org === null) { + return []; + } + + $items = $org->item ?? []; + $itemCount = is_countable($items) ? count($items) : 0; + + if ($itemCount === 0) { + return []; + } + + $lessons = []; + $orderLesson = 0; + + foreach ($items as $item) { + $itemIdentifierRef = (string) ($item['identifierref'] ?? ''); + $itemTitle = $this->getItemTitle($item); + $childItems = $item->item ?? []; + $childCount = is_countable($childItems) ? count($childItems) : 0; + + if ($itemCount === 1 && $childCount === 0 && $itemIdentifierRef !== '') { + $stepTitle = $itemTitle !== '' ? $itemTitle : 'Content'; + $step = new StepStructure( + title: $stepTitle, + resourceIdentifier: $itemIdentifierRef, + order: 0, + ); + $lessons[] = new LessonStructure( + title: 'Content', + steps: [$step], + order: $orderLesson++, + ); + break; + } + + $steps = []; + $orderStep = 0; + if ($childCount > 0) { + foreach ($childItems as $child) { + $ref = (string) ($child['identifierref'] ?? ''); + $steps[] = new StepStructure( + title: $this->getItemTitle($child), + resourceIdentifier: $ref !== '' ? $ref : null, + order: $orderStep++, + ); + } + } else { + $steps[] = new StepStructure( + title: $itemTitle !== '' ? $itemTitle : 'Step', + resourceIdentifier: $itemIdentifierRef !== '' ? $itemIdentifierRef : null, + order: 0, + ); + } + + $lessons[] = new LessonStructure( + title: $itemTitle !== '' ? $itemTitle : 'Lesson '.($orderLesson + 1), + steps: $steps, + order: $orderLesson++, + ); + } + + return $lessons; + } + + private function getDefaultOrganization(SimpleXMLElement $xml): ?SimpleXMLElement + { + $orgs = $xml->organizations ?? null; + if ($orgs === null) { + return null; + } + $defaultId = (string) ($orgs['default'] ?? ''); + foreach ($orgs->organization as $org) { + $id = (string) ($org['identifier'] ?? ''); + if ($defaultId !== '' && $id === $defaultId) { + return $org; + } + } + $first = $orgs->organization[0] ?? null; + + return $first !== null ? $first : null; + } + + private function getItemTitle(SimpleXMLElement $item): string + { + $title = $item->title ?? null; + if ($title !== null && (string) $title !== '') { + return mb_trim((string) $title); + } + + return ''; + } + + private function courseTitleFromOrg(SimpleXMLElement $xml): string + { + $org = $this->getDefaultOrganization($xml); + if ($org !== null) { + $t = $org->title ?? null; + if ($t !== null && (string) $t !== '') { + return mb_trim((string) $t); + } + } + + return 'Imported Course'; + } +} diff --git a/src/Services/CommonCartridge/ParsedManifest.php b/src/Services/CommonCartridge/ParsedManifest.php new file mode 100644 index 0000000..72e9c6f --- /dev/null +++ b/src/Services/CommonCartridge/ParsedManifest.php @@ -0,0 +1,28 @@ + $resources identifier => ResourceData + * @param list $lessons + * @param list $frameResources Links/documents from Articulate frame.xml resource_data + */ +final class ParsedManifest +{ + /** + * @param array $resources + * @param list $lessons + * @param list $frameResources + */ + public function __construct( + public string $courseTitle, + public ?string $courseDescription, + public array $resources, + public array $lessons, + public array $frameResources = [], + ) {} +} diff --git a/src/Services/CommonCartridge/ResourceData.php b/src/Services/CommonCartridge/ResourceData.php new file mode 100644 index 0000000..c39000f --- /dev/null +++ b/src/Services/CommonCartridge/ResourceData.php @@ -0,0 +1,20 @@ + element. + */ +final class ResourceData +{ + public function __construct( + public string $identifier, + public string $type, + public string $href, + /** @var list */ + public array $fileHrefs = [], + public ?string $scormType = null, + ) {} +} diff --git a/src/Services/CommonCartridge/StepStructure.php b/src/Services/CommonCartridge/StepStructure.php new file mode 100644 index 0000000..19a9434 --- /dev/null +++ b/src/Services/CommonCartridge/StepStructure.php @@ -0,0 +1,19 @@ + \Tapp\FilamentLms\Tests\TestUser::class]); +}); + +test('manifest parser extracts course title and single lesson with one step from fixture', function () { + $fixturePath = __DIR__.'/../fixtures/common-cartridge'; + $parser = new ManifestParser; + $manifest = $parser->parse($fixturePath); + + expect($manifest->courseTitle)->toBe('Child Outcomes Summary'); + expect($manifest->lessons)->toHaveCount(1); + expect($manifest->lessons[0]->title)->toBe('Content'); + expect($manifest->lessons[0]->steps[0]->title)->toBe('Child Outcomes Summary'); + expect($manifest->lessons[0]->steps)->toHaveCount(1); + expect($manifest->lessons[0]->steps[0]->resourceIdentifier)->toBe('__6c3M623PUXY_course_id_RES'); + expect($manifest->resources)->toHaveCount(1); + $resource = $manifest->resources['__6c3M623PUXY_course_id_RES']; + expect($resource->type)->toBe('webcontent'); + expect($resource->href)->toBe('index_lms.html'); +}); + +test('import service creates course lesson step and document from fixture', function () { + $fixturePath = realpath(__DIR__.'/../fixtures/common-cartridge'); + expect($fixturePath)->not->toBeFalse(); + if (! is_file($fixturePath.'/index_lms.html')) { + test()->markTestSkipped('Fixture must contain index_lms.html'); + } + $parser = new ManifestParser; + $service = new CommonCartridgeImportService($parser); + + $result = $service->import($fixturePath); + + expect($result['course'])->toBeInstanceOf(Course::class); + expect($result['course']->name)->toBe('Child Outcomes Summary'); + expect($result['lessons_created'])->toBe(1); + expect($result['steps_created'])->toBe(1); + + $course = $result['course']; + $course->load('lessons.steps.material'); + expect($course->lessons)->toHaveCount(1); + $lesson = $course->lessons->first(); + expect($lesson->steps)->toHaveCount(1); + $step = $lesson->steps->first(); + expect($step->material_type)->toBe('document'); + expect($step->material_id)->not->toBeNull(); + $document = $step->material; + expect($document)->toBeInstanceOf(Document::class); + expect($document->getFirstMedia())->not->toBeNull(); +}); + +test('parser throws when imsmanifest.xml is missing', function () { + $parser = new ManifestParser; + $parser->parse('/nonexistent/path'); +})->throws(InvalidArgumentException::class); + +test('manifest parser uses Articulate frame.xml when present for lessons and steps', function () { + $fixturePath = __DIR__.'/../fixtures/common-cartridge-with-articulate'; + $parser = new ManifestParser; + $manifest = $parser->parse($fixturePath); + + expect($manifest->courseTitle)->toBe('Child Outcomes Summary'); + expect($manifest->lessons)->toHaveCount(2); + expect($manifest->lessons[0]->title)->toBe('Home'); + expect($manifest->lessons[0]->steps)->toHaveCount(3); + expect($manifest->lessons[0]->steps[0]->title)->toBe('Home'); + expect($manifest->lessons[0]->steps[1]->title)->toBe('Child Outcomes Summary (COS) Process Online Module'); + expect($manifest->lessons[0]->steps[2]->title)->toBe('What To Expect'); + expect($manifest->lessons[1]->title)->toBe('Session 1'); + expect($manifest->lessons[1]->steps)->toHaveCount(2); + expect($manifest->lessons[1]->steps[0]->title)->toBe("So What's This All About?"); + expect($manifest->lessons[1]->steps[1]->title)->toBe('Set-Up'); +}); diff --git a/tests/Feature/StepRenderedTextTest.php b/tests/Feature/StepRenderedTextTest.php new file mode 100644 index 0000000..337fded --- /dev/null +++ b/tests/Feature/StepRenderedTextTest.php @@ -0,0 +1,85 @@ +course = Course::query()->create([ + 'name' => 'Test Course', + 'slug' => 'test-course', + 'external_id' => 'test_course', + 'award' => 'default', + ]); + $this->lesson = Lesson::query()->create([ + 'course_id' => $this->course->id, + 'order' => 0, + 'name' => 'Test Lesson', + 'slug' => 'test-lesson', + ]); +}); + +test('getRenderedText returns empty string when text is null', function () { + $step = Step::query()->create([ + 'lesson_id' => $this->lesson->id, + 'order' => 0, + 'name' => 'Step', + 'slug' => 'step', + 'text' => null, + ]); + + expect($step->getRenderedText())->toBe(''); +}); + +test('getRenderedText returns empty string when text is blank', function () { + $step = Step::query()->create([ + 'lesson_id' => $this->lesson->id, + 'order' => 0, + 'name' => 'Step', + 'slug' => 'step', + 'text' => ' ', + ]); + + expect($step->getRenderedText())->toBe(''); +}); + +test('getRenderedText renders markdown when text does not start with angle bracket', function () { + $step = Step::query()->create([ + 'lesson_id' => $this->lesson->id, + 'order' => 0, + 'name' => 'Step', + 'slug' => 'step', + 'text' => 'Hello **world**', + ]); + + expect($step->getRenderedText())->toContain(''); + expect($step->getRenderedText())->toContain('world'); +}); + +test('getRenderedText sanitizes and returns HTML when text starts with angle bracket', function () { + $step = Step::query()->create([ + 'lesson_id' => $this->lesson->id, + 'order' => 0, + 'name' => 'Step', + 'slug' => 'step', + 'text' => '

    HTML content

    ', + ]); + + expect($step->getRenderedText())->toContain('

    '); + expect($step->getRenderedText())->toContain('HTML content'); +}); + +test('getRenderedText strips script tags from HTML', function () { + $step = Step::query()->create([ + 'lesson_id' => $this->lesson->id, + 'order' => 0, + 'name' => 'Step', + 'slug' => 'step', + 'text' => '

    Safe

    ', + ]); + + expect($step->getRenderedText())->not->toContain(' + + + + + + + + + + + + +
    + + + + + + + diff --git a/tests/fixtures/common-cartridge-with-articulate/imsmanifest.xml b/tests/fixtures/common-cartridge-with-articulate/imsmanifest.xml new file mode 100644 index 0000000..c00b904 --- /dev/null +++ b/tests/fixtures/common-cartridge-with-articulate/imsmanifest.xml @@ -0,0 +1,380 @@ + + + + ADL SCORM + 1.2 + + + + <langstring xml:lang="x-none">Child Outcomes Summary</langstring> + + + + + 1 + + + + LOMv1.0 + + + Final + + + + + ADL SCORM 1.2 + + + + PT0H0M0S + + + + + + LOMv1.0 + + + yes + + + + + LOMv1.0 + + + yes + + + + + + + + Child Outcomes Summary + + Child Outcomes Summary + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/common-cartridge-with-articulate/index_lms.html b/tests/fixtures/common-cartridge-with-articulate/index_lms.html new file mode 100644 index 0000000..c80e32e --- /dev/null +++ b/tests/fixtures/common-cartridge-with-articulate/index_lms.html @@ -0,0 +1,469 @@ + + + + + + + Child Outcomes Summary + + + + + + + + + + + + + + + + + + +
    + +
    + + + + +
    + + + + + +
    + + + + +
    +
    + + + + + +

    You are offline. Trying to reconnect...

    +
    +
    + + + + + + + + + + + + diff --git a/tests/fixtures/common-cartridge-with-articulate/meta.xml b/tests/fixtures/common-cartridge-with-articulate/meta.xml new file mode 100644 index 0000000..9889fe4 --- /dev/null +++ b/tests/fixtures/common-cartridge-with-articulate/meta.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/fixtures/common-cartridge-with-articulate/story_content/frame.xml b/tests/fixtures/common-cartridge-with-articulate/story_content/frame.xml new file mode 100644 index 0000000..f48b2de --- /dev/null +++ b/tests/fixtures/common-cartridge-with-articulate/story_content/frame.xml @@ -0,0 +1,1848 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hide captions (Ctrl+Alt+C) + Show captions (Ctrl+Alt+C) + Enter full-screen (Ctrl+Alt+F) + Exit full-screen (Ctrl+Alt+F) + locked + video captions + full-screen video + mute video + pause video + picture-in-picture + play video + playback speed + video settings + Open + video volume + Mute (Ctrl+Alt+M) + Next (Ctrl+Alt+Period) + Next (Ctrl+Alt+.) + Pause (Ctrl+Alt+P) + Play (Ctrl+Alt+P) + Playback speed + Previous (Ctrl+Alt+Comma) + Previous (Ctrl+Alt+,) + Replay (Ctrl+Alt+R) + search + Settings + Skip navigation. Press enter to return to the slide. + slide progress + Submit (Ctrl+Alt+S) + %count% of %total% item visited + %count% of %total% items visited + step %count% of %total% + Unmute (Ctrl+Alt+M) + Transcipt + visited + Close + Move window + Options + Resize window + Accessible text + Toggle accessible text + Action + Alt + Background audio + Toggle background audio + Clear and return to menu + Close + Captions + Toggle captions + Continue + Ctrl + Start Course + Please rotate your device + Enable keyboard shortcuts + Filter + Toggle full-screen + Glossary + Keyboard Shortcuts + Keyboard shortcuts + Start Course + Captions + Off + On + Normal + Video Playback Settings + Playback Speed + close video settings + Mute / unmute + NEXT + Next + Menu + Normal + Playback Speed + Play / pause + PREV + Previous + hour + hours + minute + minutes + second + seconds + Replay + Resources + Restart + Resume + Search... + Search in: + Search Results + Search + Shift + Shortcut + List keyboard shortcuts + Sidebar Toggle + Slide Text + Learn More + To watch this video, use the preview feature in Storyline 360, upload your published output to a server, publish to Review 360, or switch to static video quality. + Streaming video is unavailable for local playback. + SUBMIT + Submit + Terms + audio + camera moved down + camera moved left + camera moved right + camera moved up + %count% of %total% + %count% of %total% visited + hotspot + Use the w, a, s, and d keys to move around the 360 degree image. Press the tab key to jump to interactive markers and hotspots. + 360 degree image interaction + free exploration mode + guided tour mode + next + previous + label + marker + pause audio + pause video + play audio + play video + progress + progress + tooltip + %total% item + %total% items + video + video volume + Switch zoom modes + Notes + Autoscroll + Notes + Show timestamps + Resume autoscroll + Select track + Transcripts + Zoom to fit + Comma + Continue + Period + Question mark + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/common-cartridge.zip b/tests/fixtures/common-cartridge.zip new file mode 100644 index 0000000000000000000000000000000000000000..635911c22ee824cc2594a742402c5b1b590a04fc GIT binary patch literal 915 zcmWIWW@h1H0D(;*TVucsC?Uuo!;qYxo134fo19owQk0pJo~j=j!pXqAUV=Xhgi9;9 z85miMw zfk6SQ{kg@tiFui6sl_FF6;KDP1NsVt(R?;F=(OKq1A#r?wRP$pe1cZCYkBf_C^H2r z9z4l)u1(^nYW3wUHMV)PF0d%7T{Puf&YJO4KOuv}U1>*aym^J4*%FSvj+9&w>x9_&Pnz~Ba=|@t}&VTpq z{=WYuY3a*1O3XHw-nP-<5JM%$#c&4+*UOcH|JN^I@#o{Zb$Ip$w#99e9x82fj99OK z_=l&bb;-14ZOY+RD!N&vvH8lPfBX`I=W5-GWTgON#&8CT+x0HzBN z5MX%g2%@nhHC9McLyHWEvA7Z%#8_awGc0NJ#%3&fvLkE>W@ + + + + Child Outcomes Summary + + Child Outcomes Summary + + + + + + + + + diff --git a/tests/fixtures/storyline-html5/index.html b/tests/fixtures/storyline-html5/index.html new file mode 100644 index 0000000..be5409d --- /dev/null +++ b/tests/fixtures/storyline-html5/index.html @@ -0,0 +1 @@ +HTML5 diff --git a/tests/fixtures/storyline-html5/meta.xml b/tests/fixtures/storyline-html5/meta.xml new file mode 100644 index 0000000..9c1a524 --- /dev/null +++ b/tests/fixtures/storyline-html5/meta.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/fixtures/storyline-html5/story_content/frame.xml b/tests/fixtures/storyline-html5/story_content/frame.xml new file mode 100644 index 0000000..dc6f8b2 --- /dev/null +++ b/tests/fixtures/storyline-html5/story_content/frame.xml @@ -0,0 +1,1876 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hide captions (Ctrl+Alt+C) + Show captions (Ctrl+Alt+C) + Enter full-screen (Ctrl+Alt+F) + Exit full-screen (Ctrl+Alt+F) + locked + video captions + full-screen video + mute video + pause video + picture-in-picture + play video + playback speed + video settings + Open + video volume + Mute (Ctrl+Alt+M) + Next (Ctrl+Alt+Period) + Next (Ctrl+Alt+.) + Pause (Ctrl+Alt+P) + Play (Ctrl+Alt+P) + Playback speed + Previous (Ctrl+Alt+Comma) + Previous (Ctrl+Alt+,) + Replay (Ctrl+Alt+R) + search + Settings + Skip navigation. Press enter to return to the slide. + slide progress + Submit (Ctrl+Alt+S) + %count% of %total% item visited + %count% of %total% items visited + step %count% of %total% + Unmute (Ctrl+Alt+M) + Transcipt + visited + Close + Move window + Options + Resize window + Accessible text + Toggle accessible text + Action + Alt + Background audio + Toggle background audio + Clear and return to menu + Close + Captions + Toggle captions + Continue + Ctrl + Start Course + Please rotate your device + Enable keyboard shortcuts + Filter + Toggle full-screen + Glossary + Keyboard Shortcuts + Keyboard shortcuts + Start Course + Captions + Off + On + Normal + Video Playback Settings + Playback Speed + close video settings + Mute / unmute + NEXT + Next + Menu + Normal + Playback Speed + Play / pause + PREV + Previous + hour + hours + minute + minutes + second + seconds + Replay + Resources + Restart + Resume + Search... + Search in: + Search Results + Search + Shift + Shortcut + List keyboard shortcuts + Sidebar Toggle + Slide Text + Learn More + To watch this video, use the preview feature in Storyline 360, upload your published output to a server, publish to Review 360, or switch to static video quality. + Streaming video is unavailable for local playback. + SUBMIT + Submit + Terms + audio + camera moved down + camera moved left + camera moved right + camera moved up + %count% of %total% + %count% of %total% visited + hotspot + Use the w, a, s, and d keys to move around the 360 degree image. Press the tab key to jump to interactive markers and hotspots. + 360 degree image interaction + free exploration mode + guided tour mode + next + previous + label + marker + pause audio + pause video + play audio + play video + progress + progress + tooltip + %total% item + %total% items + video + video volume + Switch zoom modes + Notes + Autoscroll + Notes + Show timestamps + Resume autoscroll + Select track + Transcripts + Zoom to fit + Comma + Continue + Period + Question mark + Select an option + eighth + fifth + first + fourth + Menu + ninth + %answer% (correct choice) + %count% result found for %term% + %count% results found for %term% + second + %time% played + seventh + sixth + tenth + third + Transcript + OK + You must complete the question before submitting. + Invalid Answer + OK + Enter name + Enter your name in the field below: + OK + Time Limit Exceeded + You have reached the time limit set for the quiz. Press 'OK' to continue. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From d3150dda150652526cb7fa7a31935801630e1064 Mon Sep 17 00:00:00 2001 From: Steve Williamson Date: Fri, 22 May 2026 15:20:50 -0400 Subject: [PATCH 04/16] wip --- config/filament-lms.php | 8 + ...edded_player_to_lms_courses_table.php.stub | 23 ++ ...layer_slide_id_to_lms_steps_table.php.stub | 22 ++ dist/filament-lms.css | 29 ++ resources/css/plugin.css | 29 ++ .../embedded-player-body-class.blade.php | 3 + resources/views/components/exit-lms.blade.php | 60 ++- .../html5-exit-completion-script.blade.php | 41 ++ .../html5-package-bridge-script.blade.php | 44 +++ .../components/html5-player-bridge.blade.php | 69 ++++ .../components/scorm-api-bridge.blade.php | 77 ++++ .../views/livewire/document-step.blade.php | 24 +- routes/web.php | 10 +- src/Concerns/CourseLayout.php | 39 +- .../BackfillEmbeddedPlayerCourses.php | 59 +++ src/Enums/CompletionMode.php | 12 + src/FilamentLmsServiceProvider.php | 4 + .../Controllers/ScormCommitController.php | 53 +++ .../Controllers/ScormPackageController.php | 58 ++- src/Livewire/DocumentStep.php | 14 +- src/LmsPanelProvider.php | 6 + src/Models/Course.php | 47 +++ src/Pages/Step.php | 95 ++++- src/Resources/CourseResource.php | 13 + .../CommonCartridgeImportService.php | 25 +- .../CommonCartridge/ScormPackageStorage.php | 3 +- src/Services/ScormProgressService.php | 275 ++++++++++++++ src/Traits/FilamentLmsUser.php | 7 + tests/Feature/CommonCartridgeImportTest.php | 10 + tests/Feature/EmbeddedPlayerTest.php | 358 ++++++++++++++++++ tests/Feature/ScormPackageServingTest.php | 170 +++++++++ tests/TestCase.php | 3 + 32 files changed, 1658 insertions(+), 32 deletions(-) create mode 100644 database/migrations/add_embedded_player_to_lms_courses_table.php.stub create mode 100644 database/migrations/add_player_slide_id_to_lms_steps_table.php.stub create mode 100644 resources/views/components/embedded-player-body-class.blade.php create mode 100644 resources/views/components/html5-exit-completion-script.blade.php create mode 100644 resources/views/components/html5-package-bridge-script.blade.php create mode 100644 resources/views/components/html5-player-bridge.blade.php create mode 100644 resources/views/components/scorm-api-bridge.blade.php create mode 100644 src/Console/Commands/BackfillEmbeddedPlayerCourses.php create mode 100644 src/Enums/CompletionMode.php create mode 100644 src/Http/Controllers/ScormCommitController.php create mode 100644 src/Services/ScormProgressService.php create mode 100644 tests/Feature/EmbeddedPlayerTest.php diff --git a/config/filament-lms.php b/config/filament-lms.php index 829c0c2..f492aef 100644 --- a/config/filament-lms.php +++ b/config/filament-lms.php @@ -160,6 +160,14 @@ | Common Cartridge / SCORM import |-------------------------------------------------------------------------- */ + /* + |-------------------------------------------------------------------------- + | Embedded player completion guards + |-------------------------------------------------------------------------- + */ + 'embedded_player_min_session_seconds' => 90, + 'embedded_player_min_session_seconds_html5' => 300, + 'common_cartridge_import' => [ 'delete_after_success' => true, 'storage_disk' => 'local', diff --git a/database/migrations/add_embedded_player_to_lms_courses_table.php.stub b/database/migrations/add_embedded_player_to_lms_courses_table.php.stub new file mode 100644 index 0000000..a95a833 --- /dev/null +++ b/database/migrations/add_embedded_player_to_lms_courses_table.php.stub @@ -0,0 +1,23 @@ +boolean('embedded_player')->default(false)->after('is_private'); + $table->string('completion_mode', 32)->default('native')->after('embedded_player'); + }); + } + + public function down(): void + { + Schema::table('lms_courses', function (Blueprint $table): void { + $table->dropColumn(['embedded_player', 'completion_mode']); + }); + } +}; diff --git a/database/migrations/add_player_slide_id_to_lms_steps_table.php.stub b/database/migrations/add_player_slide_id_to_lms_steps_table.php.stub new file mode 100644 index 0000000..58aeb90 --- /dev/null +++ b/database/migrations/add_player_slide_id_to_lms_steps_table.php.stub @@ -0,0 +1,22 @@ +string('player_slide_id')->nullable()->after('text'); + }); + } + + public function down(): void + { + Schema::table('lms_steps', function (Blueprint $table): void { + $table->dropColumn('player_slide_id'); + }); + } +}; diff --git a/dist/filament-lms.css b/dist/filament-lms.css index d91d916..736c8f5 100644 --- a/dist/filament-lms.css +++ b/dist/filament-lms.css @@ -39,6 +39,35 @@ min-height: 60vh; } +/* SCORM / HTML5 interactive packages: use full content width */ +.step-material-container--interactive { + width: 100%; + max-width: 100%; + margin: 0; + height: clamp(28rem, 80vh, 64rem); + min-height: 28rem; +} + +.step-material-container--interactive iframe { + display: block; + width: 100%; + height: 100%; + border: 0; +} + +/* Embedded player: hide Filament sidebar and use full width */ +body.lms-embedded-player .fi-sidebar { + display: none !important; +} + +body.lms-embedded-player .fi-main-ctn { + margin-inline-start: 0 !important; +} + +body.lms-embedded-player .fi-main { + max-width: 100%; +} + /* Knowledge checks: card-style radio options (single-choice Select fields on LMS tests). */ .lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio { counter-reset: lms-mcq-option; diff --git a/resources/css/plugin.css b/resources/css/plugin.css index d91d916..736c8f5 100644 --- a/resources/css/plugin.css +++ b/resources/css/plugin.css @@ -39,6 +39,35 @@ min-height: 60vh; } +/* SCORM / HTML5 interactive packages: use full content width */ +.step-material-container--interactive { + width: 100%; + max-width: 100%; + margin: 0; + height: clamp(28rem, 80vh, 64rem); + min-height: 28rem; +} + +.step-material-container--interactive iframe { + display: block; + width: 100%; + height: 100%; + border: 0; +} + +/* Embedded player: hide Filament sidebar and use full width */ +body.lms-embedded-player .fi-sidebar { + display: none !important; +} + +body.lms-embedded-player .fi-main-ctn { + margin-inline-start: 0 !important; +} + +body.lms-embedded-player .fi-main { + max-width: 100%; +} + /* Knowledge checks: card-style radio options (single-choice Select fields on LMS tests). */ .lms-test-form-wrapper .lms-knowledge-check-radio-field .fi-fo-radio { counter-reset: lms-mcq-option; diff --git a/resources/views/components/embedded-player-body-class.blade.php b/resources/views/components/embedded-player-body-class.blade.php new file mode 100644 index 0000000..0d37b47 --- /dev/null +++ b/resources/views/components/embedded-player-body-class.blade.php @@ -0,0 +1,3 @@ + diff --git a/resources/views/components/exit-lms.blade.php b/resources/views/components/exit-lms.blade.php index 9fa6629..9a9f8d5 100644 --- a/resources/views/components/exit-lms.blade.php +++ b/resources/views/components/exit-lms.blade.php @@ -1,3 +1,57 @@ - - Exit LMS - +@php + use Illuminate\Support\Facades\Auth; + use Tapp\FilamentLms\Enums\CompletionMode; + use Tapp\FilamentLms\Models\Course; + use Tapp\FilamentLms\Services\ScormProgressService; + + $courseSlug = request()->route('courseSlug'); + $course = is_string($courseSlug) + ? Course::query()->where('slug', $courseSlug)->first() + : null; + $progressService = app(ScormProgressService::class); + $user = Auth::user(); + $needsConfirm = $course + && $course->isEmbeddedPlayer() + && $course->completionMode() === CompletionMode::Html5 + && $user + && ! $progressService->courseCompletedByUser($course, $user); + $canComplete = $needsConfirm && $user + ? $progressService->userMayConfirmCourseCompletion($course, $user) + : false; + $commitUrl = $needsConfirm + ? route('filament-lms.scorm-commit.store', ['course' => $course]) + : null; +@endphp + +@if ($needsConfirm) + @include('filament-lms::components.html5-exit-completion-script') + + Exit LMS + + +@else + + Exit LMS + +@endif diff --git a/resources/views/components/html5-exit-completion-script.blade.php b/resources/views/components/html5-exit-completion-script.blade.php new file mode 100644 index 0000000..bcd9fd3 --- /dev/null +++ b/resources/views/components/html5-exit-completion-script.blade.php @@ -0,0 +1,41 @@ + diff --git a/resources/views/components/html5-package-bridge-script.blade.php b/resources/views/components/html5-package-bridge-script.blade.php new file mode 100644 index 0000000..774adc0 --- /dev/null +++ b/resources/views/components/html5-package-bridge-script.blade.php @@ -0,0 +1,44 @@ + diff --git a/resources/views/components/html5-player-bridge.blade.php b/resources/views/components/html5-player-bridge.blade.php new file mode 100644 index 0000000..51344a6 --- /dev/null +++ b/resources/views/components/html5-player-bridge.blade.php @@ -0,0 +1,69 @@ +@php + use Illuminate\Support\Facades\Auth; + use Tapp\FilamentLms\Services\ScormProgressService; + + /** @var \Tapp\FilamentLms\Models\Course $course */ + $user = Auth::user(); + $canComplete = $user + ? app(ScormProgressService::class)->userMayConfirmCourseCompletion($course, $user) + : false; +@endphp +@include('filament-lms::components.html5-exit-completion-script') + diff --git a/resources/views/components/scorm-api-bridge.blade.php b/resources/views/components/scorm-api-bridge.blade.php new file mode 100644 index 0000000..d1c2c05 --- /dev/null +++ b/resources/views/components/scorm-api-bridge.blade.php @@ -0,0 +1,77 @@ +@php + /** @var \Tapp\FilamentLms\Models\Course $course */ +@endphp + diff --git a/resources/views/livewire/document-step.blade.php b/resources/views/livewire/document-step.blade.php index 181c14c..678f48d 100644 --- a/resources/views/livewire/document-step.blade.php +++ b/resources/views/livewire/document-step.blade.php @@ -10,19 +10,31 @@ class="flex-1 flex flex-col" class="step-material-container rounded-lg border border-gray-300 cursor-pointer" wire:click="download" /> + @elseif ($document->hasScormPackage()) +
    + +
    @else - @endif - - Download - + @unless ($step->lesson->course->isEmbeddedPlayer()) + + Download + + @endunless - + @unless ($step->lesson->course->isEmbeddedPlayer()) + + @endunless diff --git a/routes/web.php b/routes/web.php index ffe8649..757dc6e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,7 @@ use Illuminate\Support\Facades\Route; use Tapp\FilamentLms\Http\Controllers\CertificateController; +use Tapp\FilamentLms\Http\Controllers\ScormCommitController; use Tapp\FilamentLms\Http\Controllers\ScormPackageController; /* @@ -22,7 +23,12 @@ Route::get('lms/certificates/{course}/{user}', [CertificateController::class, 'show']) ->name('filament-lms::certificates.show'); - Route::get('lms/scorm-package/{document}', [ScormPackageController::class, 'show']) + Route::get('lms/scorm-package/{document}/{entry?}', [ScormPackageController::class, 'show']) + ->where('entry', '.*') ->name('filament-lms.scorm-package.show') - ->middleware('auth'); + ->middleware(['web', 'auth']); + + Route::post('lms/scorm-commit/{course}', [ScormCommitController::class, 'store']) + ->name('filament-lms.scorm-commit.store') + ->middleware(['web', 'auth']); }); diff --git a/src/Concerns/CourseLayout.php b/src/Concerns/CourseLayout.php index 1cb6a0c..51494d8 100644 --- a/src/Concerns/CourseLayout.php +++ b/src/Concerns/CourseLayout.php @@ -5,20 +5,49 @@ use Filament\Support\Facades\FilamentView; use Filament\View\PanelsRenderHook; use Illuminate\Contracts\View\View; +use Tapp\FilamentLms\Pages\Step as StepPage; trait CourseLayout { - public function registerCourseLayout() + public function registerCourseLayout(): void { - FilamentView::registerRenderHook( - PanelsRenderHook::SIDEBAR_NAV_START, - fn (): View => view('filament-lms::components.nav-course-name', ['course' => $this->course]), - ); + if (! $this->course->isEmbeddedPlayer()) { + FilamentView::registerRenderHook( + PanelsRenderHook::SIDEBAR_NAV_START, + fn (): View => view('filament-lms::components.nav-course-name', ['course' => $this->course]), + ); + } FilamentView::registerRenderHook( PanelsRenderHook::TOPBAR_AFTER, fn (): View => view('filament-lms::components.topbar-course-progress', ['course' => $this->course]), ); + if ($this->course->isEmbeddedPlayer() && $this instanceof StepPage) { + FilamentView::registerRenderHook( + PanelsRenderHook::BODY_START, + fn (): View => view('filament-lms::components.embedded-player-body-class'), + ); + + if ($this->shouldRegisterScormBridge()) { + FilamentView::registerRenderHook( + PanelsRenderHook::BODY_END, + fn (): View => view('filament-lms::components.scorm-api-bridge', [ + 'course' => $this->course, + 'commitUrl' => route('filament-lms.scorm-commit.store', ['course' => $this->course]), + ]), + ); + } + + if ($this->shouldRegisterHtml5Bridge()) { + FilamentView::registerRenderHook( + PanelsRenderHook::BODY_END, + fn (): View => view('filament-lms::components.html5-player-bridge', [ + 'course' => $this->course, + 'commitUrl' => route('filament-lms.scorm-commit.store', ['course' => $this->course]), + ]), + ); + } + } } } diff --git a/src/Console/Commands/BackfillEmbeddedPlayerCourses.php b/src/Console/Commands/BackfillEmbeddedPlayerCourses.php new file mode 100644 index 0000000..4c06a65 --- /dev/null +++ b/src/Console/Commands/BackfillEmbeddedPlayerCourses.php @@ -0,0 +1,59 @@ +whereNotNull('package_path') + ->where('package_path', '!=', '') + ->pluck('id'); + + if ($courseIds->isEmpty()) { + $this->warn('No documents with retained packages found.'); + + return self::SUCCESS; + } + + $query = Course::query() + ->whereHas('steps', function ($stepQuery) use ($courseIds): void { + $stepQuery + ->where('material_type', 'document') + ->whereIn('material_id', $courseIds); + }); + + if ($courseId = $this->option('course-id')) { + $query->whereKey($courseId); + } + + $updated = 0; + foreach ($query->get() as $course) { + $course->update([ + 'embedded_player' => true, + 'completion_mode' => $course->completion_mode === CompletionMode::Native + ? CompletionMode::Html5 + : $course->completion_mode, + ]); + $updated++; + $this->line("Updated course [{$course->id}] {$course->name}"); + } + + $this->info("Updated {$updated} course(s). Re-import SCORM 1.2 zips for full API completion sync."); + + return self::SUCCESS; + } +} diff --git a/src/Enums/CompletionMode.php b/src/Enums/CompletionMode.php new file mode 100644 index 0000000..d8da994 --- /dev/null +++ b/src/Enums/CompletionMode.php @@ -0,0 +1,12 @@ +hasCommand(BackfillCourseCompletedAt::class) + ->hasCommand(BackfillEmbeddedPlayerCourses::class) ->hasCommand(ImportCartridgesCommand::class) ->hasInstallCommand(function (InstallCommand $command) { $command diff --git a/src/Http/Controllers/ScormCommitController.php b/src/Http/Controllers/ScormCommitController.php new file mode 100644 index 0000000..050c5e3 --- /dev/null +++ b/src/Http/Controllers/ScormCommitController.php @@ -0,0 +1,53 @@ +isEmbeddedPlayer(), 404); + + $user = Auth::user(); + abort_unless($user !== null && Course::accessibleTo($user)->whereKey($course->id)->exists(), 403); + + $validated = $request->validate([ + 'lesson_status' => ['nullable', 'string', 'max:64'], + 'lesson_location' => ['nullable', 'string', 'max:255'], + 'suspend_data' => ['nullable', 'string'], + 'score' => ['nullable', 'string', 'max:64'], + 'html5_complete' => ['nullable', 'boolean'], + 'html5_progress' => ['nullable', 'boolean'], + 'initialized' => ['nullable', 'boolean'], + 'finished' => ['nullable', 'boolean'], + ]); + + $service = app(ScormProgressService::class); + + if (! empty($validated['html5_complete'])) { + $result = $service->attemptManualCourseCompletion($course, $user); + + return response()->json($result, $result['ok'] ? 200 : 422); + } + + $service->processCommit($course, $user, [ + 'lesson_status' => $validated['lesson_status'] ?? null, + 'lesson_location' => $validated['lesson_location'] ?? null, + 'suspend_data' => $validated['suspend_data'] ?? null, + 'score' => $validated['score'] ?? null, + 'html5_progress' => $validated['html5_progress'] ?? false, + ]); + + return response()->json(['ok' => true]); + } +} diff --git a/src/Http/Controllers/ScormPackageController.php b/src/Http/Controllers/ScormPackageController.php index b5ba6e6..f8ee4eb 100644 --- a/src/Http/Controllers/ScormPackageController.php +++ b/src/Http/Controllers/ScormPackageController.php @@ -8,14 +8,15 @@ use Illuminate\Routing\Controller; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage; -use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\Response; +use Tapp\FilamentLms\Enums\CompletionMode; use Tapp\FilamentLms\Models\Course; use Tapp\FilamentLms\Models\Document; use Tapp\FilamentLms\Models\Step; final class ScormPackageController extends Controller { - public function show(Request $request, Document $document): BinaryFileResponse + public function show(Request $request, Document $document, ?string $entry = null): Response { abort_unless(Auth::check(), 403); abort_unless($document->hasScormPackage(), 404); @@ -26,9 +27,11 @@ public function show(Request $request, Document $document): BinaryFileResponse $user = Auth::user(); abort_unless($user !== null && Course::accessibleTo($user)->whereKey($course->id)->exists(), 403); - $disk = $document->package_disk ?: (string) config('filament-lms.common_cartridge_import.storage_disk', 'local'); - $packageRoot = Storage::disk($disk)->path($document->package_path); - $relativePath = (string) $request->query('entry', $document->getScormLaunchPath()); + $packageRoot = $this->resolvePackageRoot($document); + abort_if($packageRoot === null, 404); + + // Path-based URLs (e.g. /lms/scorm-package/1/index.html) let relative asset paths resolve correctly. + $relativePath = $entry ?? (string) $request->query('entry', $document->getScormLaunchPath()); $relativePath = ltrim(str_replace('\\', '/', $relativePath), '/'); abort_if(str_contains($relativePath, '..'), 404); @@ -39,11 +42,54 @@ public function show(Request $request, Document $document): BinaryFileResponse abort_unless(str_starts_with($realFile, $realPackageRoot), 404); abort_unless(is_file($realFile), 404); + $mimeType = $this->mimeType($realFile); + + if ($course->isEmbeddedPlayer() + && $course->completionMode() === CompletionMode::Html5 + && in_array($mimeType, ['text/html'], true)) { + $content = file_get_contents($realFile); + if (is_string($content)) { + $content = $this->injectHtml5BridgeIntoHtml($content); + + return response($content, 200, ['Content-Type' => $mimeType]); + } + } + return response()->file($realFile, [ - 'Content-Type' => $this->mimeType($realFile), + 'Content-Type' => $mimeType, ]); } + private function injectHtml5BridgeIntoHtml(string $content): string + { + $script = view('filament-lms::components.html5-package-bridge-script')->render(); + + if (str_contains($content, '')) { + return str_replace('', $script.'', $content); + } + + return $content.$script; + } + + private function resolvePackageRoot(Document $document): ?string + { + if ($document->package_path === null || $document->package_path === '') { + return null; + } + + $disk = $document->package_disk ?: (string) config('filament-lms.common_cartridge_import.storage_disk', 'local'); + $configuredRoot = Storage::disk($disk)->path($document->package_path); + + if (is_dir($configuredRoot)) { + return $configuredRoot; + } + + // Imports before Laravel 12 disk layout stored packages under storage/app/ directly. + $legacyRoot = storage_path('app/'.$document->package_path); + + return is_dir($legacyRoot) ? $legacyRoot : null; + } + private function resolveCourseForDocument(Document $document): ?Course { $step = Step::query() diff --git a/src/Livewire/DocumentStep.php b/src/Livewire/DocumentStep.php index e51f487..671a80a 100644 --- a/src/Livewire/DocumentStep.php +++ b/src/Livewire/DocumentStep.php @@ -2,9 +2,12 @@ namespace Tapp\FilamentLms\Livewire; +use Illuminate\Support\Facades\Auth; use Livewire\Component; +use Tapp\FilamentLms\Enums\CompletionMode; use Tapp\FilamentLms\Models\Document; use Tapp\FilamentLms\Models\Step; +use Tapp\FilamentLms\Services\ScormProgressService; class DocumentStep extends Component { @@ -14,11 +17,20 @@ class DocumentStep extends Component public bool $downloaded; - public function mount($step) + public function mount($step): void { $this->step = $step; $this->document = $step->material; $this->downloaded = (bool) $step->completed_at; + + $course = $step->lesson->course; + $user = Auth::user(); + if ($course->isEmbeddedPlayer() + && $course->completionMode() === CompletionMode::Html5 + && $user !== null + && $course->launchStep()?->is($step)) { + app(ScormProgressService::class)->recordStarted($course, $user); + } } public function render() diff --git a/src/LmsPanelProvider.php b/src/LmsPanelProvider.php index 409205c..0c42c77 100644 --- a/src/LmsPanelProvider.php +++ b/src/LmsPanelProvider.php @@ -174,6 +174,12 @@ function () use ($hookedNavigationItems): View { $course = Course::where('slug', $courseSlug)->firstOrFail(); + if ($course->isEmbeddedPlayer()) { + $builder->groups([]); + + return $builder; + } + $navigationGroups = $course->lessons->map(function ($lesson) { /** @var Lesson $lesson */ return NavigationGroup::make($lesson->name) diff --git a/src/Models/Course.php b/src/Models/Course.php index b9d9ad5..4133d29 100644 --- a/src/Models/Course.php +++ b/src/Models/Course.php @@ -22,6 +22,7 @@ use Tapp\FilamentFormBuilder\Models\FilamentFormUser; use Tapp\FilamentLms\Contracts\FilamentLmsUserInterface; use Tapp\FilamentLms\Database\Factories\CourseFactory; +use Tapp\FilamentLms\Enums\CompletionMode; use Tapp\FilamentLms\Models\Traits\BelongsToTenant; use Tapp\FilamentLms\Pages\CourseCompleted; use Tapp\FilamentLms\Pages\Dashboard; @@ -39,6 +40,8 @@ * @property string|null $description * @property int|null $required_test_percentage * @property bool $is_private + * @property bool $embedded_player + * @property CompletionMode|string $completion_mode * @property Carbon $created_at * @property Carbon $updated_at * @property-read \Illuminate\Database\Eloquent\Collection|Lesson[] $lessons @@ -58,6 +61,8 @@ final class Course extends Model implements HasMedia protected $casts = [ 'award_content' => 'array', 'is_private' => 'boolean', + 'embedded_player' => 'boolean', + 'completion_mode' => CompletionMode::class, ]; public function registerMediaCollections(): void @@ -107,8 +112,50 @@ public function lessons(): HasMany return $this->hasMany(Lesson::class)->ordered(); } + public function isEmbeddedPlayer(): bool + { + return (bool) $this->embedded_player; + } + + public function completionMode(): CompletionMode + { + $mode = $this->completion_mode; + + return $mode instanceof CompletionMode + ? $mode + : CompletionMode::tryFrom((string) $mode) ?? CompletionMode::Native; + } + + public function launchStep(): ?Step + { + $this->loadMissing(['lessons.steps']); + + foreach ($this->lessons->sortBy('order') as $lesson) { + foreach ($lesson->steps->sortBy('order') as $step) { + if ($step->material_type !== 'document') { + continue; + } + + $material = $step->material; + if ($material instanceof Document && $material->hasScormPackage()) { + return $step; + } + } + } + + return $this->steps()->first(); + } + public function linkToCurrentStep(): string { + if ($this->isEmbeddedPlayer()) { + $launchStep = $this->launchStep(); + + return $launchStep !== null + ? StepPage::getUrlForStep($launchStep) + : Dashboard::getUrl(); + } + // Get all steps in order $allSteps = $this->steps()->ordered()->get(); diff --git a/src/Pages/Step.php b/src/Pages/Step.php index 040add2..bab4844 100644 --- a/src/Pages/Step.php +++ b/src/Pages/Step.php @@ -3,6 +3,7 @@ namespace Tapp\FilamentLms\Pages; use Filament\Actions\Action; +use Filament\Notifications\Notification; use Filament\Pages\Page; use Filament\Support\Enums\Width; use Illuminate\Support\Facades\Auth; @@ -10,9 +11,11 @@ use Livewire\Attributes\On; use Tapp\FilamentLms\Concerns\CourseLayout; use Tapp\FilamentLms\Contracts\FilamentLmsUserInterface; +use Tapp\FilamentLms\Enums\CompletionMode; use Tapp\FilamentLms\Models\Course; use Tapp\FilamentLms\Models\Lesson; use Tapp\FilamentLms\Models\Step as StepModel; +use Tapp\FilamentLms\Services\ScormProgressService; class Step extends Page { @@ -36,6 +39,13 @@ public function mount($courseSlug, $lessonSlug, $stepSlug) $this->course->loadProgress(); $this->lesson = $this->course->lessons->where('slug', $lessonSlug)->firstOrFail(); $this->step = $this->lesson->steps->where('slug', $stepSlug)->firstOrFail(); + + if ($this->course->isEmbeddedPlayer()) { + $launchStep = $this->course->launchStep(); + if ($launchStep !== null && ! $launchStep->is($this->step)) { + return redirect()->to(static::getUrlForStep($launchStep)); + } + } // @phpstan-ignore-next-line $this->heading = $this->step->name; @@ -86,12 +96,35 @@ public function mount($courseSlug, $lessonSlug, $stepSlug) protected function getHeaderActions(): array { - $actions = [ - Action::make('viewAllCourses') + $actions = []; + + if ($this->course->isEmbeddedPlayer()) { + $exitCourse = Action::make('exitCourse') + ->label('Exit Course') + ->color('gray') + ->action(fn () => $this->exitCourse()); + + if ($this->shouldRegisterHtml5Bridge()) { + $user = Auth::user(); + $progressService = app(ScormProgressService::class); + $needsCompletionConfirm = $user !== null + && ! $progressService->courseCompletedByUser($this->course, $user); + + if ($needsCompletionConfirm) { + $exitCourse + ->requiresConfirmation() + ->modalHeading('Exit course') + ->modalDescription('Mark this course as complete before returning to your courses?'); + } + } + + $actions[] = $exitCourse; + } else { + $actions[] = Action::make('viewAllCourses') ->label('View All Courses') ->color('gray') - ->url(Dashboard::getUrl()), - ]; + ->url(Dashboard::getUrl()); + } // Add Edit button for users who can edit the step if (Auth::check()) { @@ -132,8 +165,58 @@ public function getMaxContentWidth(): Width return Width::Full; } - public function viewAllCourses() + public function exitCourse(): void + { + $user = Auth::user(); + if (! $user instanceof FilamentLmsUserInterface) { + $this->redirect(Dashboard::getUrl()); + + return; + } + + $progressService = app(ScormProgressService::class); + + if ( + $this->course->isEmbeddedPlayer() + && $this->course->completionMode() === CompletionMode::Html5 + && ! $progressService->courseCompletedByUser($this->course, $user) + ) { + $result = $progressService->attemptManualCourseCompletion($this->course, $user); + + if (! $result['ok']) { + Notification::make() + ->title('Cannot mark course complete yet') + ->body($result['message']) + ->danger() + ->send(); + + return; + } + } + + $this->redirect(Dashboard::getUrl()); + } + + #[On('scorm-course-complete')] + public function scormCourseComplete(): void + { + $user = Auth::user(); + if (! $user instanceof FilamentLmsUserInterface) { + return; + } + + app(ScormProgressService::class)->completeAllEligibleSteps($this->course, $user); + } + + public function shouldRegisterScormBridge(): bool + { + return $this->course->isEmbeddedPlayer() + && $this->course->completionMode() === CompletionMode::Scorm12; + } + + public function shouldRegisterHtml5Bridge(): bool { - return redirect()->to(Dashboard::getUrl()); + return $this->course->isEmbeddedPlayer() + && $this->course->completionMode() === CompletionMode::Html5; } } diff --git a/src/Resources/CourseResource.php b/src/Resources/CourseResource.php index ce96768..ee42a55 100644 --- a/src/Resources/CourseResource.php +++ b/src/Resources/CourseResource.php @@ -23,6 +23,7 @@ use Illuminate\Support\HtmlString; use Illuminate\Support\Str; use Tapp\FilamentLms\Concerns\HasLmsSlug; +use Tapp\FilamentLms\Enums\CompletionMode; use Tapp\FilamentLms\Models\Course; use Tapp\FilamentLms\Models\CreditCategory; use Tapp\FilamentLms\RelationManagers\CourseUsersRelationManager; @@ -123,6 +124,18 @@ public static function form(Schema $schema): Schema return null; }) ->helperText('Form must be saved before previewing.'), + Checkbox::make('embedded_player') + ->label('Embedded player mode') + ->helperText('Hides LMS step sidebar and uses the SCORM/HTML5 package as the primary navigation.'), + Select::make('completion_mode') + ->label('Completion mode') + ->options([ + CompletionMode::Native->value => 'Native (per-step in LMS)', + CompletionMode::Scorm12->value => 'SCORM 1.2 API sync', + CompletionMode::Html5->value => 'HTML5 (started on enter, complete on exit or player signal)', + ]) + ->default(CompletionMode::Native->value) + ->required(), Checkbox::make('is_private') ->label('Private Course') ->helperText('Private courses are only visible to assigned users and LMS admins'), diff --git a/src/Services/CommonCartridge/CommonCartridgeImportService.php b/src/Services/CommonCartridge/CommonCartridgeImportService.php index 27bf333..9a099ef 100644 --- a/src/Services/CommonCartridge/CommonCartridgeImportService.php +++ b/src/Services/CommonCartridge/CommonCartridgeImportService.php @@ -9,6 +9,7 @@ use Illuminate\Support\Str; use Tapp\FilamentFormBuilder\Models\FilamentForm; use Tapp\FilamentFormBuilder\Models\FilamentFormField; +use Tapp\FilamentLms\Enums\CompletionMode; use Tapp\FilamentLms\Helpers\TenantHelper; use Tapp\FilamentLms\Models\Course; use Tapp\FilamentLms\Models\Document; @@ -38,7 +39,7 @@ public static function manualImportGaps(): array return [ 'Assessments import only the first question; add additional fields in Form Builder if needed.', 'Articulate Rise courses import as a single step (no per-block lesson structure).', - 'SCORM completion and scores are not synced; learners complete steps in the LMS.', + 'Enable embedded player mode on the course for SCORM/HTML5 completion sync after import.', 'Assign learners to the course via course users when using private courses or restricted visibility.', ]; } @@ -100,10 +101,28 @@ public function import(string $extractedPath, int|string|null $tenantId = null): }); $this->attachRetainedPackage($extractedPath); + $this->finalizeEmbeddedPlayerCourse($result['course'], $extractedPath); return $result; } + private function finalizeEmbeddedPlayerCourse(Course $course, string $extractedPath): void + { + if ($this->packageDocuments === []) { + return; + } + + $extractedPath = mb_rtrim($extractedPath, '/'); + $completionMode = is_file($extractedPath.'/imsmanifest.xml') + ? CompletionMode::Scorm12 + : CompletionMode::Html5; + + $course->update([ + 'embedded_player' => true, + 'completion_mode' => $completionMode, + ]); + } + private function attachRetainedPackage(string $extractedPath): void { if ($this->packageDocuments === []) { @@ -128,12 +147,13 @@ private function createCourse(ParsedManifest $manifest, int|string|null $tenantI { $slug = $this->uniqueCourseSlug(Str::slug($manifest->courseTitle), $tenantId); $externalId = $this->uniqueCourseColumn(Str::slug($manifest->courseTitle, '_'), 'external_id', $tenantId); + $name = $this->uniqueCourseColumn($manifest->courseTitle, 'name', $tenantId); $awards = config('filament-lms.awards', ['default' => 'Default']); $defaultAward = array_key_first($awards); $data = [ - 'name' => $manifest->courseTitle, + 'name' => $name, 'slug' => $slug, 'external_id' => $externalId, 'description' => $manifest->courseDescription, @@ -321,6 +341,7 @@ private function createStep( 'material_id' => $materialId, 'material_type' => $materialType, 'text' => $text, + 'player_slide_id' => $structure->slideId, 'retry_step_id' => null, 'require_perfect_score' => false, ]); diff --git a/src/Services/CommonCartridge/ScormPackageStorage.php b/src/Services/CommonCartridge/ScormPackageStorage.php index 80bdb2c..5b55694 100644 --- a/src/Services/CommonCartridge/ScormPackageStorage.php +++ b/src/Services/CommonCartridge/ScormPackageStorage.php @@ -5,6 +5,7 @@ namespace Tapp\FilamentLms\Services\CommonCartridge; use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use RuntimeException; @@ -24,7 +25,7 @@ public function retainPackage(string $extractedPath): ?array $packageId = Str::uuid()->toString(); $relativePath = mb_rtrim($packagesDirectory, '/').'/'.$packageId; - $destination = storage_path('app/'.$relativePath); + $destination = Storage::disk($disk)->path($relativePath); if (! is_dir(dirname($destination))) { mkdir(dirname($destination), 0755, true); } diff --git a/src/Services/ScormProgressService.php b/src/Services/ScormProgressService.php new file mode 100644 index 0000000..a97b189 --- /dev/null +++ b/src/Services/ScormProgressService.php @@ -0,0 +1,275 @@ +recordStarted($course, $user); + + $location = $payload['lesson_location'] ?? null; + if (is_string($location) && $location !== '') { + $this->markPlayerProgress($course, $user); + $this->completeStepByLocation($course, $user, $location); + } + + $suspendData = $payload['suspend_data'] ?? null; + if (is_string($suspendData) && $suspendData !== '') { + $this->markPlayerProgress($course, $user); + $this->completeStepBySuspendData($course, $user, $suspendData); + } + + if (! empty($payload['html5_progress'])) { + $this->markPlayerProgress($course, $user); + } + + $status = mb_strtolower((string) ($payload['lesson_status'] ?? '')); + if (in_array($status, ['completed', 'passed'], true)) { + $this->completeAllEligibleSteps($course, $user); + } + } + + /** + * @return array{ok: bool, message: string} + */ + public function attemptManualCourseCompletion(Course $course, Authenticatable $user): array + { + if ($this->courseCompletedByUser($course, $user)) { + return ['ok' => true, 'message' => 'Course already completed.']; + } + + if (! $this->userMayConfirmCourseCompletion($course, $user)) { + return [ + 'ok' => false, + 'message' => 'Continue the course in the player before marking it complete. Progress in the LMS is recorded when you move through content in the player, or after sufficient time spent in the course.', + ]; + } + + $this->completeAllEligibleSteps($course, $user); + + return ['ok' => true, 'message' => 'Course marked complete.']; + } + + public function userMayConfirmCourseCompletion(Course $course, Authenticatable $user): bool + { + if (! $course->isEmbeddedPlayer()) { + return true; + } + + if ($this->courseCompletedByUser($course, $user)) { + return true; + } + + if (! $this->courseStartedByUser($course, $user)) { + return false; + } + + if ($this->countCompletedNonLaunchSteps($course, $user) >= 1) { + return true; + } + + if ($this->countCompletedEligibleSteps($course, $user) >= 2) { + return true; + } + + if ($this->hasRecordedPlayerProgress($course, $user) && $this->meetsMinimumSessionDuration($course, $user)) { + return true; + } + + return false; + } + + public function recordStarted(Course $course, Authenticatable $user): void + { + $launchStep = $course->launchStep(); + if ($launchStep === null) { + return; + } + + $userStep = StepUser::query()->firstOrCreate([ + 'user_id' => $user->getAuthIdentifier(), + 'step_id' => $launchStep->id, + ]); + + if ($launchStep->first_step && $userStep->wasRecentlyCreated) { + CourseStarted::dispatch($user, $course); + } + } + + public function markPlayerProgress(Course $course, Authenticatable $user): void + { + Cache::put( + $this->playerProgressCacheKey($course, $user), + true, + now()->addDays(7), + ); + } + + public function hasRecordedPlayerProgress(Course $course, Authenticatable $user): bool + { + return Cache::get($this->playerProgressCacheKey($course, $user), false) === true; + } + + public function completeAllEligibleSteps(Course $course, Authenticatable $user): void + { + $course->loadMissing(['lessons.steps']); + + foreach ($this->eligibleStepsForBulkComplete($course) as $step) { + $step->complete($user); + } + } + + public function completeStepByLocation(Course $course, Authenticatable $user, string $location): void + { + $step = $this->findStepByPlayerReference($course, $location); + if ($step !== null) { + $step->complete($user); + } + } + + public function completeStepBySuspendData(Course $course, Authenticatable $user, string $suspendData): void + { + foreach ($course->steps()->whereNotNull('player_slide_id')->pluck('player_slide_id') as $slideId) { + if ($slideId !== '' && str_contains($suspendData, (string) $slideId)) { + $step = $course->steps()->where('player_slide_id', $slideId)->first(); + if ($step instanceof Step) { + $step->complete($user); + } + } + } + } + + public function courseStartedByUser(Course $course, Authenticatable $user): bool + { + $launchStep = $course->launchStep(); + if ($launchStep === null) { + return false; + } + + return StepUser::query() + ->where('user_id', $user->getAuthIdentifier()) + ->where('step_id', $launchStep->id) + ->exists(); + } + + public function courseCompletedByUser(Course $course, Authenticatable $user): bool + { + $eligible = $this->eligibleStepsForBulkComplete($course); + if ($eligible->isEmpty()) { + return false; + } + + $completedCount = StepUser::query() + ->where('user_id', $user->getAuthIdentifier()) + ->whereIn('step_id', $eligible->pluck('id')) + ->whereNotNull('completed_at') + ->count(); + + return $completedCount >= $eligible->count(); + } + + public function meetsMinimumSessionDuration(Course $course, Authenticatable $user): bool + { + $launchStep = $course->launchStep(); + if ($launchStep === null) { + return false; + } + + $userStep = StepUser::query() + ->where('user_id', $user->getAuthIdentifier()) + ->where('step_id', $launchStep->id) + ->first(); + + if ($userStep === null) { + return false; + } + + $minimumSeconds = $course->completionMode() === CompletionMode::Html5 + ? (int) config('filament-lms.embedded_player_min_session_seconds_html5', 300) + : (int) config('filament-lms.embedded_player_min_session_seconds', 90); + + return $userStep->created_at !== null + && $userStep->created_at->diffInSeconds(now()) >= $minimumSeconds; + } + + /** + * @return Collection + */ + private function eligibleStepsForBulkComplete(Course $course): Collection + { + return $course->steps->filter(fn (Step $step): bool => $step->material_type !== 'test'); + } + + private function countCompletedEligibleSteps(Course $course, Authenticatable $user): int + { + $stepIds = $this->eligibleStepsForBulkComplete($course)->pluck('id'); + + return StepUser::query() + ->where('user_id', $user->getAuthIdentifier()) + ->whereIn('step_id', $stepIds) + ->whereNotNull('completed_at') + ->count(); + } + + private function countCompletedNonLaunchSteps(Course $course, Authenticatable $user): int + { + $launchStep = $course->launchStep(); + if ($launchStep === null) { + return 0; + } + + $stepIds = $this->eligibleStepsForBulkComplete($course) + ->reject(fn (Step $step): bool => $step->is($launchStep)) + ->pluck('id'); + + if ($stepIds->isEmpty()) { + return 0; + } + + return StepUser::query() + ->where('user_id', $user->getAuthIdentifier()) + ->whereIn('step_id', $stepIds) + ->whereNotNull('completed_at') + ->count(); + } + + private function playerProgressCacheKey(Course $course, Authenticatable $user): string + { + return 'lms_player_progress_'.$course->id.'_'.$user->getAuthIdentifier(); + } + + private function findStepByPlayerReference(Course $course, string $location): ?Step + { + $location = trim($location); + if ($location === '') { + return null; + } + + $exact = $course->steps()->where('player_slide_id', $location)->first(); + if ($exact instanceof Step) { + return $exact; + } + + return $course->steps() + ->whereNotNull('player_slide_id') + ->get() + ->first(fn (Step $step): bool => $step->player_slide_id !== null + && (str_contains($location, $step->player_slide_id) + || str_contains($step->player_slide_id, $location))); + } +} diff --git a/src/Traits/FilamentLmsUser.php b/src/Traits/FilamentLmsUser.php index d3d2ad3..453bc64 100644 --- a/src/Traits/FilamentLmsUser.php +++ b/src/Traits/FilamentLmsUser.php @@ -80,6 +80,13 @@ public function getCourseProgress(Course $course): float */ public function canAccessStep(Step $step): bool { + $course = $step->lesson->course; + if ($course->isEmbeddedPlayer()) { + $launchStep = $course->launchStep(); + + return $launchStep !== null && $launchStep->is($step); + } + // Default implementation: check if previous steps are completed // Use the protected method directly to avoid circular dependency with available attribute return $step->checkPreviousStepsCompleted(); diff --git a/tests/Feature/CommonCartridgeImportTest.php b/tests/Feature/CommonCartridgeImportTest.php index 34d2c9a..1836d90 100644 --- a/tests/Feature/CommonCartridgeImportTest.php +++ b/tests/Feature/CommonCartridgeImportTest.php @@ -43,6 +43,16 @@ expect($resource->href)->toBe('index_lms.html'); }); +test('import service uniquifies duplicate course names', function () { + $fixturePath = realpath(__DIR__.'/../fixtures/common-cartridge'); + $service = new CommonCartridgeImportService(new ManifestParser, new ScormPackageStorage); + + $service->import($fixturePath); + $second = $service->import($fixturePath); + + expect($second['course']->name)->toBe('Child Outcomes Summary-1'); +}); + test('import service creates course lesson step and document from fixture', function () { $fixturePath = realpath(__DIR__.'/../fixtures/common-cartridge'); expect($fixturePath)->not->toBeFalse(); diff --git a/tests/Feature/EmbeddedPlayerTest.php b/tests/Feature/EmbeddedPlayerTest.php new file mode 100644 index 0000000..874e11b --- /dev/null +++ b/tests/Feature/EmbeddedPlayerTest.php @@ -0,0 +1,358 @@ +create([ + 'embedded_player' => true, + 'completion_mode' => CompletionMode::Scorm12, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $document = Document::query()->create([ + 'name' => 'Player', + 'package_disk' => 'local', + 'package_path' => 'lms-scorm-packages/'.Str::uuid(), + 'package_launch_path' => 'index.html', + ]); + $launchStep = Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 0, + 'material_type' => 'document', + 'material_id' => $document->id, + ]); + Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 1, + 'name' => 'Later', + ]); + + expect($course->launchStep()?->is($launchStep))->toBeTrue(); +}); + +test('scorm commit marks step by player slide id and bulk completes on passed status', function () { + config(['filament-lms.user_model' => TestUser::class]); + + $user = TestUser::query()->create([ + 'name' => 'Learner', + 'first_name' => 'Learner', + 'last_name' => 'User', + 'email' => 'scorm-commit@example.com', + 'password' => bcrypt('password'), + ]); + + $course = Course::factory()->create([ + 'embedded_player' => true, + 'completion_mode' => CompletionMode::Scorm12, + 'is_private' => false, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $slideId = 'Slide_abc123'; + $stepWithSlide = Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 0, + 'player_slide_id' => $slideId, + 'material_type' => null, + 'material_id' => null, + ]); + $otherStep = Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 1, + 'material_type' => null, + 'material_id' => null, + ]); + + $this->actingAs($user); + + $this->postJson(route('filament-lms.scorm-commit.store', ['course' => $course]), [ + 'lesson_location' => $slideId, + 'lesson_status' => 'incomplete', + ])->assertSuccessful(); + + expect(StepUser::query() + ->where('user_id', $user->id) + ->where('step_id', $stepWithSlide->id) + ->whereNotNull('completed_at') + ->exists())->toBeTrue(); + + $this->postJson(route('filament-lms.scorm-commit.store', ['course' => $course]), [ + 'lesson_status' => 'passed', + ])->assertSuccessful(); + + expect(StepUser::query() + ->where('user_id', $user->id) + ->where('step_id', $otherStep->id) + ->whereNotNull('completed_at') + ->exists())->toBeTrue(); +}); + +test('scorm commit does not bulk complete test steps', function () { + config(['filament-lms.user_model' => TestUser::class]); + + $user = TestUser::query()->create([ + 'name' => 'Learner', + 'first_name' => 'Learner', + 'last_name' => 'User', + 'email' => 'scorm-skip-test@example.com', + 'password' => bcrypt('password'), + ]); + + $course = Course::factory()->create([ + 'embedded_player' => true, + 'completion_mode' => CompletionMode::Scorm12, + 'is_private' => false, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $test = Test::query()->create(['name' => 'Quiz']); + $testStep = Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 0, + 'material_type' => 'test', + 'material_id' => $test->id, + ]); + + $this->actingAs($user); + + $this->postJson(route('filament-lms.scorm-commit.store', ['course' => $course]), [ + 'lesson_status' => 'completed', + ])->assertSuccessful(); + + expect(StepUser::query() + ->where('user_id', $user->id) + ->where('step_id', $testStep->id) + ->whereNotNull('completed_at') + ->exists())->toBeFalse(); +}); + +test('html5 record started creates launch step progress row', function () { + config(['filament-lms.user_model' => TestUser::class]); + + $user = TestUser::query()->create([ + 'name' => 'Learner', + 'first_name' => 'Learner', + 'last_name' => 'User', + 'email' => 'html5-start@example.com', + 'password' => bcrypt('password'), + ]); + + $course = Course::factory()->create([ + 'embedded_player' => true, + 'completion_mode' => CompletionMode::Html5, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $document = Document::query()->create([ + 'name' => 'Player', + 'package_disk' => 'local', + 'package_path' => 'lms-scorm-packages/'.Str::uuid(), + 'package_launch_path' => 'index.html', + ]); + $launchStep = Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 0, + 'material_type' => 'document', + 'material_id' => $document->id, + ]); + + app(ScormProgressService::class)->recordStarted($course, $user); + + expect(StepUser::query() + ->where('user_id', $user->id) + ->where('step_id', $launchStep->id) + ->exists())->toBeTrue(); +}); + +test('html5 manual complete is rejected without meaningful progress', function () { + config(['filament-lms.user_model' => TestUser::class]); + + $user = TestUser::query()->create([ + 'name' => 'Learner', + 'first_name' => 'Learner', + 'last_name' => 'User', + 'email' => 'html5-guard@example.com', + 'password' => bcrypt('password'), + ]); + + $course = Course::factory()->create([ + 'embedded_player' => true, + 'completion_mode' => CompletionMode::Html5, + 'is_private' => false, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $document = Document::query()->create([ + 'name' => 'Player', + 'package_disk' => 'local', + 'package_path' => 'lms-scorm-packages/'.Str::uuid(), + 'package_launch_path' => 'index.html', + ]); + Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 0, + 'material_type' => 'document', + 'material_id' => $document->id, + ]); + Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 1, + ]); + + app(ScormProgressService::class)->recordStarted($course, $user); + + $this->actingAs($user); + + $this->postJson(route('filament-lms.scorm-commit.store', ['course' => $course]), [ + 'html5_complete' => true, + ])->assertUnprocessable() + ->assertJsonPath('ok', false); + + expect(app(ScormProgressService::class)->userMayConfirmCourseCompletion($course, $user))->toBeFalse(); +}); + +test('html5 manual complete is allowed after player progress and minimum session', function () { + config([ + 'filament-lms.user_model' => TestUser::class, + 'filament-lms.embedded_player_min_session_seconds_html5' => 1, + ]); + + $user = TestUser::query()->create([ + 'name' => 'Learner', + 'first_name' => 'Learner', + 'last_name' => 'User', + 'email' => 'html5-allow@example.com', + 'password' => bcrypt('password'), + ]); + + $course = Course::factory()->create([ + 'embedded_player' => true, + 'completion_mode' => CompletionMode::Html5, + 'is_private' => false, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $document = Document::query()->create([ + 'name' => 'Player', + 'package_disk' => 'local', + 'package_path' => 'lms-scorm-packages/'.Str::uuid(), + 'package_launch_path' => 'index.html', + ]); + $launchStep = Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 0, + 'material_type' => 'document', + 'material_id' => $document->id, + ]); + $otherStep = Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 1, + ]); + + $service = app(ScormProgressService::class); + $service->recordStarted($course, $user); + $service->markPlayerProgress($course, $user); + + StepUser::query() + ->where('user_id', $user->id) + ->where('step_id', $launchStep->id) + ->update(['created_at' => now()->subMinutes(10)]); + + $this->actingAs($user); + + expect($service->userMayConfirmCourseCompletion($course, $user))->toBeTrue(); + + $this->postJson(route('filament-lms.scorm-commit.store', ['course' => $course]), [ + 'html5_complete' => true, + ])->assertSuccessful() + ->assertJsonPath('ok', true); + + expect(StepUser::query() + ->where('user_id', $user->id) + ->where('step_id', $otherStep->id) + ->whereNotNull('completed_at') + ->exists())->toBeTrue(); +}); + +test('html5 progress commit records player activity', function () { + config(['filament-lms.user_model' => TestUser::class]); + + $user = TestUser::query()->create([ + 'name' => 'Learner', + 'first_name' => 'Learner', + 'last_name' => 'User', + 'email' => 'html5-progress@example.com', + 'password' => bcrypt('password'), + ]); + + $course = Course::factory()->create([ + 'embedded_player' => true, + 'completion_mode' => CompletionMode::Html5, + 'is_private' => false, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $document = Document::query()->create([ + 'name' => 'Player', + 'package_disk' => 'local', + 'package_path' => 'lms-scorm-packages/'.Str::uuid(), + 'package_launch_path' => 'index.html', + ]); + Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 0, + 'material_type' => 'document', + 'material_id' => $document->id, + ]); + + app(ScormProgressService::class)->recordStarted($course, $user); + + $this->actingAs($user); + + $this->postJson(route('filament-lms.scorm-commit.store', ['course' => $course]), [ + 'html5_progress' => true, + ])->assertSuccessful(); + + expect(app(ScormProgressService::class)->hasRecordedPlayerProgress($course, $user))->toBeTrue(); +}); + +test('only launch step is accessible on embedded courses', function () { + config(['filament-lms.user_model' => TestUser::class]); + + $user = TestUser::query()->create([ + 'name' => 'Learner', + 'first_name' => 'Learner', + 'last_name' => 'User', + 'email' => 'embedded-access@example.com', + 'password' => bcrypt('password'), + ]); + + $course = Course::factory()->create([ + 'embedded_player' => true, + 'completion_mode' => CompletionMode::Html5, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $document = Document::query()->create([ + 'name' => 'Player', + 'package_disk' => 'local', + 'package_path' => 'lms-scorm-packages/'.Str::uuid(), + 'package_launch_path' => 'index.html', + ]); + $launchStep = Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 0, + 'material_type' => 'document', + 'material_id' => $document->id, + ]); + $otherStep = Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'order' => 1, + ]); + + expect($user->canAccessStep($launchStep))->toBeTrue(); + expect($user->canAccessStep($otherStep))->toBeFalse(); +}); diff --git a/tests/Feature/ScormPackageServingTest.php b/tests/Feature/ScormPackageServingTest.php index d968e18..d3bc08b 100644 --- a/tests/Feature/ScormPackageServingTest.php +++ b/tests/Feature/ScormPackageServingTest.php @@ -2,7 +2,11 @@ declare(strict_types=1); +use Tapp\FilamentLms\Enums\CompletionMode; +use Tapp\FilamentLms\Models\Course; use Tapp\FilamentLms\Models\Document; +use Tapp\FilamentLms\Models\Lesson; +use Tapp\FilamentLms\Models\Step; use Tapp\FilamentLms\Services\CommonCartridge\CommonCartridgeImportService; use Tapp\FilamentLms\Services\CommonCartridge\ManifestParser; use Tapp\FilamentLms\Services\CommonCartridge\ScormPackageStorage; @@ -39,3 +43,169 @@ $response->assertSuccessful(); }); + +test('serves scorm package stored under legacy storage path when local disk uses private root', function () { + config([ + 'filament-lms.user_model' => TestUser::class, + 'filesystems.disks.local.root' => storage_path('app/private'), + ]); + + $user = TestUser::query()->create([ + 'name' => 'Learner', + 'first_name' => 'Learner', + 'last_name' => 'User', + 'email' => 'legacy-learner@example.com', + 'password' => bcrypt('password'), + ]); + + $packageId = (string) \Illuminate\Support\Str::uuid(); + $relativePath = 'lms-scorm-packages/'.$packageId; + $legacyRoot = storage_path('app/'.$relativePath); + if (! is_dir($legacyRoot)) { + mkdir($legacyRoot, 0755, true); + } + file_put_contents($legacyRoot.'/index.html', 'Legacy package'); + + $course = Course::factory()->create([ + 'name' => 'Legacy SCORM Course', + 'slug' => 'legacy-scorm-course', + 'is_private' => false, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $document = Document::query()->create([ + 'name' => 'Home', + 'package_disk' => 'local', + 'package_path' => $relativePath, + 'package_launch_path' => 'index.html', + ]); + Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'material_type' => 'document', + 'material_id' => $document->id, + ]); + + $this->actingAs($user); + + $response = $this->get(route('filament-lms.scorm-package.show', [ + 'document' => $document->id, + 'entry' => 'index.html', + ])); + + $response->assertSuccessful(); + expect($response->headers->get('content-type'))->toContain('text/html'); +}); + +test('serves nested package assets via path-based urls', function () { + config([ + 'filament-lms.user_model' => TestUser::class, + ]); + + $user = TestUser::query()->create([ + 'name' => 'Learner', + 'first_name' => 'Learner', + 'last_name' => 'User', + 'email' => 'nested-assets@example.com', + 'password' => bcrypt('password'), + ]); + + $packageId = (string) \Illuminate\Support\Str::uuid(); + $relativePath = 'lms-scorm-packages/'.$packageId; + $packageRoot = storage_path('app/'.$relativePath); + if (! is_dir($packageRoot)) { + mkdir($packageRoot, 0755, true); + } + mkdir($packageRoot.'/story_content', 0755, true); + file_put_contents($packageRoot.'/index.html', ''); + file_put_contents($packageRoot.'/story_content/triggers.js', 'window.triggersLoaded = true;'); + + $course = Course::factory()->create([ + 'name' => 'Nested Assets Course', + 'slug' => 'nested-assets-course', + 'is_private' => false, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $document = Document::query()->create([ + 'name' => 'Home', + 'package_disk' => 'local', + 'package_path' => $relativePath, + 'package_launch_path' => 'index.html', + ]); + Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'material_type' => 'document', + 'material_id' => $document->id, + ]); + + $this->actingAs($user); + + $this->get(route('filament-lms.scorm-package.show', [ + 'document' => $document->id, + 'entry' => 'index.html', + ]))->assertSuccessful(); + + $launchUrl = route('filament-lms.scorm-package.show', [ + 'document' => $document->id, + 'entry' => 'index.html', + ]); + expect($launchUrl)->toContain('/lms/scorm-package/'.$document->id.'/index.html'); + expect($launchUrl)->not->toContain('?entry='); + + $this->get(route('filament-lms.scorm-package.show', [ + 'document' => $document->id, + 'entry' => 'story_content/triggers.js', + ])) + ->assertSuccessful() + ->assertHeader('content-type', 'application/javascript'); +}); + +test('serves html5 package index with injected bridge for embedded player courses', function () { + config([ + 'filament-lms.user_model' => TestUser::class, + ]); + + $user = TestUser::query()->create([ + 'name' => 'Learner', + 'first_name' => 'Learner', + 'last_name' => 'User', + 'email' => 'html5-bridge@example.com', + 'password' => bcrypt('password'), + ]); + + $packageId = (string) \Illuminate\Support\Str::uuid(); + $relativePath = 'lms-scorm-packages/'.$packageId; + $packageRoot = storage_path('app/'.$relativePath); + if (! is_dir($packageRoot)) { + mkdir($packageRoot, 0755, true); + } + file_put_contents($packageRoot.'/index.html', 'Player'); + + $course = Course::factory()->create([ + 'name' => 'HTML5 Bridge Course', + 'slug' => 'html5-bridge-course', + 'is_private' => false, + 'embedded_player' => true, + 'completion_mode' => CompletionMode::Html5, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $document = Document::query()->create([ + 'name' => 'Home', + 'package_disk' => 'local', + 'package_path' => $relativePath, + 'package_launch_path' => 'index.html', + ]); + Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'material_type' => 'document', + 'material_id' => $document->id, + ]); + + $this->actingAs($user); + + $response = $this->get(route('filament-lms.scorm-package.show', [ + 'document' => $document->id, + 'entry' => 'index.html', + ])); + + $response->assertSuccessful(); + expect($response->getContent())->toContain('lms-html5-progress'); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index e76c47c..985e820 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -105,6 +105,8 @@ protected function setUpDatabase($app) $table->text('description')->nullable(); $table->unsignedTinyInteger('required_test_percentage')->nullable(); $table->boolean('is_private')->default(false); + $table->boolean('embedded_player')->default(false); + $table->string('completion_mode', 32)->default('native'); $table->timestamps(); $table->softDeletes(); }); @@ -131,6 +133,7 @@ protected function setUpDatabase($app) $table->unsignedBigInteger('material_id')->nullable(); $table->string('material_type')->nullable(); $table->text('text')->nullable(); + $table->string('player_slide_id')->nullable(); $table->foreignId('retry_step_id')->nullable()->constrained('lms_steps')->onDelete('set null'); $table->boolean('require_perfect_score')->default(false); $table->timestamps(); From bfd8f1d125614e7d9b37bdc4f4db810b2998f428 Mon Sep 17 00:00:00 2001 From: swilla <304159+swilla@users.noreply.github.com> Date: Fri, 22 May 2026 19:21:19 +0000 Subject: [PATCH 05/16] Fix styling --- tests/Feature/ScormPackageServingTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Feature/ScormPackageServingTest.php b/tests/Feature/ScormPackageServingTest.php index d3bc08b..a9938ed 100644 --- a/tests/Feature/ScormPackageServingTest.php +++ b/tests/Feature/ScormPackageServingTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Illuminate\Support\Str; use Tapp\FilamentLms\Enums\CompletionMode; use Tapp\FilamentLms\Models\Course; use Tapp\FilamentLms\Models\Document; @@ -58,7 +59,7 @@ 'password' => bcrypt('password'), ]); - $packageId = (string) \Illuminate\Support\Str::uuid(); + $packageId = (string) Str::uuid(); $relativePath = 'lms-scorm-packages/'.$packageId; $legacyRoot = storage_path('app/'.$relativePath); if (! is_dir($legacyRoot)) { @@ -108,7 +109,7 @@ 'password' => bcrypt('password'), ]); - $packageId = (string) \Illuminate\Support\Str::uuid(); + $packageId = (string) Str::uuid(); $relativePath = 'lms-scorm-packages/'.$packageId; $packageRoot = storage_path('app/'.$relativePath); if (! is_dir($packageRoot)) { @@ -171,7 +172,7 @@ 'password' => bcrypt('password'), ]); - $packageId = (string) \Illuminate\Support\Str::uuid(); + $packageId = (string) Str::uuid(); $relativePath = 'lms-scorm-packages/'.$packageId; $packageRoot = storage_path('app/'.$relativePath); if (! is_dir($packageRoot)) { From 6f040bb1a52a394d1c1716949000f6a49cb85c0a Mon Sep 17 00:00:00 2001 From: Steve Williamson Date: Thu, 28 May 2026 12:44:47 -0400 Subject: [PATCH 06/16] Fix common cartridge import review issues --- src/Jobs/ImportCommonCartridgeJob.php | 39 +++++++++------ .../ArticulateSlideContentExtractor.php | 21 ++++++-- .../ArticulateSlideContentExtractorTest.php | 48 +++++++++++++++++++ tests/Unit/ImportCommonCartridgeJobTest.php | 37 ++++++++++++++ 4 files changed, 127 insertions(+), 18 deletions(-) create mode 100644 tests/Unit/ImportCommonCartridgeJobTest.php diff --git a/src/Jobs/ImportCommonCartridgeJob.php b/src/Jobs/ImportCommonCartridgeJob.php index 17137c0..5c9904d 100644 --- a/src/Jobs/ImportCommonCartridgeJob.php +++ b/src/Jobs/ImportCommonCartridgeJob.php @@ -35,13 +35,14 @@ public function __construct( public function handle(CommonCartridgeImportService $importService): void { $extractPath = null; + $tempRootPath = null; try { if (! is_file($this->storedPath)) { throw new RuntimeException('Stored import file not found: '.$this->storedPath); } - $extractPath = $this->extractZip($this->storedPath); + $extractPath = $this->extractZip($this->storedPath, $tempRootPath); $root = mb_rtrim($extractPath, '/'); Log::channel('single')->info('CC import: package root', [ @@ -49,7 +50,6 @@ public function handle(CommonCartridgeImportService $importService): void 'package_root' => $root, 'imsmanifest_exists' => is_file($root.'/imsmanifest.xml'), 'frame_xml_exists' => is_file($root.'/story_content/frame.xml'), - 'sample_slide_js_exists' => is_file($root.'/html5/data/js/6FA6ZHMtWms.js'), ]); $result = $importService->import($extractPath, $this->tenantId); @@ -80,15 +80,17 @@ public function handle(CommonCartridgeImportService $importService): void throw $e; } finally { - if ($extractPath !== null && is_dir($extractPath)) { - $this->deleteDirectory($extractPath); + if ($tempRootPath !== null && is_dir($tempRootPath)) { + $this->deleteDirectory($tempRootPath); } } } - private function extractZip(string $zipPath): string + private function extractZip(string $zipPath, ?string &$tempRootPath = null): string { $extractPath = storage_path('app/temp/cc-import-'.Str::uuid()->toString()); + $tempRootPath = $extractPath; + if (! is_dir(dirname($extractPath))) { mkdir(dirname($extractPath), 0755, true); } @@ -104,31 +106,38 @@ private function extractZip(string $zipPath): string } /** - * When the zip has a single root directory (e.g. "COS_SCORM12") that contains imsmanifest.xml, - * use that subdirectory as the package root so the parser finds frame.xml and html5/data/js. + * When the zip has a single root directory (e.g. "COS_SCORM12") that contains package markers, + * use that subdirectory as the package root so the parser finds imsmanifest.xml or frame.xml. */ private function normalizePackageRoot(string $extractPath): string { - $manifestPath = mb_rtrim($extractPath, '/').'/imsmanifest.xml'; - if (is_file($manifestPath)) { - return mb_rtrim($extractPath, '/'); + $root = mb_rtrim($extractPath, '/'); + if ($this->hasPackageRootMarkers($root)) { + return $root; } + $entries = @scandir($extractPath); if ($entries === false) { - return mb_rtrim($extractPath, '/'); + return $root; } $dirs = array_filter($entries, function ($name) use ($extractPath) { return $name !== '.' && $name !== '..' && is_dir(mb_rtrim($extractPath, '/').'/'.$name); }); if (count($dirs) === 1) { - $subDir = mb_rtrim($extractPath, '/').'/'.reset($dirs); - $nestedManifest = $subDir.'/imsmanifest.xml'; - if (is_file($nestedManifest)) { + $subDir = $root.'/'.reset($dirs); + if ($this->hasPackageRootMarkers($subDir)) { return $subDir; } } - return mb_rtrim($extractPath, '/'); + return $root; + } + + private function hasPackageRootMarkers(string $path): bool + { + $root = mb_rtrim($path, '/'); + + return is_file($root.'/imsmanifest.xml') || is_file($root.'/story_content/frame.xml'); } private function deleteDirectory(string $path): void diff --git a/src/Services/CommonCartridge/ArticulateSlideContentExtractor.php b/src/Services/CommonCartridge/ArticulateSlideContentExtractor.php index c294431..8fbe713 100644 --- a/src/Services/CommonCartridge/ArticulateSlideContentExtractor.php +++ b/src/Services/CommonCartridge/ArticulateSlideContentExtractor.php @@ -254,7 +254,7 @@ private function extractJsonWithDoubleQuotePattern(string $content): ?string } $start += strlen($needle); $len = strlen($content); - $end = $start; + $end = null; for ($i = $start; $i < $len; $i++) { if ($content[$i] === '"') { $backslashes = 0; @@ -265,12 +265,23 @@ private function extractJsonWithDoubleQuotePattern(string $content): ?string } if ($backslashes % 2 === 0) { $end = $i; + break; } } } + if ($end === null) { + return null; + } $jsonStr = substr($content, $start, $end - $start); + if ($jsonStr === '') { + return null; + } + $placeholder = "\x00"; + $jsonStr = str_replace('\\\\\"', $placeholder, $jsonStr); + $jsonStr = str_replace('\\"', '"', $jsonStr); + $jsonStr = str_replace($placeholder, '\\\\\"', $jsonStr); - return $jsonStr !== '' ? $jsonStr : null; + return $jsonStr; } /** @@ -285,7 +296,7 @@ private function extractJsonWithSingleQuotePattern(string $content): ?string } $start += strlen($needle); $len = strlen($content); - $end = $start; + $end = null; for ($i = $start; $i < $len; $i++) { if ($content[$i] === "'") { $backslashes = 0; @@ -296,9 +307,13 @@ private function extractJsonWithSingleQuotePattern(string $content): ?string } if ($backslashes % 2 === 0) { $end = $i; + break; } } } + if ($end === null) { + return null; + } $jsonStr = substr($content, $start, $end - $start); if ($jsonStr === '') { return null; diff --git a/tests/Unit/ArticulateSlideContentExtractorTest.php b/tests/Unit/ArticulateSlideContentExtractorTest.php index 4966739..a659443 100644 --- a/tests/Unit/ArticulateSlideContentExtractorTest.php +++ b/tests/Unit/ArticulateSlideContentExtractorTest.php @@ -102,6 +102,54 @@ @rmdir($tmp); }); +test('getSlideData ignores content after double quoted slide payload', function () { + $tmp = sys_get_temp_dir().'/articulate-test-'.uniqid(); + mkdir($tmp, 0755, true); + mkdir($tmp.'/html5/data/js', 0755, true); + + $slideId = 'DoubleQuotedSlide'; + $json = ['title' => 'Double quoted', 'slideLayers' => []]; + $rawJson = json_encode($json); + $escaped = str_replace(['\\', '"'], ['\\\\', '\\"'], $rawJson); + $jsContent = 'window.globalProvideData("slide", "'.$escaped.'"); console.log("later quoted content");'; + file_put_contents($tmp.'/html5/data/js/'.$slideId.'.js', $jsContent); + + $data = $this->extractor->getSlideData($tmp, $slideId); + + expect($data)->toBeArray(); + expect($this->extractor->getSlideTitle($data ?? []))->toBe('Double quoted'); + + unlink($tmp.'/html5/data/js/'.$slideId.'.js'); + @rmdir($tmp.'/html5/data/js'); + @rmdir($tmp.'/html5/data'); + @rmdir($tmp.'/html5'); + @rmdir($tmp); +}); + +test('getSlideData ignores content after single quoted slide payload', function () { + $tmp = sys_get_temp_dir().'/articulate-test-'.uniqid(); + mkdir($tmp, 0755, true); + mkdir($tmp.'/html5/data/js', 0755, true); + + $slideId = 'SingleQuotedSlide'; + $json = ['title' => 'Single quoted', 'slideLayers' => []]; + $rawJson = json_encode($json); + $escaped = str_replace(['\\', "'"], ['\\\\', "\\'"], $rawJson); + $jsContent = "window.globalProvideData('slide', '".$escaped."'); console.log('later quoted content');"; + file_put_contents($tmp.'/html5/data/js/'.$slideId.'.js', $jsContent); + + $data = $this->extractor->getSlideData($tmp, $slideId); + + expect($data)->toBeArray(); + expect($this->extractor->getSlideTitle($data ?? []))->toBe('Single quoted'); + + unlink($tmp.'/html5/data/js/'.$slideId.'.js'); + @rmdir($tmp.'/html5/data/js'); + @rmdir($tmp.'/html5/data'); + @rmdir($tmp.'/html5'); + @rmdir($tmp); +}); + test('extractFromSlideData builds html from slide data', function () { $data = [ 'slideLayers' => [ diff --git a/tests/Unit/ImportCommonCartridgeJobTest.php b/tests/Unit/ImportCommonCartridgeJobTest.php new file mode 100644 index 0000000..6da4065 --- /dev/null +++ b/tests/Unit/ImportCommonCartridgeJobTest.php @@ -0,0 +1,37 @@ +open($zipPath, ZipArchive::CREATE))->toBeTrue(); + $zip->addFromString('wrapped/index.html', 'Storyline'); + $zip->addFromString('wrapped/story_content/frame.xml', ''); + $zip->close(); + + $job = new ImportCommonCartridgeJob($zipPath, 1); + $method = new ReflectionMethod($job, 'extractZip'); + $method->setAccessible(true); + + $tempRootPath = null; + $packageRoot = $method->invokeArgs($job, [$zipPath, &$tempRootPath]); + + expect($tempRootPath)->not->toBeNull(); + expect($packageRoot)->toBe($tempRootPath.'/wrapped'); + expect(is_dir($tempRootPath))->toBeTrue(); + expect(is_file($packageRoot.'/story_content/frame.xml'))->toBeTrue(); + + $deleteDirectory = new ReflectionMethod($job, 'deleteDirectory'); + $deleteDirectory->setAccessible(true); + $deleteDirectory->invoke($job, $tempRootPath); + + expect(is_dir($tempRootPath))->toBeFalse(); + + @unlink($zipPath); +}); From c58a0b3a8d4e0470ff09885d63976ddce4892eee Mon Sep 17 00:00:00 2001 From: Steve Williamson Date: Thu, 28 May 2026 12:47:39 -0400 Subject: [PATCH 07/16] pao --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index b5b7a2f..64232c5 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "require-dev": { "filament/upgrade": "^4.0", "larastan/larastan": "^3.0||^2.9", + "laravel/pao": "^1.0", "laravel/pint": "^1.14", "nunomaduro/collision": "^8.1.1||^7.10.0", "orchestra/testbench": "^11.0.0||^10.0.0", From 7e1575e9931257137fe2e676604da37c16cecf5c Mon Sep 17 00:00:00 2001 From: Steve Williamson Date: Thu, 28 May 2026 12:53:48 -0400 Subject: [PATCH 08/16] Fix CI failures for import cartridge PR --- src/Exports/CourseProgressExport.php | 11 +++++----- src/Jobs/ImportCommonCartridgeJob.php | 6 ++--- src/Livewire/LmsTestFormShow.php | 8 ++++--- src/Models/Course.php | 4 +++- src/Models/Document.php | 5 +++++ src/Models/Step.php | 22 +++++++++++++++++++ src/Models/StepUser.php | 2 ++ .../ArticulateSlideContentExtractor.php | 6 ++--- .../CommonCartridgeImportService.php | 4 ++-- .../CommonCartridge/ManifestParser.php | 10 --------- src/Services/ScormProgressService.php | 4 +++- tests/TestCase.php | 1 + tests/Unit/ImportCommonCartridgeJobTest.php | 4 ++-- 13 files changed, 56 insertions(+), 31 deletions(-) diff --git a/src/Exports/CourseProgressExport.php b/src/Exports/CourseProgressExport.php index bc59780..d4a0bca 100644 --- a/src/Exports/CourseProgressExport.php +++ b/src/Exports/CourseProgressExport.php @@ -2,7 +2,8 @@ namespace Tapp\FilamentLms\Exports; -use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Support\Carbon; use Maatwebsite\Excel\Concerns\FromQuery; use Maatwebsite\Excel\Concerns\WithHeadings; @@ -11,16 +12,16 @@ class CourseProgressExport implements FromQuery, WithHeadings, WithMapping { - protected Builder $query; + protected EloquentBuilder|QueryBuilder $query; - public function __construct(Builder $query) + public function __construct(EloquentBuilder|QueryBuilder $query) { $this->query = $query; } - public function query() + public function query(): EloquentBuilder|QueryBuilder { - return CourseProgressQueryService::buildQuery(); + return $this->query; } public function headings(): array diff --git a/src/Jobs/ImportCommonCartridgeJob.php b/src/Jobs/ImportCommonCartridgeJob.php index 5c9904d..9b876be 100644 --- a/src/Jobs/ImportCommonCartridgeJob.php +++ b/src/Jobs/ImportCommonCartridgeJob.php @@ -35,7 +35,7 @@ public function __construct( public function handle(CommonCartridgeImportService $importService): void { $extractPath = null; - $tempRootPath = null; + $tempRootPath = ''; try { if (! is_file($this->storedPath)) { @@ -80,13 +80,13 @@ public function handle(CommonCartridgeImportService $importService): void throw $e; } finally { - if ($tempRootPath !== null && is_dir($tempRootPath)) { + if ($tempRootPath !== '' && is_dir($tempRootPath)) { $this->deleteDirectory($tempRootPath); } } } - private function extractZip(string $zipPath, ?string &$tempRootPath = null): string + private function extractZip(string $zipPath, string &$tempRootPath): string { $extractPath = storage_path('app/temp/cc-import-'.Str::uuid()->toString()); $tempRootPath = $extractPath; diff --git a/src/Livewire/LmsTestFormShow.php b/src/Livewire/LmsTestFormShow.php index 2509d50..ab95269 100644 --- a/src/Livewire/LmsTestFormShow.php +++ b/src/Livewire/LmsTestFormShow.php @@ -5,6 +5,8 @@ namespace Tapp\FilamentLms\Livewire; use Filament\Forms\Components\Radio; +use Filament\Forms\Components\RichEditor; +use Filament\Forms\Components\Select; use Illuminate\Support\HtmlString; use Tapp\FilamentFormBuilder\Enums\FilamentFieldTypeEnum; use Tapp\FilamentFormBuilder\Livewire\FilamentForm\Show; @@ -31,7 +33,7 @@ public function getFormSchema(): array $filamentField = $this->parseField($filamentField, $fieldData->toArray()); - if ($fieldData->type === FilamentFieldTypeEnum::SELECT_MULTIPLE) { + if ($fieldData->type === FilamentFieldTypeEnum::SELECT_MULTIPLE && $filamentField instanceof Select) { $filamentField = $filamentField ->multiple() ->live() @@ -80,11 +82,11 @@ public function getFormSchema(): array ->live(); } - if ($fieldData->type === FilamentFieldTypeEnum::RICH_EDITOR) { + if ($fieldData->type === FilamentFieldTypeEnum::RICH_EDITOR && $filamentField instanceof RichEditor) { $filamentField = $filamentField->disableToolbarButtons(['attachFiles']); } - if ($fieldData->type === FilamentFieldTypeEnum::SELECT) { + if ($fieldData->type === FilamentFieldTypeEnum::SELECT && $filamentField instanceof Radio) { $filamentField = $filamentField ->inline(false) ->extraFieldWrapperAttributes([ diff --git a/src/Models/Course.php b/src/Models/Course.php index 4133d29..38cd8f8 100644 --- a/src/Models/Course.php +++ b/src/Models/Course.php @@ -143,7 +143,9 @@ public function launchStep(): ?Step } } - return $this->steps()->first(); + $step = $this->steps()->first(); + + return $step instanceof Step ? $step : null; } public function linkToCurrentStep(): string diff --git a/src/Models/Document.php b/src/Models/Document.php index 59639af..96b2e88 100644 --- a/src/Models/Document.php +++ b/src/Models/Document.php @@ -12,6 +12,11 @@ use Tapp\FilamentLms\Models\Traits\BelongsToTenant; use Tapp\FilamentLms\Traits\HasMediaUrl; +/** + * @property string|null $package_disk + * @property string|null $package_path + * @property string|null $package_launch_path + */ class Document extends Model implements HasMedia { use BelongsToTenant; diff --git a/src/Models/Step.php b/src/Models/Step.php index 8e07f27..3c53dea 100644 --- a/src/Models/Step.php +++ b/src/Models/Step.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Str; use Spatie\EloquentSortable\Sortable; use Spatie\EloquentSortable\SortableTrait; use Tapp\FilamentLms\Contracts\FilamentLmsUserInterface; @@ -27,8 +28,10 @@ * @property string $name * @property string $slug * @property string $type + * @property string|null $text * @property int|null $material_id * @property string|null $material_type + * @property string|null $player_slide_id * @property Carbon|null $completed_at * @property Carbon $created_at * @property Carbon $updated_at @@ -78,6 +81,25 @@ public function retryStep(): BelongsTo return $this->belongsTo(Step::class, 'retry_step_id'); } + public function getRenderedText(): string + { + $text = mb_trim((string) $this->text); + if ($text === '') { + return ''; + } + + $html = str_starts_with(ltrim($text), '<') + ? $text + : Str::markdown($text); + + return $this->sanitizeRenderedText($html); + } + + private function sanitizeRenderedText(string $html): string + { + return preg_replace('#]*>.*?#is', '', $html) ?? ''; + } + public function complete($user = null) { // @phpstan-ignore-next-line diff --git a/src/Models/StepUser.php b/src/Models/StepUser.php index 6a834ff..9d5279c 100644 --- a/src/Models/StepUser.php +++ b/src/Models/StepUser.php @@ -6,10 +6,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Support\Carbon; use Tapp\FilamentLms\Models\Traits\BelongsToTenant; /** * @property string|null $completed_at + * @property Carbon|null $created_at * @property int|null $seconds */ class StepUser extends Pivot diff --git a/src/Services/CommonCartridge/ArticulateSlideContentExtractor.php b/src/Services/CommonCartridge/ArticulateSlideContentExtractor.php index 8fbe713..f456c8f 100644 --- a/src/Services/CommonCartridge/ArticulateSlideContentExtractor.php +++ b/src/Services/CommonCartridge/ArticulateSlideContentExtractor.php @@ -341,9 +341,7 @@ private function stripUtf8Bom(string $content): string private function sanitizeJsonForDecode(string $json): string { $sanitized = mb_convert_encoding($json, 'UTF-8', 'UTF-8'); - if ($sanitized !== false) { - $json = $sanitized; - } + $json = $sanitized; $json = preg_replace('/,\s*([}\]])/', '$1', $json) ?? $json; $json = $this->escapeLiteralNewlinesInsideJsonStrings($json); @@ -550,7 +548,7 @@ public function getAssessmentFirstQuestionAndOptions(array $data): ?array break; } } - if ($question === null || $question === '') { + if ($question === null) { return null; } diff --git a/src/Services/CommonCartridge/CommonCartridgeImportService.php b/src/Services/CommonCartridge/CommonCartridgeImportService.php index 9a099ef..dd26b52 100644 --- a/src/Services/CommonCartridge/CommonCartridgeImportService.php +++ b/src/Services/CommonCartridge/CommonCartridgeImportService.php @@ -272,10 +272,10 @@ private function createStep( } // Always persist slide-extracted text when present (text field is the source for step content). - $firstQuestionAndOptions = $isAssessment && $slideData !== null + $firstQuestionAndOptions = $isAssessment ? $extractor->getAssessmentFirstQuestionAndOptions($slideData) : null; - $formName = $firstQuestionAndOptions !== null && $firstQuestionAndOptions['question'] !== '' + $formName = $firstQuestionAndOptions !== null ? $firstQuestionAndOptions['question'] : ($structure->title !== '' ? $structure->title : 'Assessment'); diff --git a/src/Services/CommonCartridge/ManifestParser.php b/src/Services/CommonCartridge/ManifestParser.php index 66842c6..79ec097 100644 --- a/src/Services/CommonCartridge/ManifestParser.php +++ b/src/Services/CommonCartridge/ManifestParser.php @@ -316,16 +316,6 @@ private function organizationsElement(SimpleXMLElement $xml): ?SimpleXMLElement return $xml->organizations ?? null; } - private function resourcesElement(SimpleXMLElement $xml): ?SimpleXMLElement - { - $imscp = $xml->children(self::NS_IMSCP); - if (isset($imscp->resources)) { - return $imscp->resources; - } - - return $xml->resources ?? null; - } - private function getItemTitle(SimpleXMLElement $item): string { return $this->elementText($item, 'title'); diff --git a/src/Services/ScormProgressService.php b/src/Services/ScormProgressService.php index a97b189..d15481b 100644 --- a/src/Services/ScormProgressService.php +++ b/src/Services/ScormProgressService.php @@ -265,11 +265,13 @@ private function findStepByPlayerReference(Course $course, string $location): ?S return $exact; } - return $course->steps() + $matchingStep = $course->steps() ->whereNotNull('player_slide_id') ->get() ->first(fn (Step $step): bool => $step->player_slide_id !== null && (str_contains($location, $step->player_slide_id) || str_contains($step->player_slide_id, $location))); + + return $matchingStep instanceof Step ? $matchingStep : null; } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 985e820..aa7e5c8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -172,6 +172,7 @@ protected function setUpDatabase($app) $app['db']->connection()->getSchemaBuilder()->create('lms_documents', function (Blueprint $table) { $table->id(); $table->string('name'); + $table->string('file_path')->nullable(); $table->string('package_disk')->nullable(); $table->string('package_path')->nullable(); $table->string('package_launch_path')->nullable(); diff --git a/tests/Unit/ImportCommonCartridgeJobTest.php b/tests/Unit/ImportCommonCartridgeJobTest.php index 6da4065..90460b0 100644 --- a/tests/Unit/ImportCommonCartridgeJobTest.php +++ b/tests/Unit/ImportCommonCartridgeJobTest.php @@ -19,10 +19,10 @@ $method = new ReflectionMethod($job, 'extractZip'); $method->setAccessible(true); - $tempRootPath = null; + $tempRootPath = ''; $packageRoot = $method->invokeArgs($job, [$zipPath, &$tempRootPath]); - expect($tempRootPath)->not->toBeNull(); + expect($tempRootPath)->not->toBe(''); expect($packageRoot)->toBe($tempRootPath.'/wrapped'); expect(is_dir($tempRootPath))->toBeTrue(); expect(is_file($packageRoot.'/story_content/frame.xml'))->toBeTrue(); From 4c6b49bd0819744e86790a56df1361f0e205e80c Mon Sep 17 00:00:00 2001 From: Steve Williamson Date: Thu, 28 May 2026 13:02:40 -0400 Subject: [PATCH 09/16] Address remaining Cursor review comments --- routes/web.php | 4 ++-- src/Console/Commands/BackfillEmbeddedPlayerCourses.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/routes/web.php b/routes/web.php index 757dc6e..82e222a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -26,9 +26,9 @@ Route::get('lms/scorm-package/{document}/{entry?}', [ScormPackageController::class, 'show']) ->where('entry', '.*') ->name('filament-lms.scorm-package.show') - ->middleware(['web', 'auth']); + ->middleware('auth'); Route::post('lms/scorm-commit/{course}', [ScormCommitController::class, 'store']) ->name('filament-lms.scorm-commit.store') - ->middleware(['web', 'auth']); + ->middleware('auth'); }); diff --git a/src/Console/Commands/BackfillEmbeddedPlayerCourses.php b/src/Console/Commands/BackfillEmbeddedPlayerCourses.php index 4c06a65..40d93e4 100644 --- a/src/Console/Commands/BackfillEmbeddedPlayerCourses.php +++ b/src/Console/Commands/BackfillEmbeddedPlayerCourses.php @@ -18,22 +18,22 @@ final class BackfillEmbeddedPlayerCourses extends Command public function handle(): int { - $courseIds = Document::query() + $documentIds = Document::query() ->whereNotNull('package_path') ->where('package_path', '!=', '') ->pluck('id'); - if ($courseIds->isEmpty()) { + if ($documentIds->isEmpty()) { $this->warn('No documents with retained packages found.'); return self::SUCCESS; } $query = Course::query() - ->whereHas('steps', function ($stepQuery) use ($courseIds): void { + ->whereHas('steps', function ($stepQuery) use ($documentIds): void { $stepQuery ->where('material_type', 'document') - ->whereIn('material_id', $courseIds); + ->whereIn('material_id', $documentIds); }); if ($courseId = $this->option('course-id')) { From c4a2a7dfabdecd32fcce0a8e351533fabbbe93b2 Mon Sep 17 00:00:00 2001 From: Steve Williamson Date: Thu, 28 May 2026 13:10:51 -0400 Subject: [PATCH 10/16] Fix Cursor follow-up review issues --- resources/views/components/exit-lms.blade.php | 6 ++- .../Controllers/ScormPackageController.php | 2 +- .../CommonCartridgeImportService.php | 6 +-- tests/Feature/ScormPackageServingTest.php | 49 +++++++++++++++++++ 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/resources/views/components/exit-lms.blade.php b/resources/views/components/exit-lms.blade.php index 9a9f8d5..d252b86 100644 --- a/resources/views/components/exit-lms.blade.php +++ b/resources/views/components/exit-lms.blade.php @@ -41,13 +41,17 @@ document.getElementById('lms-exit-with-complete')?.addEventListener('click', function (event) { event.preventDefault(); if (typeof window.lmsConfirmHtml5CourseComplete !== 'function' || ! window.lmsConfirmHtml5CourseComplete()) { + window.location.href = '/'; + return; } window.lmsPostHtml5CourseComplete(this.dataset.commitUrl, this.dataset.csrfToken) .then(() => { window.location.href = '/'; }) - .catch(() => {}); + .catch(() => { + window.location.href = '/'; + }); }); @else diff --git a/src/Http/Controllers/ScormPackageController.php b/src/Http/Controllers/ScormPackageController.php index f8ee4eb..fbeba91 100644 --- a/src/Http/Controllers/ScormPackageController.php +++ b/src/Http/Controllers/ScormPackageController.php @@ -114,7 +114,7 @@ private function mimeType(string $path): string 'png' => 'image/png', 'jpg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', - 'svg' => 'image/svg+xml', + 'svg' => 'application/octet-stream', 'woff' => 'font/woff', 'woff2' => 'font/woff2', 'ttf' => 'font/ttf', diff --git a/src/Services/CommonCartridge/CommonCartridgeImportService.php b/src/Services/CommonCartridge/CommonCartridgeImportService.php index dd26b52..fadb5d4 100644 --- a/src/Services/CommonCartridge/CommonCartridgeImportService.php +++ b/src/Services/CommonCartridge/CommonCartridgeImportService.php @@ -93,6 +93,9 @@ public function import(string $extractedPath, int|string|null $tenantId = null): $stepsCreated += $this->createResourcesStepIfNeeded($course, $lesson, $lessonStructure, $manifest->frameResources); } + $this->attachRetainedPackage($extractedPath); + $this->finalizeEmbeddedPlayerCourse($course, $extractedPath); + return [ 'course' => $course, 'lessons_created' => $lessonsCreated, @@ -100,9 +103,6 @@ public function import(string $extractedPath, int|string|null $tenantId = null): ]; }); - $this->attachRetainedPackage($extractedPath); - $this->finalizeEmbeddedPlayerCourse($result['course'], $extractedPath); - return $result; } diff --git a/tests/Feature/ScormPackageServingTest.php b/tests/Feature/ScormPackageServingTest.php index a9938ed..c00fd0d 100644 --- a/tests/Feature/ScormPackageServingTest.php +++ b/tests/Feature/ScormPackageServingTest.php @@ -159,6 +159,55 @@ ->assertHeader('content-type', 'application/javascript'); }); +test('serves svg package assets as octet stream to avoid same-origin script execution', function () { + config([ + 'filament-lms.user_model' => TestUser::class, + ]); + + $user = TestUser::query()->create([ + 'name' => 'Learner', + 'first_name' => 'Learner', + 'last_name' => 'User', + 'email' => 'svg-assets@example.com', + 'password' => bcrypt('password'), + ]); + + $packageId = (string) Str::uuid(); + $relativePath = 'lms-scorm-packages/'.$packageId; + $packageRoot = storage_path('app/'.$relativePath); + if (! is_dir($packageRoot)) { + mkdir($packageRoot, 0755, true); + } + file_put_contents($packageRoot.'/icon.svg', ''); + + $course = Course::factory()->create([ + 'name' => 'SVG Assets Course', + 'slug' => 'svg-assets-course', + 'is_private' => false, + ]); + $lesson = Lesson::factory()->create(['course_id' => $course->id]); + $document = Document::query()->create([ + 'name' => 'Home', + 'package_disk' => 'local', + 'package_path' => $relativePath, + 'package_launch_path' => 'index.html', + ]); + Step::factory()->create([ + 'lesson_id' => $lesson->id, + 'material_type' => 'document', + 'material_id' => $document->id, + ]); + + $this->actingAs($user); + + $this->get(route('filament-lms.scorm-package.show', [ + 'document' => $document->id, + 'entry' => 'icon.svg', + ])) + ->assertSuccessful() + ->assertHeader('content-type', 'application/octet-stream'); +}); + test('serves html5 package index with injected bridge for embedded player courses', function () { config([ 'filament-lms.user_model' => TestUser::class, From 1e0880da9751395943d49583a7a53deffaff838d Mon Sep 17 00:00:00 2001 From: Steve Williamson Date: Thu, 28 May 2026 13:15:41 -0400 Subject: [PATCH 11/16] Address additional Cursor review feedback --- src/FilamentLmsServiceProvider.php | 4 +- src/Livewire/DocumentStep.php | 7 ++ src/Models/Step.php | 92 ++++++++++++++++- .../CommonCartridgeImportService.php | 99 ++++++++++--------- tests/Feature/StepRenderedTextTest.php | 17 ++++ 5 files changed, 169 insertions(+), 50 deletions(-) diff --git a/src/FilamentLmsServiceProvider.php b/src/FilamentLmsServiceProvider.php index 513b7df..f4dfa93 100644 --- a/src/FilamentLmsServiceProvider.php +++ b/src/FilamentLmsServiceProvider.php @@ -56,11 +56,11 @@ public function configurePackage(Package $package): void 'make_material_nullable_in_lms_steps_table', 'backfill_lms_course_user_completed_at_from_step_dates', 'change_name_to_text_on_lms_tests_table', + 'create_lms_credit_categories_table', + 'create_lms_course_credit_category_table', 'add_scorm_package_columns_to_lms_documents_table', 'add_embedded_player_to_lms_courses_table', 'add_player_slide_id_to_lms_steps_table', - 'create_lms_credit_categories_table', - 'create_lms_course_credit_category_table', ]) ->hasCommand(BackfillCourseCompletedAt::class) ->hasCommand(BackfillEmbeddedPlayerCourses::class) diff --git a/src/Livewire/DocumentStep.php b/src/Livewire/DocumentStep.php index 671a80a..c752323 100644 --- a/src/Livewire/DocumentStep.php +++ b/src/Livewire/DocumentStep.php @@ -43,6 +43,13 @@ public function download() $this->downloaded = true; $mediaItem = $this->document->getFirstMedia(); + if ($mediaItem === null) { + if ($this->document->hasScormPackage()) { + return redirect()->to($this->getPdfUrl()); + } + + abort(404); + } return response()->download($mediaItem->getPath(), $mediaItem->file_name); } diff --git a/src/Models/Step.php b/src/Models/Step.php index 3c53dea..57c8873 100644 --- a/src/Models/Step.php +++ b/src/Models/Step.php @@ -3,6 +3,9 @@ namespace Tapp\FilamentLms\Models; use Carbon\Carbon; +use DOMDocument; +use DOMElement; +use DOMNode; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -61,6 +64,30 @@ class Step extends Model implements Sortable 'require_perfect_score' => 'boolean', ]; + private const ALLOWED_RENDERED_TEXT_TAGS = [ + 'a', + 'blockquote', + 'br', + 'code', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'li', + 'ol', + 'p', + 'pre', + 'strong', + 'ul', + ]; + + private const ALLOWED_RENDERED_TEXT_ATTRIBUTES = [ + 'a' => ['href', 'rel', 'target', 'title'], + ]; + protected static function newFactory() { return StepFactory::new(); @@ -97,7 +124,70 @@ public function getRenderedText(): string private function sanitizeRenderedText(string $html): string { - return preg_replace('#]*>.*?#is', '', $html) ?? ''; + $document = new DOMDocument('1.0', 'UTF-8'); + $previous = libxml_use_internal_errors(true); + $document->loadHTML('
    '.$html.'
    ', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + + $root = $document->getElementById('lms-rendered-text-root'); + if (! $root instanceof DOMElement) { + return ''; + } + + $this->sanitizeRenderedTextNode($root); + + $rendered = ''; + foreach ($root->childNodes as $childNode) { + $rendered .= $document->saveHTML($childNode) ?: ''; + } + + return $rendered; + } + + private function sanitizeRenderedTextNode(DOMNode $node): void + { + for ($child = $node->firstChild; $child !== null; $child = $next) { + $next = $child->nextSibling; + + if (! $child instanceof DOMElement) { + continue; + } + + $tagName = mb_strtolower($child->tagName); + if (! in_array($tagName, self::ALLOWED_RENDERED_TEXT_TAGS, true)) { + $child->parentNode?->removeChild($child); + + continue; + } + + $this->sanitizeRenderedTextAttributes($child, $tagName); + $this->sanitizeRenderedTextNode($child); + } + } + + private function sanitizeRenderedTextAttributes(DOMElement $element, string $tagName): void + { + $allowedAttributes = self::ALLOWED_RENDERED_TEXT_ATTRIBUTES[$tagName] ?? []; + + foreach (iterator_to_array($element->attributes) as $attribute) { + if (! in_array($attribute->name, $allowedAttributes, true)) { + $element->removeAttribute($attribute->name); + } + } + + if ($tagName !== 'a') { + return; + } + + $href = html_entity_decode($element->getAttribute('href'), ENT_QUOTES | ENT_HTML5, 'UTF-8'); + if ($href !== '' && preg_match('/^\s*(javascript|data|vbscript):/i', $href) === 1) { + $element->removeAttribute('href'); + } + + if ($element->getAttribute('target') === '_blank') { + $element->setAttribute('rel', 'noopener noreferrer'); + } } public function complete($user = null) diff --git a/src/Services/CommonCartridge/CommonCartridgeImportService.php b/src/Services/CommonCartridge/CommonCartridgeImportService.php index fadb5d4..e20d0fb 100644 --- a/src/Services/CommonCartridge/CommonCartridgeImportService.php +++ b/src/Services/CommonCartridge/CommonCartridgeImportService.php @@ -52,58 +52,63 @@ public static function manualImportGaps(): array */ public function import(string $extractedPath, int|string|null $tenantId = null): array { - Log::channel('single')->info('CC import: start', [ - 'context' => 'cc-import', - 'extracted_path' => mb_rtrim($extractedPath, '/'), - ]); - - $manifest = $this->parser->parse($extractedPath); - $extractedPath = mb_rtrim($extractedPath, '/'); $this->packageDocuments = []; - $this->packageLaunchHref = $manifest->preferredLaunchHref; - - $result = DB::transaction(function () use ($manifest, $extractedPath, $tenantId) { - $course = $this->createCourse($manifest, $tenantId); - $lessonsCreated = 0; - $stepsCreated = 0; - $isFirstStepOfCourse = true; - $primaryResourceId = $this->getPrimaryWebContentResourceId( - $manifest->resources, - $manifest->preferredLaunchHref, - ); - - foreach ($manifest->lessons as $lessonStructure) { - $lesson = $this->createLesson($course, $lessonStructure); - $lessonsCreated++; - - foreach ($lessonStructure->steps as $stepStructure) { - $this->createStep( - $course, - $lesson, - $stepStructure, - $manifest->resources, - $extractedPath, - $manifest->preferredLaunchHref, - $isFirstStepOfCourse ? $primaryResourceId : null, - ); - $isFirstStepOfCourse = false; - $stepsCreated++; - } + $this->packageLaunchHref = null; - $stepsCreated += $this->createResourcesStepIfNeeded($course, $lesson, $lessonStructure, $manifest->frameResources); - } + try { + Log::channel('single')->info('CC import: start', [ + 'context' => 'cc-import', + 'extracted_path' => mb_rtrim($extractedPath, '/'), + ]); - $this->attachRetainedPackage($extractedPath); - $this->finalizeEmbeddedPlayerCourse($course, $extractedPath); + $manifest = $this->parser->parse($extractedPath); + $extractedPath = mb_rtrim($extractedPath, '/'); + $this->packageLaunchHref = $manifest->preferredLaunchHref; + + return DB::transaction(function () use ($manifest, $extractedPath, $tenantId) { + $course = $this->createCourse($manifest, $tenantId); + $lessonsCreated = 0; + $stepsCreated = 0; + $isFirstStepOfCourse = true; + $primaryResourceId = $this->getPrimaryWebContentResourceId( + $manifest->resources, + $manifest->preferredLaunchHref, + ); + + foreach ($manifest->lessons as $lessonStructure) { + $lesson = $this->createLesson($course, $lessonStructure); + $lessonsCreated++; + + foreach ($lessonStructure->steps as $stepStructure) { + $this->createStep( + $course, + $lesson, + $stepStructure, + $manifest->resources, + $extractedPath, + $manifest->preferredLaunchHref, + $isFirstStepOfCourse ? $primaryResourceId : null, + ); + $isFirstStepOfCourse = false; + $stepsCreated++; + } + + $stepsCreated += $this->createResourcesStepIfNeeded($course, $lesson, $lessonStructure, $manifest->frameResources); + } - return [ - 'course' => $course, - 'lessons_created' => $lessonsCreated, - 'steps_created' => $stepsCreated, - ]; - }); + $this->attachRetainedPackage($extractedPath); + $this->finalizeEmbeddedPlayerCourse($course, $extractedPath); - return $result; + return [ + 'course' => $course, + 'lessons_created' => $lessonsCreated, + 'steps_created' => $stepsCreated, + ]; + }); + } finally { + $this->packageDocuments = []; + $this->packageLaunchHref = null; + } } private function finalizeEmbeddedPlayerCourse(Course $course, string $extractedPath): void diff --git a/tests/Feature/StepRenderedTextTest.php b/tests/Feature/StepRenderedTextTest.php index 337fded..edf548c 100644 --- a/tests/Feature/StepRenderedTextTest.php +++ b/tests/Feature/StepRenderedTextTest.php @@ -83,3 +83,20 @@ expect($step->getRenderedText())->not->toContain('link', + ]); + + $rendered = $step->getRenderedText(); + + expect($rendered)->not->toContain('', + ]); + + $rendered = $step->getRenderedText(); + + expect($rendered)->toContain('Wrapped text'); + expect($rendered)->not->toContain('
    '); + expect($rendered)->not->toContain(''); + expect($rendered)->not->toContain(' + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + + diff --git a/tests/fixtures/articulate-rise/scormdriver/auto-scripts/AutoBookmark.js b/tests/fixtures/articulate-rise/scormdriver/auto-scripts/AutoBookmark.js new file mode 100644 index 0000000..d71738c --- /dev/null +++ b/tests/fixtures/articulate-rise/scormdriver/auto-scripts/AutoBookmark.js @@ -0,0 +1,3 @@ +// version: 7.12.0.a.1.5.1 +// sha: 4770bd49d79a0e9175bc7d4962e8ed15628c37a2 +function SetBookmark(){var o=window.parent,t=window.location.href;o.SetBookmark(t.substring(t.toLowerCase().lastIndexOf("/scormcontent/")+14,t.length),document.title),o.CommitData()}SetBookmark(); \ No newline at end of file diff --git a/tests/fixtures/articulate-rise/scormdriver/auto-scripts/AutoCompleteSCO.js b/tests/fixtures/articulate-rise/scormdriver/auto-scripts/AutoCompleteSCO.js new file mode 100644 index 0000000..a83cd92 --- /dev/null +++ b/tests/fixtures/articulate-rise/scormdriver/auto-scripts/AutoCompleteSCO.js @@ -0,0 +1,3 @@ +// version: 7.12.0.a.1.5.1 +// sha: 4770bd49d79a0e9175bc7d4962e8ed15628c37a2 +function SetSCOComplete(){var e=window.parent;e.SetReachedEnd(),e.CommitData()}SetSCOComplete(); \ No newline at end of file diff --git a/tests/fixtures/articulate-rise/scormdriver/auto-scripts/CourseExit.js b/tests/fixtures/articulate-rise/scormdriver/auto-scripts/CourseExit.js new file mode 100644 index 0000000..bfdbe6d --- /dev/null +++ b/tests/fixtures/articulate-rise/scormdriver/auto-scripts/CourseExit.js @@ -0,0 +1,3 @@ +// version: 7.12.0.a.1.5.1 +// sha: 4770bd49d79a0e9175bc7d4962e8ed15628c37a2 +function niExit(){var o=window.parent;confirm("Are You Sure You Wish To Exit This Course?")&&o.ConcedeControl()} \ No newline at end of file diff --git a/tests/fixtures/articulate-rise/scormdriver/blank.html b/tests/fixtures/articulate-rise/scormdriver/blank.html new file mode 100644 index 0000000..e8e6d75 --- /dev/null +++ b/tests/fixtures/articulate-rise/scormdriver/blank.html @@ -0,0 +1,51 @@ + + + + + Blank page + + + +   + + + diff --git a/tests/fixtures/articulate-rise/scormdriver/browsersniff.js b/tests/fixtures/articulate-rise/scormdriver/browsersniff.js new file mode 100644 index 0000000..4cd5023 --- /dev/null +++ b/tests/fixtures/articulate-rise/scormdriver/browsersniff.js @@ -0,0 +1,3 @@ +// version: 7.12.0.a.1.5.1 +// sha: 4770bd49d79a0e9175bc7d4962e8ed15628c37a2 +var is_js,agt=navigator.userAgent.toLowerCase(),is_major=parseInt(navigator.appVersion),is_minor=parseFloat(navigator.appVersion),is_nav=-1!=agt.indexOf("mozilla")&&-1==agt.indexOf("spoofer")&&-1==agt.indexOf("compatible")&&-1==agt.indexOf("opera")&&-1==agt.indexOf("webtv")&&-1==agt.indexOf("hotjava"),is_nav2=is_nav&&2==is_major,is_nav3=is_nav&&3==is_major,is_nav4=is_nav&&4==is_major,is_nav4up=is_nav&&is_major>=4,is_navonly=is_nav&&(-1!=agt.indexOf(";nav")||-1!=agt.indexOf("; nav")),is_nav6=is_nav&&5==is_major&&agt.indexOf("rv:0")>-1,is_nav6up=is_nav&&is_major>=5,is_gecko=-1!=agt.indexOf("gecko"),is_ie=-1!=agt.indexOf("msie")&&-1==agt.indexOf("opera"),is_ie3=is_ie&&is_major<4,is_ie4=is_ie&&4==is_major&&-1!=agt.indexOf("msie 4"),is_ie4up=is_ie&&is_major>=4,is_ie5=is_ie&&4==is_major&&-1!=agt.indexOf("msie 5.0"),is_ie5_5=is_ie&&4==is_major&&-1!=agt.indexOf("msie 5.5"),is_ie5up=is_ie&&!is_ie3&&!is_ie4,is_ie5_5up=is_ie&&!is_ie3&&!is_ie4&&!is_ie5,is_ie6=is_ie&&4==is_major&&-1!=agt.indexOf("msie 6."),is_ie6up=is_ie&&!is_ie3&&!is_ie4&&!is_ie5&&!is_ie5_5,is_aol=-1!=agt.indexOf("aol"),is_aol3=is_aol&&is_ie3,is_aol4=is_aol&&is_ie4,is_aol5=-1!=agt.indexOf("aol 5"),is_aol6=-1!=agt.indexOf("aol 6"),is_opera=-1!=agt.indexOf("opera"),is_opera2=-1!=agt.indexOf("opera 2")||-1!=agt.indexOf("opera/2"),is_opera3=-1!=agt.indexOf("opera 3")||-1!=agt.indexOf("opera/3"),is_opera4=-1!=agt.indexOf("opera 4")||-1!=agt.indexOf("opera/4"),is_opera5=-1!=agt.indexOf("opera 5")||-1!=agt.indexOf("opera/5"),is_opera5up=is_opera&&!is_opera2&&!is_opera3&&!is_opera4,is_webtv=-1!=agt.indexOf("webtv"),is_TVNavigator=-1!=agt.indexOf("navio")||-1!=agt.indexOf("navio_aoltv"),is_AOLTV=is_TVNavigator,is_hotjava=-1!=agt.indexOf("hotjava"),is_hotjava3=is_hotjava&&3==is_major,is_hotjava3up=is_hotjava&&is_major>=3;is_js=is_nav2||is_ie3?1:is_nav3?1.1:is_opera5up?1.3:is_opera?1.1:is_nav4&&is_minor<=4.05||is_ie4?1.2:is_nav4&&is_minor>4.05||is_ie5?1.3:is_hotjava3up?1.4:is_nav6||is_gecko||is_nav6up?1.5:is_ie5up?1.3:0;var is_win=-1!=agt.indexOf("win")||-1!=agt.indexOf("16bit"),is_win95=-1!=agt.indexOf("win95")||-1!=agt.indexOf("windows 95"),is_win16=-1!=agt.indexOf("win16")||-1!=agt.indexOf("16bit")||-1!=agt.indexOf("windows 3.1")||-1!=agt.indexOf("windows 16-bit"),is_win31=-1!=agt.indexOf("windows 3.1")||-1!=agt.indexOf("win16")||-1!=agt.indexOf("windows 16-bit"),is_winme=-1!=agt.indexOf("win 9x 4.90"),is_win2k=-1!=agt.indexOf("windows nt 5.0"),is_win98=-1!=agt.indexOf("win98")||-1!=agt.indexOf("windows 98"),is_winnt=-1!=agt.indexOf("winnt")||-1!=agt.indexOf("windows nt"),is_win32=is_win95||is_winnt||is_win98||is_major>=4&&"Win32"==navigator.platform||-1!=agt.indexOf("win32")||-1!=agt.indexOf("32bit"),is_os2=-1!=agt.indexOf("os/2")||-1!=navigator.appVersion.indexOf("OS/2")||-1!=agt.indexOf("ibm-webexplorer"),is_mac=-1!=agt.indexOf("mac");is_mac&&is_ie5up&&(is_js=1.4);var is_mac68k=is_mac&&(-1!=agt.indexOf("68k")||-1!=agt.indexOf("68000")),is_macppc=is_mac&&(-1!=agt.indexOf("ppc")||-1!=agt.indexOf("powerpc")),is_sun=-1!=agt.indexOf("sunos"),is_sun4=-1!=agt.indexOf("sunos 4"),is_sun5=-1!=agt.indexOf("sunos 5"),is_suni86=is_sun&&-1!=agt.indexOf("i86"),is_irix=-1!=agt.indexOf("irix"),is_irix5=-1!=agt.indexOf("irix 5"),is_irix6=-1!=agt.indexOf("irix 6")||-1!=agt.indexOf("irix6"),is_hpux=-1!=agt.indexOf("hp-ux"),is_hpux9=is_hpux&&-1!=agt.indexOf("09."),is_hpux10=is_hpux&&-1!=agt.indexOf("10."),is_aix=-1!=agt.indexOf("aix"),is_aix1=-1!=agt.indexOf("aix 1"),is_aix2=-1!=agt.indexOf("aix 2"),is_aix3=-1!=agt.indexOf("aix 3"),is_aix4=-1!=agt.indexOf("aix 4"),is_linux=-1!=agt.indexOf("inux"),is_sco=-1!=agt.indexOf("sco")||-1!=agt.indexOf("unix_sv"),is_unixware=-1!=agt.indexOf("unix_system_v"),is_mpras=-1!=agt.indexOf("ncr"),is_reliant=-1!=agt.indexOf("reliantunix"),is_dec=-1!=agt.indexOf("dec")||-1!=agt.indexOf("osf1")||-1!=agt.indexOf("dec_alpha")||-1!=agt.indexOf("alphaserver")||-1!=agt.indexOf("ultrix")||-1!=agt.indexOf("alphastation"),is_sinix=-1!=agt.indexOf("sinix"),is_freebsd=-1!=agt.indexOf("freebsd"),is_bsd=-1!=agt.indexOf("bsd"),is_unix=-1!=agt.indexOf("x11")||is_sun||is_irix||is_hpux||is_sco||is_unixware||is_mpras||is_reliant||is_dec||is_sinix||is_aix||is_linux||is_bsd||is_freebsd,is_vms=-1!=agt.indexOf("vax")||-1!=agt.indexOf("openvms"); \ No newline at end of file diff --git a/tests/fixtures/articulate-rise/scormdriver/driverOptions.js b/tests/fixtures/articulate-rise/scormdriver/driverOptions.js new file mode 100644 index 0000000..769c450 --- /dev/null +++ b/tests/fixtures/articulate-rise/scormdriver/driverOptions.js @@ -0,0 +1,9 @@ +function loadDriverOptions (scope) { + scope.APPID = "WQBM2ARBZR"; + scope.CLOUDURL = null; + scope.USE_STRICT_SUSPEND_DATA_LIMITS = false; + scope.SHOW_DEBUG_ON_LAUNCH = false; + scope.FORCED_COMMIT_TIME = 20000; + scope.strLMSStandard = "SCORM"; + scope.REVIEW_MODE_IS_READ_ONLY = false; +} diff --git a/tests/fixtures/articulate-rise/scormdriver/goodbye.html b/tests/fixtures/articulate-rise/scormdriver/goodbye.html new file mode 100644 index 0000000..186365f --- /dev/null +++ b/tests/fixtures/articulate-rise/scormdriver/goodbye.html @@ -0,0 +1,35 @@ + + + + Goodbye + + + + +
    +
    👋 Bye!
    +
    You may now leave this page.
    +
    + + diff --git a/tests/fixtures/articulate-rise/scormdriver/indexAPI.html b/tests/fixtures/articulate-rise/scormdriver/indexAPI.html new file mode 100644 index 0000000..67e0b68 --- /dev/null +++ b/tests/fixtures/articulate-rise/scormdriver/indexAPI.html @@ -0,0 +1,90 @@ + + + + + + + + + Loading course + + + + + + + + + + + + +
    + + + + +
    + + + + + + diff --git a/tests/fixtures/articulate-rise/scormdriver/preloadIntegrity.js b/tests/fixtures/articulate-rise/scormdriver/preloadIntegrity.js new file mode 100644 index 0000000..47afceb --- /dev/null +++ b/tests/fixtures/articulate-rise/scormdriver/preloadIntegrity.js @@ -0,0 +1,86 @@ +var COURSE_PACKAGE_VERSION = "b2JkLCeO"; +var RESET_LEARNER_DATA = false; + +function isTrue(val) { + return val && String(val).toLowerCase() === 'true'; +} + +function verifySuspendDataVersion(maxAttempts) { + var commitDataAndBook; + var sentBookCheck; + var sentDataCheck; + var suspendData = GetDataChunk(); + var suspendDataCPV; + + if (!suspendData) { + return; + } + + if (maxAttempts === 0) { + WriteToDebug('WARNING: ERROR WITH CLEARING SUSPEND OR BOOKMARKING DATA!'); + WriteToDebug('NO MORE ATTEMPTS LEFT. COURSE BEHAVIOR MAY BE NEGATIVELY IMPACTED.'); + return; + } + + try { + suspendData = JSON.parse(suspendData); + suspendDataCPV = suspendData.cpv; + } catch (e) { + WriteToDebug('WARNING: Issue parsing suspend data for CPV. Version reset behavior disabled.'); + return; + } + + if (suspendDataCPV !== COURSE_PACKAGE_VERSION) { + WriteToDebug('WARNING: Suspend data version mismatch against package version.'); + if (RESET_LEARNER_DATA) { + + if (REVIEW_MODE_IS_READ_ONLY && GetLessonMode() === 3) { + WriteToDebug('WARNING: Course is in review mode. Unable to clear suspend and bookmarking data!'); + WriteToDebug('Resetting of learner data will not occur. Course behavior may be negatively impacted.'); + return; + } + + WriteToDebug('Okay to clear data. Now clearing suspend and bookmarking data...'); + + sentDataCheck = SetDataChunk(""); + WriteToDebug('Suspend Data Cleared: ' + sentDataCheck ); + + sentBookCheck = SetBookmark(""); + WriteToDebug('Bookmark Data Cleared: ' + sentBookCheck ); + + commitDataAndBook = CommitData(); + WriteToDebug('Data Commited: ' + commitDataAndBook ); + + if (isTrue(sentDataCheck) && isTrue(sentBookCheck) && isTrue(commitDataAndBook)) { + WriteToDebug('Verified that all bookmarking and suspend data have been cleared. Data commited successfully.'); + } else { + WriteToDebug('WARNING: ERROR WITH CLEARING SUSPEND OR BOOKMARKING DATA! RETRY ATTEMPTS LEFT: ' + (maxAttempts-1)); + verifySuspendDataVersion(maxAttempts-1); + } + } else { + WriteToDebug('WARNING: Course reset flag not turned on. Resetting of learner data will not occur.'); + WriteToDebug('Course behavior may be negatively impacted.'); + } + } +} + +function verifyForcedCommitTime() { + if (FORCED_COMMIT_TIME >= 5000) { + WriteToDebug('Starting Forced Commit heartbeat...'); + var intervalID = setInterval(commitHeartbeat, FORCED_COMMIT_TIME); + } else { + WriteToDebug('Forced Commit Time cannot be lower than 5000 milliseconds. Not using forced commit time.'); + } +} + +function commitHeartbeat() { + WriteToDebug('Forced Commit heartbeat triggered. Now saving current session time...'); + SetSessionTime(GetSessionAccumulatedTime()); + + WriteToDebug('Forced Commit heartbeat trigger is now triggering a commit...'); + CommitData(); +} + +function getCourseTitle() { + return 'A Quick Guide to the Completion of the MDA' +} diff --git a/tests/fixtures/articulate-rise/scormdriver/scormdriver.js b/tests/fixtures/articulate-rise/scormdriver/scormdriver.js new file mode 100644 index 0000000..13f4fce --- /dev/null +++ b/tests/fixtures/articulate-rise/scormdriver/scormdriver.js @@ -0,0 +1,18 @@ +// version: 7.12.0.a.1.5.1 +// sha: 4770bd49d79a0e9175bc7d4962e8ed15628c37a2 +/*! Copyright © 2003-2018 Rustici Software, LLC All Rights Reserved. www.rusticisoftware.com */ +var VERSION="7.12.0.a.1.5.1",PREFERENCE_DEFAULT=0,PREFERENCE_OFF=-1,PREFERENCE_ON=1,LESSON_STATUS_PASSED=1,LESSON_STATUS_COMPLETED=2,LESSON_STATUS_FAILED=3,LESSON_STATUS_INCOMPLETE=4,LESSON_STATUS_BROWSED=5,LESSON_STATUS_NOT_ATTEMPTED=6,ENTRY_REVIEW=1,ENTRY_FIRST_TIME=2,ENTRY_RESUME=3,MODE_NORMAL=1,MODE_BROWSE=2,MODE_REVIEW=3,MAX_CMI_TIME=36002439990,NO_ERROR=0,ERROR_LMS=1,ERROR_INVALID_PREFERENCE=2,ERROR_INVALID_NUMBER=3,ERROR_INVALID_ID=4,ERROR_INVALID_STATUS=5,ERROR_INVALID_RESPONSE=6,ERROR_NOT_LOADED=7,ERROR_INVALID_INTERACTION_RESPONSE=8,EXIT_TYPE_SUSPEND="SUSPEND",EXIT_TYPE_FINISH="FINISH",EXIT_TYPE_TIMEOUT="TIMEOUT",EXIT_TYPE_UNLOAD="UNLOAD",INTERACTION_RESULT_CORRECT="CORRECT",INTERACTION_RESULT_WRONG="WRONG",INTERACTION_RESULT_UNANTICIPATED="UNANTICIPATED",INTERACTION_RESULT_NEUTRAL="NEUTRAL",INTERACTION_TYPE_TRUE_FALSE="true-false",INTERACTION_TYPE_CHOICE="choice",INTERACTION_TYPE_FILL_IN="fill-in",INTERACTION_TYPE_LONG_FILL_IN="long-fill-in",INTERACTION_TYPE_MATCHING="matching",INTERACTION_TYPE_PERFORMANCE="performance",INTERACTION_TYPE_SEQUENCING="sequencing",INTERACTION_TYPE_LIKERT="likert",INTERACTION_TYPE_NUMERIC="numeric",DATA_CHUNK_PAIR_SEPARATOR="###",DATA_CHUNK_VALUE_SEPARATOR="$$",APPID="__APPID__",CLOUDURL="__CLOUDURL__",blnDebug=!0,strLMSStandard="AUTO",DEFAULT_EXIT_TYPE=EXIT_TYPE_SUSPEND,AICC_LESSON_ID="1",EXIT_BEHAVIOR="SCORM_RECOMMENDED",EXIT_TARGET="goodbye.html",AICC_COMM_DISABLE_XMLHTTP=!1,AICC_COMM_DISABLE_IFRAME=!1,AICC_COMM_PREPEND_HTTP_IF_MISSING=!0,AICC_REPORT_MIN_MAX_SCORE=!0,SHOW_DEBUG_ON_LAUNCH=!1,DO_NOT_REPORT_INTERACTIONS=!1,SCORE_CAN_ONLY_IMPROVE=!1,REVIEW_MODE_IS_READ_ONLY=!0,TCAPI_DONT_USE_BROKEN_URN_IDS=!0,TCAPI_LRS_ENDPOINT="",TCAPI_LRS_KEY="",TCAPI_LRS_SECRET="",TCAPI_LRS_ACTOR="",AICC_RE_CHECK_LOADED_INTERVAL=250,AICC_RE_CHECK_ATTEMPTS_BEFORE_TIMEOUT=240,USE_AICC_KILL_TIME=!0,AICC_ENTRY_FLAG_DEFAULT=ENTRY_REVIEW,AICC_USE_CUSTOM_COMMS=!1,FORCED_COMMIT_TIME="0",ALLOW_NONE_STANDARD=!0,USE_2004_SUSPENDALL_NAVREQ=!1,USE_STRICT_SUSPEND_DATA_LIMITS=!0,EXIT_SUSPEND_IF_COMPLETED=!1,EXIT_NORMAL_IF_PASSED=!1,AICC_ENCODE_PARAMETER_VALUES=!0,PASS_FAIL_SETS_COMPLETION_FOR_2004=!0,ALLOW_INTERACTION_NULL_LEARNER_RESPONSE=!1,PREVENT_STATUS_CHANGE_DURING_INIT=!1,USE_LEGACY_IDENTIFIERS_FOR_2004=!1,URI_IDENTIFIER_PREFIX="urn:scormdriver:",noop=Function.prototype;function GetQueryStringValue(e,t){var r;return null===(r=SearchQueryStringPairs((t=t.substring(1)).split("&"),e))&&(r=SearchQueryStringPairs(t.split(/[\?\&]/),e)),null===r?(WriteToDebug("GetQueryStringValue Element '"+e+"' Not Found, Returning: empty string"),""):(WriteToDebug("GetQueryStringValue for '"+e+"' Returning: "+r),r)}function SearchQueryStringPairs(e,t){var r,n,i="";for(t=t.toLowerCase(),r=0;r0)if(-1!=(""+u).indexOf(".")){var c="0"+(""+u).substr((""+u).indexOf("."),(""+u).length);t+="+"+ZeroPad((""+u).substr(0,(""+u).indexOf("."))+"."+(c*=60),2)}else t+="+"+ZeroPad(u,2);else t+=ZeroPad(u,2);return t}function ConvertIso8601TimeStampToDate(e){e=new String(e);var t=new Array,r=(t=e.split(/[\:T+-]/))[0],n=t[1]-1,i=t[2],o=t[3],a=t[4],s=t[5];return new Date(r,n,i,o,a,s,0)}function ConvertDateToCMIDate(e){var t,r,n;return WriteToDebug("In ConvertDateToCMIDate"),t=(e=new Date(e)).getFullYear(),r=e.getMonth()+1,n=e.getDate(),ZeroPad(t,4)+"/"+ZeroPad(r,2)+"/"+ZeroPad(n,2)}function ConvertDateToCMITime(e){var t,r,n;return t=(e=new Date(e)).getHours(),r=e.getMinutes(),n=e.getSeconds(),ZeroPad(t,2)+":"+ZeroPad(r,2)+":"+ZeroPad(n,2)}function ConvertCMITimeSpanToMS(e){var t,r,n,i,o;return WriteToDebug("In ConvertCMITimeSpanToMS, strTime="+e),t=e.split(":"),IsValidCMITimeSpan(e)?(WriteToDebug("intHours="+(r=t[0])+" intMinutes="+(n=t[1])+" intSeconds="+(i=t[2])),o=36e5*r+6e4*n+1e3*i,WriteToDebug("Returning "+(o=Math.round(o))),o):(WriteToDebug("ERROR - Invalid TimeSpan"),SetErrorInfo(SCORM_ERROR_GENERAL,"LMS ERROR - Invalid time span passed to ConvertCMITimeSpanToMS, please contact technical support"),0)}function ConvertScorm2004TimeToMS(e){WriteToDebug("In ConvertScorm2004TimeToMS, strIso8601Time="+e);var t,r,n,i=0,o=0,a=0,s=0,u=0,c=0,l=0,C=36e5,I=864e5,_=26298e5;e=new String(e),t="",r="",n=!1;for(var S=1;S=0}function IsValidCMITimeSpan(e){WriteToDebug("In IsValidCMITimeSpan strValue="+e);return e.search(/^\d?\d?\d?\d:\d?\d:\d?\d(.\d\d?)?$/)>-1?(WriteToDebug("Returning True"),!0):(WriteToDebug("Returning False"),!1)}function IsValidIso8601TimeSpan(e){WriteToDebug("In IsValidIso8601TimeSpan strValue="+e);return e.search(/^P(\d+Y)?(\d+M)?(\d+D)?(T(\d+H)?(\d+M)?(\d+(.\d\d?)?S)?)?$/)>-1?(WriteToDebug("Returning True"),!0):(WriteToDebug("Returning False"),!1)}function ConvertMilliSecondsToTCAPITime(e,t){var r,n,i,o,a,s;return WriteToDebug("In ConvertMilliSecondsToTCAPITime, intTotalMilliseconds = "+e+", blnIncludeFraction = "+t),null!=t&&null!=t||(t=!0),WriteToDebug("Separated Parts, intHours="+(r=(e-(o=e%1e3)-1e3*(i=(e-o)/1e3%60)-6e4*(n=(e-o-1e3*i)/6e4%60))/36e5)+", intMinutes="+n+", intSeconds="+i+", intMilliseconds="+o),1e4==r&&(WriteToDebug("Max intHours detected"),100==(n=(e-36e5*(r=9999))/6e4)&&(n=99),100==(i=(e-36e5*r-6e4*(n=Math.floor(n)))/1e3)&&(i=99),WriteToDebug("Separated Parts, intHours="+r+", intMinutes="+n+", intSeconds="+(i=Math.floor(i))+", intMilliseconds="+(o=e-36e5*r-6e4*n-1e3*i))),a=Math.floor(o/10),s=ZeroPad(r,4)+":"+ZeroPad(n,2)+":"+ZeroPad(i,2),t&&(s+="."+a),WriteToDebug("strCMITimeSpan="+s),r>9999&&(s="9999:99:99",t&&(s+=".99")),WriteToDebug("returning "+s),s}function ConvertMilliSecondsToSCORMTime(e,t){var r,n,i,o,a,s;return WriteToDebug("In ConvertMilliSecondsToSCORMTime, intTotalMilliseconds = "+e+", blnIncludeFraction = "+t),null!=t&&null!=t||(t=!0),WriteToDebug("Separated Parts, intHours="+(r=(e-(o=e%1e3)-1e3*(i=(e-o)/1e3%60)-6e4*(n=(e-o-1e3*i)/6e4%60))/36e5)+", intMinutes="+n+", intSeconds="+i+", intMilliseconds="+o),1e4==r&&(WriteToDebug("Max intHours detected"),100==(n=(e-36e5*(r=9999))/6e4)&&(n=99),100==(i=(e-36e5*r-6e4*(n=Math.floor(n)))/1e3)&&(i=99),WriteToDebug("Separated Parts, intHours="+r+", intMinutes="+n+", intSeconds="+(i=Math.floor(i))+", intMilliseconds="+(o=e-36e5*r-6e4*n-1e3*i))),a=Math.floor(o/10),s=ZeroPad(r,4)+":"+ZeroPad(n,2)+":"+ZeroPad(i,2),t&&(s+="."+a),WriteToDebug("strCMITimeSpan="+s),r>9999&&(s="9999:99:99",t&&(s+=".99")),WriteToDebug("returning "+s),s}function ConvertMilliSecondsIntoSCORM2004Time(e){WriteToDebug("In ConvertMilliSecondsIntoSCORM2004Time intTotalMilliseconds="+e);var t,r,n,i,o,a,s,u="",c=6e3,l=36e4,C=864e4,I=26298e4,_=315576e4;return t=Math.floor(e/10),t-=(s=Math.floor(t/_))*_,t-=(a=Math.floor(t/I))*I,t-=(o=Math.floor(t/C))*C,t-=(i=Math.floor(t/l))*l,t-=(n=Math.floor(t/c))*c,s>0&&(u+=s+"Y"),a>0&&(u+=a+"M"),o>0&&(u+=o+"D"),(t-=100*(r=Math.floor(t/100)))+r+n+i>0&&(u+="T",i>0&&(u+=i+"H"),n>0&&(u+=n+"M"),t+r>0&&(u+=r,t>0&&(u+="."+t),u+="S")),""==u&&(u="T0S"),WriteToDebug("Returning-"+(u="P"+u)),u}function ZeroPad(e,t){var r,n,i,o;WriteToDebug("In ZeroPad intNum="+e+" intNumDigits="+t);var a=!1;if(-1!=(r=new String(e)).indexOf("-")&&(a=!0,r=r.substr(1,r.length)),-1!=r.indexOf(".")&&(r.replace(".",""),i=r.substr(r.indexOf(".")+1,r.length),r=r.substr(0,r.indexOf("."))),(n=r.length)>t)WriteToDebug("Length of string is greater than num digits, trimming string"),r=r.substr(0,t);else for(o=n;o0&&!IsValidDecimal(r)?(WriteToDebug("Returning False - min value supplied range is not a valid decimal, min="+r),!1):n.length>0&&!IsValidDecimal(n)?(WriteToDebug("Returning False - max value supplied for range is not a valid decimal, max="+n),!1):!(r.length>0&&n.length>0&&parseFloat(r)>parseFloat(n))||(WriteToDebug("Returning False - min value supplied for range is greater than the max, min="+r+", max="+n),!1)):(WriteToDebug("Returning false - string supplied for range has incorrect number of parts, parts="+t.length+", strValue="+e),!1)}function ConvertDecimalRangeToDecimalBasedOnLearnerResponse(e,t,r){WriteToDebug("In ConvertDecimalRangeToDecimalBasedOnLearnerResponse strValue="+e+",strLearnerResponse="+t+",blnCorrect="+r);var n,i,o;if(r)return WriteToDebug("Returning strLearnerResponse"),t;if(2===(n=(e=new String(e)).split("[:]")).length){if(i=Trim(n[0]),o=Trim(n[1]),i.length>0)return WriteToDebug("Returning strMin"),i;if(o.length>0)return WriteToDebug("Returning strMax"),o}return WriteToDebug("Returning null"),null}function IsValidDecimal(e){return WriteToDebug("In IsValidDecimal, strValue="+e),(e=new String(e)).search(/[^.\d-]/)>-1?(WriteToDebug("Returning False - character other than a digit, dash or period found"),!1):e.search("-")>-1&&e.indexOf("-",1)>-1?(WriteToDebug("Returning False - dash found in the middle of the string"),!1):e.indexOf(".")!=e.lastIndexOf(".")?(WriteToDebug("Returning False - more than one decimal point found"),!1):e.search(/\d/)<0?(WriteToDebug("Returning False - no digits found"),!1):(WriteToDebug("Returning True"),!0)}function IsAlphaNumeric(e){return WriteToDebug("In IsAlphaNumeric"),(window&&Object.hasOwnProperty.call(window,"ActiveXObject")&&!window.ActiveXObject?e.search(RegExp("\\w")):e.search(RegExp("\\p{L}|\\p{N}","u")))<0?(WriteToDebug("Returning false"),!1):(WriteToDebug("Returning true"),!0)}function ReverseNameSequence(e){var t,r,n;return""==e&&(e="Not Found, Learner Name"),n=e.indexOf(","),t=e.slice(n+1),r=e.slice(0,n),(t=Trim(t))+" "+(r=Trim(r))}function LTrim(e){return(e=new String(e)).replace(/^\s+/,"")}function RTrim(e){return(e=new String(e)).replace(/\s+$/,"")}function Trim(e){return LTrim(RTrim(e)).replace(/\s{2,}/g," ")}function GetValueFromDataChunk(e){var t,r=new String(GetDataChunk()),n=new Array,i=new Array;for(n=r.split(parent.DATA_CHUNK_PAIR_SEPARATOR),t=0;t4e3?(WriteToDebug("SCORM2004_SetDataChunk - suspend_data too large for SCORM 2004 2nd ed (4000 character limit) but will try to persist anyway."),e.length>64e3?(WriteToDebug("SCORM2004_SetDataChunk - suspend_data too large for SCORM 2004 3rd & 4th ed (64000 character limit) so failing to persist."),!1):SCORM2004_CallSetValue("cmi.suspend_data",e)):SCORM2004_CallSetValue("cmi.suspend_data",e)}function SCORM2004_GetLaunchData(){return WriteToDebug("In SCORM2004_GetLaunchData"),SCORM2004_ClearErrorInfo(),SCORM2004_CallGetValue("cmi.launch_data")}function SCORM2004_GetComments(){var e;WriteToDebug("In SCORM2004_GetComments"),SCORM2004_ClearErrorInfo();var t="";e=SCORM2004_CallGetValue("cmi.comments_from_learner._count");for(var r=0;r0&&(t+=" | "),t+=SCORM2004_CallGetValue("cmi.comments_from_learner."+r+".comment");return t}function SCORM2004_WriteComment(e){var t,r;return WriteToDebug("In SCORM2004_WriteComment strComment="+e),SCORM2004_ClearErrorInfo(),0==e.search(/ \| /)&&(e=e.substr(3)),e.replace(/\|\|/g,"|"),r=SCORM2004_CallSetValue("cmi.comments_from_learner."+(t=SCORM2004_CallGetValue("cmi.comments_from_learner._count"))+".comment",e),r=SCORM2004_CallSetValue("cmi.comments_from_learner."+t+".timestamp",ConvertDateToIso8601TimeStamp(new Date))&&r}function SCORM2004_GetLMSComments(){var e;WriteToDebug("In SCORM2004_GetLMSComments"),SCORM2004_ClearErrorInfo();var t="";e=SCORM2004_CallGetValue("cmi.comments_from_lms._count");for(var r=0;r0&&(t+=" \r\n"),t+=SCORM2004_CallGetValue("cmi.comments_from_lms."+r+".comment");return t}function SCORM2004_GetAudioPlayPreference(){var e;return WriteToDebug("In SCORM2004_GetAudioPlayPreference"),SCORM2004_ClearErrorInfo(),""==(e=SCORM2004_CallGetValue("cmi.learner_preference.audio_level"))&&(e=0),WriteToDebug("intTempPreference="+(e=parseInt(e,10))),e>0?(WriteToDebug("Returning On"),PREFERENCE_ON):e<=0?(WriteToDebug("Returning Off"),PREFERENCE_OFF):(WriteToDebug("Error: Invalid preference"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_PREFERENCE,"Invalid audio preference received from LMS","intTempPreference="+e),null)}function SCORM2004_GetAudioVolumePreference(){var e;return WriteToDebug("In SCORM2004_GetAudioVollumePreference"),SCORM2004_ClearErrorInfo(),WriteToDebug("intTempPreference="+(e=SCORM2004_CallGetValue("cmi.learner_preference.audio_level"))),""==e&&(e=100),(e=parseInt(e,10))<=0&&(WriteToDebug("Setting to 100"),e=100),e>0&&e<=100?(WriteToDebug("Returning "+e),e):(WriteToDebug("ERROR: invalid preference"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_PREFERENCE,"Invalid audio preference received from LMS","intTempPreference="+e),null)}function SCORM2004_SetAudioPreference(e,t){return WriteToDebug("In SCORM2004_SetAudioPreference PlayPreference="+e+", intPercentOfMaxVolume="+t),SCORM2004_ClearErrorInfo(),e==PREFERENCE_OFF&&(WriteToDebug("Setting percent to 0"),t=0),SCORM2004_CallSetValue("cmi.learner_preference.audio_level",t)}function SCORM2004_SetLanguagePreference(e){return WriteToDebug("In SCORM2004_SetLanguagePreference strLanguage="+e),SCORM2004_ClearErrorInfo(),SCORM2004_CallSetValue("cmi.learner_preference.language",e)}function SCORM2004_GetLanguagePreference(){return WriteToDebug("In SCORM2004_GetLanguagePreference"),SCORM2004_ClearErrorInfo(),SCORM2004_CallGetValue("cmi.learner_preference.language")}function SCORM2004_SetSpeedPreference(e){return WriteToDebug("In SCORM2004_SetSpeedPreference intPercentOfMax="+e),SCORM2004_ClearErrorInfo(),SCORM2004_CallSetValue("cmi.learner_preference.delivery_speed",e)}function SCORM2004_GetSpeedPreference(){var e;return WriteToDebug("In SCORM2004_GetSpeedPreference"),SCORM2004_ClearErrorInfo(),WriteToDebug("intSCORMSpeed="+(e=SCORM2004_CallGetValue("cmi.learner_preference.delivery_speed"))),""==e&&(WriteToDebug("Detected empty string, defaulting to 100"),e=100),(e=parseInt(e,10))<0?(WriteToDebug("ERROR - out of range"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_SPEED,"Invalid speed preference received from LMS - out of range","intSCORMSpeed="+e),null):(WriteToDebug("intSCORMSpeed "+e),e)}function SCORM2004_SetTextPreference(e){return WriteToDebug("In SCORM2004_SetTextPreference intPreference="+e),SCORM2004_ClearErrorInfo(),SCORM2004_CallSetValue("cmi.learner_preference.audio_captioning",e)}function SCORM2004_GetTextPreference(){var e;return WriteToDebug("In SCORM2004_GetTextPreference"),SCORM2004_ClearErrorInfo(),e=SCORM2004_CallGetValue("cmi.learner_preference.audio_captioning"),WriteToDebug("intTempPreference="+(e=parseInt(e,10))),e>0?(WriteToDebug("Returning On"),PREFERENCE_ON):0==e||""==e?(WriteToDebug("Returning Default"),PREFERENCE_DEFAULT):e<0?(WriteToDebug("Returning Off"),PREFERENCE_OFF):(WriteToDebug("Error: Invalid preference"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_PREFERENCE,"Invalid text preference received from LMS","intTempPreference="+e),null)}function SCORM2004_GetPreviouslyAccumulatedTime(){var e,t;return WriteToDebug("In SCORM2004_GetPreviouslyAccumulatedTime"),SCORM2004_ClearErrorInfo(),WriteToDebug("strIso8601Time="+(e=SCORM2004_CallGetValue("cmi.total_time"))),IsValidIso8601TimeSpan(e)?(WriteToDebug("Returning "+(t=ConvertScorm2004TimeToMS(e))),t):(WriteToDebug("ERROR - Invalid Iso8601Time"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_TIMESPAN,"Invalid timespan received from LMS","strTime="+e),null)}function SCORM2004_SaveTime(e){var t;return WriteToDebug("In SCORM2004_SaveTime intMilliSeconds="+e),SCORM2004_ClearErrorInfo(),WriteToDebug("strISO8601Time="+(t=ConvertMilliSecondsIntoSCORM2004Time(e))),SCORM2004_CallSetValue("cmi.session_time",t)}function SCORM2004_GetMaxTimeAllowed(){var e,t;return WriteToDebug("In SCORM2004_GetMaxTimeAllowed"),SCORM2004_ClearErrorInfo(),WriteToDebug("strIso8601Time="+(e=SCORM2004_CallGetValue("cmi.max_time_allowed"))),""==e&&(e="20Y"),IsValidIso8601TimeSpan(e)?(WriteToDebug("intMilliseconds="+(t=ConvertScorm2004TimeToMS(ConvertScorm2004TimeToMS))),t):(WriteToDebug("ERROR - Invalid Iso8601Time"),SCORM2004_SetErrorInfoManually(SCORM_ERROR_INVALID_TIMESPAN,"Invalid timespan received from LMS","strIso8601Time="+e),null)}function SCORM2004_DisplayMessageOnTimeout(){var e;return WriteToDebug("In SCORM2004_DisplayMessageOnTimeout"),SCORM2004_ClearErrorInfo(),WriteToDebug("strTLA="+(e=SCORM2004_CallGetValue("cmi.time_limit_action"))),e==SCORM2004_TLA_EXIT_MESSAGE||e==SCORM2004_TLA_CONTINUE_MESSAGE?(WriteToDebug("returning true"),!0):e==SCORM2004_TLA_EXIT_NO_MESSAGE||e==SCORM2004_TLA_CONTINUE_NO_MESSAGE||""==e?(WriteToDebug("returning false"),!1):(WriteToDebug("Error invalid TLA"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_TIME_LIMIT_ACTION,"Invalid time limit action received from LMS","strTLA="+e),null)}function SCORM2004_ExitOnTimeout(){var e;return WriteToDebug("In SCORM2004_ExitOnTimeout"),SCORM2004_ClearErrorInfo(),WriteToDebug("strTLA="+(e=SCORM2004_CallGetValue("cmi.time_limit_action"))),e==SCORM2004_TLA_EXIT_MESSAGE||e==SCORM2004_TLA_EXIT_NO_MESSAGE?(WriteToDebug("returning true"),!0):e==SCORM2004_TLA_CONTINUE_MESSAGE||e==SCORM2004_TLA_CONTINUE_NO_MESSAGE||""==e?(WriteToDebug("returning false"),!1):(WriteToDebug("ERROR invalid TLA"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_TIME_LIMIT_ACTION,"Invalid time limit action received from LMS","strTLA="+e),null)}function SCORM2004_GetPassingScore(){var e;return WriteToDebug("In SCORM2004_GetPassingScore"),SCORM2004_ClearErrorInfo(),WriteToDebug("fltScore="+(e=SCORM2004_CallGetValue("cmi.scaled_passing_score"))),""==e&&(e=0),IsValidDecimal(e)?(e=parseFloat(e),WriteToDebug("returning fltScore-"+(e*=100)),e):(WriteToDebug("Error - score is not a valid decimal"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_DECIMAL,"Invalid mastery score received from LMS","fltScore="+e),null)}function SCORM2004_SetScore(e,t,r){var n,i;return WriteToDebug("In SCORM2004_SetScore intScore="+(e=RoundToPrecision(e,7))+", intMaxScore="+(t=RoundToPrecision(t,7))+", intMinScore="+(r=RoundToPrecision(r,7))),SCORM2004_ClearErrorInfo(),i=RoundToPrecision(e/100,7),n=SCORM2004_CallSetValue("cmi.score.raw",e),n=SCORM2004_CallSetValue("cmi.score.max",t)&&n,n=SCORM2004_CallSetValue("cmi.score.min",r)&&n,WriteToDebug("Returning "+(n=SCORM2004_CallSetValue("cmi.score.scaled",i)&&n)),n}function SCORM2004_GetScore(){return WriteToDebug("In SCORM2004_GetScore"),SCORM2004_ClearErrorInfo(),SCORM2004_CallGetValue("cmi.score.raw")}function SCORM2004_GetScaledScore(){return WriteToDebug("In SCORM2004_GetScaledScore"),SCORM2004_ClearErrorInfo(),SCORM2004_CallGetValue("cmi.score.scaled")}function SCORM2004_RecordInteraction(e,t,r,n,i,o,a,s,u,c){var l,C,I;return IsNumeric(r)||(r=new String(r)),SCORM2004_ClearErrorInfo(),WriteToDebug("intInteractionIndex="+(C=SCORM2004_CallGetValue("cmi.interactions._count"))),""==C&&(WriteToDebug("Setting Interaction Index to 0"),C=0),WriteToDebug("strResult="+(I=1==r||"true"==r||r==INTERACTION_RESULT_CORRECT?SCORM2004_RESULT_CORRECT:"false"==String(r)||r==INTERACTION_RESULT_WRONG?SCORM2004_RESULT_WRONG:r==INTERACTION_RESULT_UNANTICIPATED?SCORM2004_RESULT_UNANTICIPATED:r==INTERACTION_RESULT_NEUTRAL?SCORM2004_RESULT_NEUTRAL:IsNumeric(r)?r:"")),l=SCORM2004_CallSetValue("cmi.interactions."+C+".id",e=CreateValidIdentifier(e)),l=SCORM2004_CallSetValue("cmi.interactions."+C+".type",c)&&l,null!==t&&(l=SCORM2004_CallSetValue("cmi.interactions."+C+".learner_response",t)&&l),null!=I&&null!=I&&""!=I&&(l=SCORM2004_CallSetValue("cmi.interactions."+C+".result",I)&&l),null!=n&&null!=n&&""!=n&&(l=SCORM2004_CallSetValue("cmi.interactions."+C+".correct_responses.0.pattern",n)&&l),null!=i&&null!=i&&""!=i&&(l=SCORM2004_CallSetValue("cmi.interactions."+C+".description",i)&&l),null!=o&&null!=o&&""!=o&&(l=SCORM2004_CallSetValue("cmi.interactions."+C+".weighting",o)&&l),null!=a&&null!=a&&""!=a&&(l=SCORM2004_CallSetValue("cmi.interactions."+C+".latency",ConvertMilliSecondsIntoSCORM2004Time(a))&&l),null!=s&&null!=s&&""!=s&&(l=SCORM2004_CallSetValue("cmi.interactions."+C+".objectives.0.id",s)&&l),WriteToDebug("Returning "+(l=SCORM2004_CallSetValue("cmi.interactions."+C+".timestamp",ConvertDateToIso8601TimeStamp(u))&&l)),l}function SCORM2004_RecordTrueFalseInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In SCORM2004_RecordTrueFalseInteraction strID="+e+", strResponse="+c+", blnCorrect="+r+", strCorrectResponse="+l+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l=null;return t?c="true":null!==t&&(c="false"),1==n?l="true":0==n&&(l="false"),SCORM2004_RecordInteraction(e,c,r,l,i,o,a,s,u,SCORM2004_INTERACTION_TYPE_TRUE_FALSE)}function SCORM2004_RecordMultipleChoiceInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In SCORM2004_RecordMultipleChoiceInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l="";if(null!==t){c="";for(var C=0;C0&&(c+="[,]"),c+=t[C].Long}for(C=0;C0&&(l+="[,]"),l+=n[C].Long;return SCORM2004_RecordInteraction(e,c,r,l,i,o,a,s,u,SCORM2004_INTERACTION_TYPE_CHOICE)}function SCORM2004_RecordFillInInteraction(e,t,r,n,i,o,a,s,u){var c;return WriteToDebug("In SCORM2004_RecordFillInInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u),null==n&&(n=""),c=(n=new String(n)).length>250||t.length>250?SCORM2004_INTERACTION_TYPE_LONG_FILL_IN:SCORM2004_INTERACTION_TYPE_FILL_IN,n.length>4e3&&(n=n.substr(0,4e3)),SCORM2004_RecordInteraction(e,t,r,n,i,o,a,s,u,c)}function SCORM2004_RecordMatchingInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In SCORM2004_RecordMatchingInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l="";if(null!==t){c="";for(var C=0;C0&&(c+="[,]"),c+=t[C].Source.Long+"[.]"+t[C].Target.Long}for(C=0;C0&&(l+="[,]"),l+=n[C].Source.Long+"[.]"+n[C].Target.Long;return SCORM2004_RecordInteraction(e,c,r,l,i,o,a,s,u,SCORM2004_INTERACTION_TYPE_MATCHING)}function SCORM2004_RecordPerformanceInteraction(e,t,r,n,i,o,a,s,u){return WriteToDebug("In SCORM2004_RecordPerformanceInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u),null!==t&&((t=new String(t)).length>250&&(t=t.substr(0,250)),t="[.]"+t),null==n&&(n=""),(n=new String(n)).length>250&&(n=n.substr(0,250)),SCORM2004_RecordInteraction(e,t,r,n="[.]"+n,i,o,a,s,u,SCORM2004_INTERACTION_TYPE_PERFORMANCE)}function SCORM2004_RecordSequencingInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In SCORM2004_RecordSequencingInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l="";if(null!==t){c="";for(var C=0;C0&&(c+="[,]"),c+=t[C].Long}for(C=0;C0&&(l+="[,]"),l+=n[C].Long;return SCORM2004_RecordInteraction(e,c,r,l,i,o,a,s,u,SCORM2004_INTERACTION_TYPE_SEQUENCING)}function SCORM2004_RecordLikertInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In RecordLikertInteraction strID="+e+", response="+t+", blnCorrect="+r+", correctResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l="";return null!==t&&(c=t.Long),null!=n&&(l=n.Long),SCORM2004_RecordInteraction(e,c,r,l,i,o,a,s,u,SCORM2004_INTERACTION_TYPE_LIKERT)}function SCORM2004_RecordNumericInteraction(e,t,r,n,i,o,a,s,u){if(WriteToDebug("In SCORM2004_RecordNumericInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u),null!=n&&null!=n){if(IsValidDecimal(n)&&WriteToDebug("SCORM2004_RecordNumericInteraction received decimal correct response and converted to range, strCorrectResponse="+(n=n+"[:]"+n)),!IsValidDecimalRange(n))return WriteToDebug("Returning False - SCORM2004_RecordNumericInteraction received invalid correct response decimal range, strCorrectResponse="+n),!1;if(null===ConvertDecimalRangeToDecimalBasedOnLearnerResponse(n,t,r))return WriteToDebug("Returning False - SCORM2004_RecordNumericInteraction received invalid correct response decimal range, response and correct indicator, strCorrectResponse="+n+", strResponse="+t+", blnCorrect="+r),!1}return SCORM2004_RecordInteraction(e,t,r,n,i,o,a,s,u,SCORM2004_INTERACTION_TYPE_NUMERIC)}function SCORM2004_GetEntryMode(){var e;return WriteToDebug("In SCORM2004_GetEntryMode"),SCORM2004_ClearErrorInfo(),WriteToDebug("strEntry="+(e=SCORM2004_CallGetValue("cmi.entry"))),e==SCORM2004_ENTRY_ABINITIO?(WriteToDebug("Returning first time"),ENTRY_FIRST_TIME):e==SCORM2004_ENTRY_RESUME?(WriteToDebug("Returning resume"),ENTRY_RESUME):e==SCORM2004_ENTRY_NORMAL?(WriteToDebug("returning normal"),ENTRY_REVIEW):(WriteToDebug("ERROR - invalid entry mode"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_ENTRY,"Invalid entry vocab received from LMS","strEntry="+e),null)}function SCORM2004_GetLessonMode(){var e;return WriteToDebug("In SCORM2004_GetLessonMode"),SCORM2004_ClearErrorInfo(),WriteToDebug("strLessonMode="+(e=SCORM2004_CallGetValue("cmi.mode"))),e==SCORM2004_BROWSE?(WriteToDebug("Returning browse"),MODE_BROWSE):e==SCORM2004_NORMAL?(WriteToDebug("returning normal"),MODE_NORMAL):e==SCORM2004_REVIEW?(WriteToDebug("Returning Review"),MODE_REVIEW):(WriteToDebug("ERROR - invalid lesson mode"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_LESSON_MODE,"Invalid lesson_mode vocab received from LMS","strLessonMode="+e),null)}function SCORM2004_GetTakingForCredit(){var e;return WriteToDebug("In SCORM2004_GetTakingForCredit"),SCORM2004_ClearErrorInfo(),WriteToDebug("strCredit="+(e=SCORM2004_CallGetValue("cmi.credit"))),"credit"==e?(WriteToDebug("Returning true"),!0):"no-credit"==e?(WriteToDebug("Returning false"),!1):(WriteToDebug("ERROR - invalid credit"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_CREDIT,"Invalid credit vocab received from LMS","strCredit="+e),null)}function SCORM2004_SetObjectiveScore(e,t,r,n){var i,o,a;return WriteToDebug("In SCORM2004_SetObjectiveScore, strObejctiveID="+e+", intScore="+(t=RoundToPrecision(t,7))+", intMaxScore="+(r=RoundToPrecision(r,7))+", intMinScore="+(n=RoundToPrecision(n,7))),SCORM2004_ClearErrorInfo(),WriteToDebug("intObjectiveIndex="+(i=SCORM2004_FindObjectiveIndexFromID(e))),a=RoundToPrecision(t/100,7),o=SCORM2004_CallSetValue("cmi.objectives."+i+".id",e),o=SCORM2004_CallSetValue("cmi.objectives."+i+".score.raw",t)&&o,o=SCORM2004_CallSetValue("cmi.objectives."+i+".score.max",r)&&o,o=SCORM2004_CallSetValue("cmi.objectives."+i+".score.min",n)&&o,WriteToDebug("Returning "+(o=SCORM2004_CallSetValue("cmi.objectives."+i+".score.scaled",a)&&o)),o}function SCORM2004_SetObjectiveStatus(e,t){var r,n,i="",o="";return WriteToDebug("In SCORM2004_SetObjectiveStatus strObjectiveID="+e+", Lesson_Status="+t),SCORM2004_ClearErrorInfo(),WriteToDebug("intObjectiveIndex="+(r=SCORM2004_FindObjectiveIndexFromID(e))),t==LESSON_STATUS_PASSED?(i=SCORM2004_PASSED,o=SCORM2004_COMPLETED):t==LESSON_STATUS_FAILED?(i=SCORM2004_FAILED,o=SCORM2004_COMPLETED):t==LESSON_STATUS_COMPLETED||t==LESSON_STATUS_BROWSED?(i=SCORM2004_UNKNOWN,o=SCORM2004_COMPLETED):t==LESSON_STATUS_INCOMPLETE?(i=SCORM2004_UNKNOWN,o=SCORM2004_INCOMPLETE):t==LESSON_STATUS_NOT_ATTEMPTED&&(i=SCORM2004_UNKNOWN,o=SCORM2004_NOT_ATTEMPTED),WriteToDebug("strSCORMSuccessStatus="+i),WriteToDebug("strSCORMCompletionStatus="+o),n=SCORM2004_CallSetValue("cmi.objectives."+r+".id",e),n=SCORM2004_CallSetValue("cmi.objectives."+r+".success_status",i)&&n,WriteToDebug("Returning "+(n=SCORM2004_CallSetValue("cmi.objectives."+r+".completion_status",o)&&n)),n}function SCORM2004_SetObjectiveDescription(e,t){var r;return WriteToDebug("In SCORM2004_SetObjectiveDescription strObjectiveID="+e+", strObjectiveDescription="+t),SCORM2004_ClearErrorInfo(),WriteToDebug("intObjectiveIndex="+(r=SCORM2004_FindObjectiveIndexFromID(e))),blnResult=SCORM2004_CallSetValue("cmi.objectives."+r+".id",e),blnResult=SCORM2004_CallSetValue("cmi.objectives."+r+".description",t)&&blnResult,WriteToDebug("Returning "+blnResult),blnResult}function SCORM2004_GetObjectiveScore(e){var t;return WriteToDebug("In SCORM2004_GetObjectiveScore, strObejctiveID="+e),SCORM2004_ClearErrorInfo(),WriteToDebug("intObjectiveIndex="+(t=SCORM2004_FindObjectiveIndexFromID(e))),SCORM2004_CallGetValue("cmi.objectives."+t+".score.raw")}function SCORM2004_GetObjectiveStatus(e){var t,r,n;return WriteToDebug("In SCORM2004_GetObjectiveStatus, strObejctiveID="+e),SCORM2004_ClearErrorInfo(),WriteToDebug("intObjectiveIndex="+(t=SCORM2004_FindObjectiveIndexFromID(e))),r=SCORM2004_CallGetValue("cmi.objectives."+t+".success_status"),n=SCORM2004_CallGetValue("cmi.objectives."+t+".completion_status"),r==SCORM2004_PASSED?(WriteToDebug("returning Passed"),LESSON_STATUS_PASSED):r==SCORM2004_FAILED?(WriteToDebug("Returning Failed"),LESSON_STATUS_FAILED):n==SCORM2004_COMPLETED?(WriteToDebug("Returning Completed"),LESSON_STATUS_COMPLETED):n==SCORM2004_INCOMPLETE?(WriteToDebug("Returning Incomplete"),LESSON_STATUS_INCOMPLETE):n==SCORM2004_NOT_ATTEMPTED||n==SCORM2004_UNKNOWN||""==n?(WriteToDebug("Returning Not Attempted"),LESSON_STATUS_NOT_ATTEMPTED):(WriteToDebug("ERROR - status not found"),SCORM2004_SetErrorInfoManually(SCORM2004_ERROR_INVALID_STATUS,"Invalid objective status received from LMS or initial status not yet recorded for objective","strCompletionStatus="+n),null)}function SCORM2004_GetObjectiveProgressMeasure(e){return SCORM2004_CallGetValue("cmi.objectives."+e+".progress_measure")}function SCORM2004_GetObjectiveDescription(e){var t;return WriteToDebug("In SCORM2004_GetObjectiveDescription, strObejctiveID="+e),SCORM2004_ClearErrorInfo(),WriteToDebug("intObjectiveIndex="+(t=SCORM2004_FindObjectiveIndexFromID(e))),strDescription=SCORM2004_CallGetValue("cmi.objectives."+t+".description"),strDescription}function SCORM2004_FindObjectiveIndexFromID(e){var t,r,n;if(WriteToDebug("In SCORM2004_FindObjectiveIndexFromID"),""==(t=SCORM2004_CallGetValue("cmi.objectives._count")))return WriteToDebug("Setting intCount=0"),0;for(WriteToDebug("intCount="+(t=parseInt(t,10))),r=0;ra&&(i=r,a=o));return i>=0?i:(WriteToDebug("Did not find match, returning null"),null)}function SCORM2004_GetInteractionType(e){var t;if(WriteToDebug("In SCORM2004_GetInteractionType, strInteractionID="+e),SCORM2004_ClearErrorInfo(),null==(t=SCORM2004_FindInteractionIndexFromID(e))||null==t)return null;switch(WriteToDebug("intInteractionIndex="+t),SCORM2004_CallGetValue("cmi.interactions."+t+".type")){case SCORM2004_INTERACTION_TYPE_FILL_IN:return INTERACTION_TYPE_FILL_IN;case SCORM2004_INTERACTION_TYPE_LONG_FILL_IN:return INTERACTION_TYPE_LONG_FILL_IN;case SCORM2004_INTERACTION_TYPE_CHOICE:return INTERACTION_TYPE_CHOICE;case SCORM2004_INTERACTION_TYPE_LIKERT:return INTERACTION_TYPE_LIKERT;case SCORM2004_INTERACTION_TYPE_MATCHING:return INTERACTION_TYPE_MATCHING;case SCORM2004_INTERACTION_TYPE_NUMERIC:return INTERACTION_TYPE_NUMERIC;case SCORM2004_INTERACTION_TYPE_PERFORMANCE:return INTERACTION_TYPE_PERFORMANCE;case SCORM2004_INTERACTION_TYPE_SEQUENCING:return INTERACTION_TYPE_SEQUENCING;case SCORM2004_INTERACTION_TYPE_TRUE_FALSE:return INTERACTION_TYPE_TRUE_FALSE;default:return""}}function SCORM2004_GetInteractionTimestamp(e){WriteToDebug("In SCORM2004_GetInteractionTimestamp, strInteractionID="+e);var t=SCORM2004_FindInteractionIndexFromID(e);return WriteToDebug("intInteractionIndex="+t),SCORM2004_ClearErrorInfo(),null==t||null==t?null:SCORM2004_CallGetValue(ConvertIso8601TimeStampToDate("cmi.interactions."+t+".timestamp"))}function SCORM2004_GetInteractionCorrectResponses(e){WriteToDebug("In SCORM2004_GetInteractionCorrectResponses, strInteractionID="+e);var t=SCORM2004_FindInteractionIndexFromID(e);if(WriteToDebug("intInteractionIndex="+t),SCORM2004_ClearErrorInfo(),null==t||null==t)return null;var r=SCORM2004_CallGetValue("cmi.interactions."+t+".type"),n=SCORM2004_CallGetValue("cmi.interactions."+t+".correct_responses._count");if(""==n)return WriteToDebug("Setting intCorrectResponseCount=0"),0;if(WriteToDebug("intCorrectResponseCount="+(n=parseInt(n,10))),0==n)return new Array;n>1&&WriteToDebug("SCORM Driver is not currently implemented to support multiple correct response combinations and will only return the first");var i=new String(SCORM2004_CallGetValue("cmi.interactions."+t+".correct_responses.0.pattern")).split("[,]");return WriteToDebug("aryResponse.length = "+i.length),WriteToDebug("aryResponse.length = "+(i=SCORM2004_ProcessResponseArray(r,i)).length),i}function SCORM2004_GetInteractionWeighting(e){WriteToDebug("In SCORM2004_GetInteractionWeighting, strInteractionID="+e);var t=SCORM2004_FindInteractionIndexFromID(e);return WriteToDebug("intInteractionIndex="+t),SCORM2004_ClearErrorInfo(),null==t||null==t?null:SCORM2004_CallGetValue("cmi.interactions."+t+".weighting")}function SCORM2004_GetInteractionLearnerResponses(e){WriteToDebug("In SCORM2004_GetInteractionLearnerResponses, strInteractionID="+e);var t=SCORM2004_FindInteractionIndexFromID(e);if(WriteToDebug("intInteractionIndex="+t),SCORM2004_ClearErrorInfo(),null==t||null==t)return null;var r=SCORM2004_CallGetValue("cmi.interactions."+t+".type"),n=new String(SCORM2004_CallGetValue("cmi.interactions."+t+".learner_response")).split("[,]");return WriteToDebug("aryResponses.length = "+n.length),n=SCORM2004_ProcessResponseArray(r,n)}function SCORM2004_ProcessResponseArray(e,t){WriteToDebug("Processing Response Array with "+t.length+" pieces");for(var r=0;r4096?(WriteToDebug("SCORM_SetDataChunk - suspend_data too large (4096 character limit for SCORM 1.2)"),!1):SCORM_CallLMSSetValue("cmi.suspend_data",e)}function SCORM_GetLaunchData(){return WriteToDebug("In SCORM_GetLaunchData"),SCORM_ClearErrorInfo(),SCORM_CallLMSGetValue("cmi.launch_data")}function SCORM_GetComments(){return WriteToDebug("In SCORM_GetComments"),SCORM_ClearErrorInfo(),SCORM_CallLMSGetValue("cmi.comments")}function SCORM_WriteComment(e){return WriteToDebug("In SCORM_WriteComment strComment="+e),SCORM_ClearErrorInfo(),SCORM_CallLMSSetValue("cmi.comments",e)}function SCORM_GetLMSComments(){return WriteToDebug("In SCORM_GetLMSComments"),SCORM_ClearErrorInfo(),SCORM_CallLMSGetValue("cmi.comments_from_lms")}function SCORM_GetAudioPlayPreference(){var e;return WriteToDebug("In SCORM_GetAudioPlayPreference"),SCORM_ClearErrorInfo(),""==(e=SCORM_CallLMSGetValue("cmi.student_preference.audio"))&&(e=0),WriteToDebug("intTempPreference="+(e=parseInt(e,10))),e>0?(WriteToDebug("Returning On"),PREFERENCE_ON):0==e?(WriteToDebug("Returning Default"),PREFERENCE_DEFAULT):e<0?(WriteToDebug("returning Off"),PREFERENCE_OFF):(WriteToDebug("Error: Invalid preference"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_PREFERENCE,"Invalid audio preference received from LMS","intTempPreference="+e),null)}function SCORM_GetAudioVolumePreference(){var e;return WriteToDebug("In SCORM_GetAudioVollumePreference"),SCORM_ClearErrorInfo(),WriteToDebug("intTempPreference="+(e=SCORM_CallLMSGetValue("cmi.student_preference.audio"))),""==e&&(e=100),(e=parseInt(e,10))<=0&&(WriteToDebug("Setting to 100"),e=100),e>0&&e<=100?(WriteToDebug("Returning "+e),e):(WriteToDebug("ERROR: invalid preference"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_PREFERENCE,"Invalid audio preference received from LMS","intTempPreference="+e),null)}function SCORM_SetAudioPreference(e,t){return WriteToDebug("In SCORM_SetAudioPreference PlayPreference="+e+", intPercentOfMaxVolume="+t),SCORM_ClearErrorInfo(),e==PREFERENCE_OFF&&(WriteToDebug("Setting percent to -1 - OFF"),t=-1),SCORM_CallLMSSetValue("cmi.student_preference.audio",t)}function SCORM_SetLanguagePreference(e){return WriteToDebug("In SCORM_SetLanguagePreference strLanguage="+e),SCORM_ClearErrorInfo(),SCORM_CallLMSSetValue("cmi.student_preference.language",e)}function SCORM_GetLanguagePreference(){return WriteToDebug("In SCORM_GetLanguagePreference"),SCORM_ClearErrorInfo(),SCORM_CallLMSGetValue("cmi.student_preference.language")}function SCORM_SetSpeedPreference(e){var t;return WriteToDebug("In SCORM_SetSpeedPreference intPercentOfMax="+e),SCORM_ClearErrorInfo(),WriteToDebug("intSCORMSpeed="+(t=2*e-100)),SCORM_CallLMSSetValue("cmi.student_preference.speed",t)}function SCORM_GetSpeedPreference(){var e,t;return WriteToDebug("In SCORM_GetSpeedPreference"),SCORM_ClearErrorInfo(),WriteToDebug("intSCORMSpeed="+(e=SCORM_CallLMSGetValue("cmi.student_preference.speed"))),""==e&&(WriteToDebug("Detected empty string, defaulting to 100"),e=100),ValidInteger(e)?(e=parseInt(e,10))<-100||e>100?(WriteToDebug("ERROR - out of range"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_SPEED,"Invalid speed preference received from LMS - out of range","intSCORMSpeed="+e),null):(t=(e+100)/2,WriteToDebug("Returning "+(t=parseInt(t,10))),t):(WriteToDebug("ERROR - invalid integer"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_SPEED,"Invalid speed preference received from LMS - not an integer","intSCORMSpeed="+e),null)}function SCORM_SetTextPreference(e){return WriteToDebug("In SCORM_SetTextPreference intPreference="+e),SCORM_ClearErrorInfo(),SCORM_CallLMSSetValue("cmi.student_preference.text",e)}function SCORM_GetTextPreference(){var e;return WriteToDebug("In SCORM_GetTextPreference"),SCORM_ClearErrorInfo(),e=SCORM_CallLMSGetValue("cmi.student_preference.text"),WriteToDebug("intTempPreference="+(e=parseInt(e,10))),e>0?(WriteToDebug("Returning On"),PREFERENCE_ON):0==e||""==e?(WriteToDebug("Returning Default"),PREFERENCE_DEFAULT):e<0?(WriteToDebug("returning Off"),PREFERENCE_OFF):(WriteToDebug("Error: Invalid preference"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_PREFERENCE,"Invalid text preference received from LMS","intTempPreference="+e),null)}function SCORM_GetPreviouslyAccumulatedTime(){var e,t;return WriteToDebug("In SCORM_GetPreviouslyAccumulatedTime"),SCORM_ClearErrorInfo(),WriteToDebug("strCMITime="+(e=SCORM_CallLMSGetValue("cmi.core.total_time"))),IsValidCMITimeSpan(e)?(WriteToDebug("Returning "+(t=ConvertCMITimeSpanToMS(e))),t):(WriteToDebug("ERROR - Invalid CMITimeSpan"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_TIMESPAN,"Invalid timespan received from LMS","strTime="+e),null)}function SCORM_SaveTime(e){var t;return WriteToDebug("In SCORM_SaveTime intMilliSeconds="+e),SCORM_ClearErrorInfo(),WriteToDebug("strCMITime="+(t=ConvertMilliSecondsToSCORMTime(e,!0))),SCORM_CallLMSSetValue("cmi.core.session_time",t)}function SCORM_GetMaxTimeAllowed(){var e,t;return WriteToDebug("In SCORM_GetMaxTimeAllowed"),SCORM_ClearErrorInfo(),WriteToDebug("strCMITime="+(e=SCORM_CallLMSGetValue("cmi.student_data.max_time_allowed"))),""==e&&(e="9999:99:99.99"),IsValidCMITimeSpan(e)?(WriteToDebug("intMilliseconds="+(t=ConvertCMITimeSpanToMS(e))),t):(WriteToDebug("ERROR - Invalid CMITimeSpan"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_TIMESPAN,"Invalid timespan received from LMS","strTime="+e),null)}function SCORM_DisplayMessageOnTimeout(){var e;return SCORM_ClearErrorInfo(),WriteToDebug("In SCORM_DisplayMessageOnTimeout"),WriteToDebug("strTLA="+(e=SCORM_CallLMSGetValue("cmi.student_data.time_limit_action"))),e==SCORM_TLA_EXIT_MESSAGE||e==SCORM_TLA_CONTINUE_MESSAGE?(WriteToDebug("returning true"),!0):e==SCORM_TLA_EXIT_NO_MESSAGE||e==SCORM_TLA_CONTINUE_NO_MESSAGE||""==e?(WriteToDebug("returning false"),!1):(WriteToDebug("Error invalid TLA"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_TIME_LIMIT_ACTION,"Invalid time limit action received from LMS","strTLA="+e),null)}function SCORM_ExitOnTimeout(){var e;return WriteToDebug("In SCORM_ExitOnTimeout"),SCORM_ClearErrorInfo(),WriteToDebug("strTLA="+(e=SCORM_CallLMSGetValue("cmi.student_data.time_limit_action"))),e==SCORM_TLA_EXIT_MESSAGE||e==SCORM_TLA_EXIT_NO_MESSAGE?(WriteToDebug("returning true"),!0):e==SCORM_TLA_CONTINUE_MESSAGE||e==SCORM_TLA_CONTINUE_NO_MESSAGE||""==e?(WriteToDebug("returning false"),!1):(WriteToDebug("ERROR invalid TLA"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_TIME_LIMIT_ACTION,"Invalid time limit action received from LMS","strTLA="+e),null)}function SCORM_GetPassingScore(){var e;return WriteToDebug("In SCORM_GetPassingScore"),SCORM_ClearErrorInfo(),WriteToDebug("fltScore="+(e=SCORM_CallLMSGetValue("cmi.student_data.mastery_score"))),""==e&&(e=0),IsValidDecimal(e)?(e=parseFloat(e),WriteToDebug("returning fltScore"),e):(WriteToDebug("Error - score is not a valid decimal"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_DECIMAL,"Invalid mastery score received from LMS","fltScore="+e),null)}function SCORM_SetScore(e,t,r){var n;return WriteToDebug("In SCORM_SetScore intScore="+(e=RoundToPrecision(e,7))+", intMaxScore="+(t=RoundToPrecision(t,7))+", intMinScore="+(r=RoundToPrecision(r,7))),SCORM_ClearErrorInfo(),n=SCORM_CallLMSSetValue("cmi.core.score.raw",e),n=SCORM_CallLMSSetValue("cmi.core.score.max",t)&&n,WriteToDebug("Returning "+(n=SCORM_CallLMSSetValue("cmi.core.score.min",r)&&n)),n}function SCORM_GetScore(){return WriteToDebug("In SCORM_GetScore"),SCORM_ClearErrorInfo(),SCORM_CallLMSGetValue("cmi.core.score.raw")}function SCORM_SetPointBasedScore(e,t,r){return WriteToDebug("SCORM_SetPointBasedScore - SCORM 1.1 and 1.2 do not support SetPointBasedScore, falling back to SetScore"),SCORM_SetScore(e,t,r)}function SCORM_GetScaledScore(e,t,r){return WriteToDebug("SCORM_GetScaledScore - SCORM 1.1 and 1.2 do not support GetScaledScore, returning false"),!1}function SCORM_RecordInteraction(e,t,r,n,i,o,a,s,u,c,l,C){var I,_,S,T;return SCORM_ClearErrorInfo(),WriteToDebug("intInteractionIndex="+(S=SCORM_CallLMSGetValue("cmi.interactions._count"))),""==S&&(WriteToDebug("Setting Interaction Index to 0"),S=0),IsNumeric(r)?T=r:1==r||r==INTERACTION_RESULT_CORRECT?T=SCORM_RESULT_CORRECT:""==r||"false"==r||r==INTERACTION_RESULT_WRONG?T=SCORM_RESULT_WRONG:r==INTERACTION_RESULT_UNANTICIPATED?T=SCORM_RESULT_UNANTICIPATED:r==INTERACTION_RESULT_NEUTRAL&&(T=SCORM_RESULT_NEUTRAL),WriteToDebug("strResult="+T),I=SCORM_CallLMSSetValue("cmi.interactions."+S+".id",e),I=SCORM_CallLMSSetValue("cmi.interactions."+S+".type",c)&&I,null!==t&&0==(_=SCORM_CallLMSSetValue("cmi.interactions."+S+".student_response",t))&&null!==l&&1==(_=SCORM_CallLMSSetValue("cmi.interactions."+S+".student_response",l))&&SCORM_ClearErrorInfo(),I=I&&_,null!=n&&null!=n&&""!=n&&(0==(_=SCORM_CallLMSSetValue("cmi.interactions."+S+".correct_responses.0.pattern",n))&&1==(_=SCORM_CallLMSSetValue("cmi.interactions."+S+".correct_responses.0.pattern",C))&&SCORM_ClearErrorInfo(),I=I&&_),null!=T&&null!=T&&""!=T&&(I=SCORM_CallLMSSetValue("cmi.interactions."+S+".result",T)&&I),null!=o&&null!=o&&""!=o&&(I=SCORM_CallLMSSetValue("cmi.interactions."+S+".weighting",o)&&I),null!=a&&null!=a&&""!=a&&(I=SCORM_CallLMSSetValue("cmi.interactions."+S+".latency",ConvertMilliSecondsToSCORMTime(a,!0))&&I),null!=s&&null!=s&&""!=s&&(I=SCORM_CallLMSSetValue("cmi.interactions."+S+".objectives.0.id",s)&&I),WriteToDebug("Returning "+(I=SCORM_CallLMSSetValue("cmi.interactions."+S+".time",ConvertDateToCMITime(u))&&I)),I}function SCORM_RecordTrueFalseInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In SCORM_RecordTrueFalseInteraction strID="+e+", strResponse="+c+", blnCorrect="+r+", strCorrectResponse="+l+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l=null;return 1==t?c="t":null!==t&&(c="f"),1==n?l="t":0==n&&(l="f"),SCORM_RecordInteraction(e,c,r,l,i,o,a,s,u,SCORM_INTERACTION_TYPE_TRUE_FALSE,c,l)}function SCORM_RecordMultipleChoiceInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In SCORM_RecordMultipleChoiceInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l=null,C="",I="";if(null!==t){c="",l="";for(var _=0;_0&&(c+=","),l.length>0&&(l+=","),c+=t[_].Short,l+=t[_].Long}for(_=0;_0&&(C+=","),I.length>0&&(I+=","),C+=n[_].Short,I+=n[_].Long;return SCORM_blnUsingProxyAPI?SCORM_RecordInteraction(e,c,r,C,i,o,a,s,u,SCORM_INTERACTION_TYPE_CHOICE,l,I):SCORM_RecordInteraction(e,l,r,I,i,o,a,s,u,SCORM_INTERACTION_TYPE_CHOICE,c,C)}function SCORM_RecordFillInInteraction(e,t,r,n,i,o,a,s,u){return WriteToDebug("In SCORM_RecordFillInInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u),(t=new String(t)).length>255&&(t=t.substr(0,255)),null==n&&(n=""),(n=new String(n)).length>255&&(n=n.substr(0,255)),SCORM_RecordInteraction(e,t,r,n,i,o,a,s,u,SCORM_INTERACTION_FILL_IN,t,n)}function SCORM_RecordMatchingInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In SCORM_RecordMatchingInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l=null,C="",I="";if(null!==t){c="",l="";for(var _=0;_0&&(c+=","),l.length>0&&(l+=","),c+=t[_].Source.Short+"."+t[_].Target.Short,l+=t[_].Source.Long+"."+t[_].Target.Long}for(_=0;_0&&(C+=","),I.length>0&&(I+=","),C+=n[_].Source.Short+"."+n[_].Target.Short,I+=n[_].Source.Long+"."+n[_].Target.Long;return SCORM_blnUsingProxyAPI?SCORM_RecordInteraction(e,c,r,C,i,o,a,s,u,SCORM_INTERACTION_TYPE_MATCHING,l,I):SCORM_RecordInteraction(e,l,r,I,i,o,a,s,u,SCORM_INTERACTION_TYPE_MATCHING,c,C)}function SCORM_RecordPerformanceInteraction(e,t,r,n,i,o,a,s,u){return WriteToDebug("In SCORM_RecordPerformanceInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u),null!==t&&(t=new String(t)).length>255&&(t=t.substr(0,255)),null==n&&(n=""),(n=new String(n)).length>255&&(n=n.substr(0,255)),SCORM_RecordInteraction(e,t,r,n,i,o,a,s,u,SCORM_INTERACTION_TYPE_PERFORMANCE,t,n)}function SCORM_RecordSequencingInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In SCORM_RecordSequencingInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l=null,C="",I="";if(null!==t){c="",l="";for(var _=0;_0&&(c+=","),l.length>0&&(l+=","),c+=t[_].Short,l+=t[_].Long}for(_=0;_0&&(C+=","),I.length>0&&(I+=","),C+=n[_].Short,I+=n[_].Long;return SCORM_blnUsingProxyAPI?SCORM_RecordInteraction(e,c,r,C,i,o,a,s,u,SCORM_INTERACTION_TYPE_SEQUENCING,l,I):SCORM_RecordInteraction(e,l,r,I,i,o,a,s,u,SCORM_INTERACTION_TYPE_SEQUENCING,c,C)}function SCORM_RecordLikertInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In SCORM_RecordLikertInteraction strID="+e+", response="+t+", blnCorrect="+r+", correctResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l=null,C="",I="";return null!==t&&(c=t.Short,l=t.Long),null!=n&&(C=n.Short,I=n.Long),SCORM_blnUsingProxyAPI?SCORM_RecordInteraction(e,c,r,C,i,o,a,s,u,SCORM_INTERACTION_TYPE_LIKERT,l,I):SCORM_RecordInteraction(e,l,r,I,i,o,a,s,u,SCORM_INTERACTION_TYPE_LIKERT,c,C)}function SCORM_RecordNumericInteraction(e,t,r,n,i,o,a,s,u){return WriteToDebug("In SCORM_RecordNumericInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u),null==n||null==n||(IsValidDecimalRange(n)&&(n=ConvertDecimalRangeToDecimalBasedOnLearnerResponse(n,t,r)),IsValidDecimal(n))?SCORM_RecordInteraction(e,t,r,n,i,o,a,s,u,SCORM_INTERACTION_TYPE_NUMERIC,t,n):(WriteToDebug("Returning False - SCORM_RecordNumericInteraction received invalid correct response (not a decimal), strCorrectResponse="+n),!1)}function SCORM_GetEntryMode(){var e;return WriteToDebug("In SCORM_GetEntryMode"),SCORM_ClearErrorInfo(),WriteToDebug("strEntry="+(e=SCORM_CallLMSGetValue("cmi.core.entry"))),e==SCORM_ENTRY_ABINITIO?(WriteToDebug("Returning first time"),ENTRY_FIRST_TIME):e==SCORM_ENTRY_RESUME?(WriteToDebug("Returning resume"),ENTRY_RESUME):e==SCORM_ENTRY_NORMAL?(WriteToDebug("returning normal"),ENTRY_REVIEW):(WriteToDebug("ERROR - invalide entry mode"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_ENTRY,"Invalid entry vocab received from LMS","strEntry="+e),null)}function SCORM_GetLessonMode(){var e;return WriteToDebug("In SCORM_GetLessonMode"),SCORM_ClearErrorInfo(),WriteToDebug("strLessonMode="+(e=SCORM_CallLMSGetValue("cmi.core.lesson_mode"))),e==SCORM_BROWSE?(WriteToDebug("Returning browse"),MODE_BROWSE):e==SCORM_NORMAL?(WriteToDebug("returning normal"),MODE_NORMAL):e==SCORM_REVIEW?(WriteToDebug("Returning Review"),MODE_REVIEW):(WriteToDebug("ERROR - invalid lesson mode"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_LESSON_MODE,"Invalid lesson_mode vocab received from LMS","strLessonMode="+e),null)}function SCORM_GetTakingForCredit(){var e;return WriteToDebug("In SCORM_GetTakingForCredit"),SCORM_ClearErrorInfo(),WriteToDebug("strCredit="+(e=SCORM_CallLMSGetValue("cmi.core.credit"))),"credit"==e?(WriteToDebug("Returning true"),!0):"no-credit"==e?(WriteToDebug("Returning false"),!1):(WriteToDebug("ERROR - invalid credit"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_CREDIT,"Invalid credit vocab received from LMS","strCredit="+e),null)}function SCORM_SetObjectiveScore(e,t,r,n){var i,o;return WriteToDebug("In SCORM_SetObjectiveScore, strObejctiveID="+e+", intScore="+(t=RoundToPrecision(t,7))+", intMaxScore="+(r=RoundToPrecision(r,7))+", intMinScore="+(n=RoundToPrecision(n,7))),SCORM_ClearErrorInfo(),WriteToDebug("intObjectiveIndex="+(i=SCORM_FindObjectiveIndexFromID(e))),o=SCORM_CallLMSSetValue("cmi.objectives."+i+".id",e),o=SCORM_CallLMSSetValue("cmi.objectives."+i+".score.raw",t)&&o,o=SCORM_CallLMSSetValue("cmi.objectives."+i+".score.max",r)&&o,WriteToDebug("Returning "+(o=SCORM_CallLMSSetValue("cmi.objectives."+i+".score.min",n)&&o)),o}function SCORM_SetObjectiveDescription(e,t){var r;return WriteToDebug("In SCORM_SetObjectiveDescription, strObjectiveDescription="+t),WriteToDebug("Objective Descriptions are not supported prior to SCORM 2004"),SCORM_ClearErrorInfo(),WriteToDebug("Returning "+(r=SCORM_TRUE)),r}function SCORM_SetObjectiveStatus(e,t){var r,n,i="";return WriteToDebug("In SCORM_SetObjectiveStatus strObjectiveID="+e+", Lesson_Status="+t),SCORM_ClearErrorInfo(),WriteToDebug("intObjectiveIndex="+(r=SCORM_FindObjectiveIndexFromID(e))),t==LESSON_STATUS_PASSED?i=SCORM_PASSED:t==LESSON_STATUS_FAILED?i=SCORM_FAILED:t==LESSON_STATUS_COMPLETED?i=SCORM_COMPLETED:t==LESSON_STATUS_BROWSED?i=SCORM_BROWSED:t==LESSON_STATUS_INCOMPLETE?i=SCORM_INCOMPLETE:t==LESSON_STATUS_NOT_ATTEMPTED&&(i=SCORM_NOT_ATTEMPTED),WriteToDebug("strSCORMStatus="+i),n=SCORM_CallLMSSetValue("cmi.objectives."+r+".id",e),WriteToDebug("Returning "+(n=SCORM_CallLMSSetValue("cmi.objectives."+r+".status",i)&&n)),n}function SCORM_GetObjectiveScore(e){var t;return WriteToDebug("In SCORM_GetObjectiveScore, strObejctiveID="+e),SCORM_ClearErrorInfo(),WriteToDebug("intObjectiveIndex="+(t=SCORM_FindObjectiveIndexFromID(e))),SCORM_CallLMSGetValue("cmi.objectives."+t+".score.raw")}function SCORM_GetObjectiveDescription(e){return WriteToDebug("In SCORM_GetObjectiveDescription, strObejctiveID="+e),WriteToDebug("ObjectiveDescription is not supported prior to SCORM 2004"),""}function SCORM_GetObjectiveStatus(e){var t,r;return WriteToDebug("In SCORM_GetObjectiveStatus, strObejctiveID="+e),SCORM_ClearErrorInfo(),WriteToDebug("intObjectiveIndex="+(t=SCORM_FindObjectiveIndexFromID(e))),(r=SCORM_CallLMSGetValue("cmi.objectives."+t+".status"))==SCORM_PASSED?(WriteToDebug("returning Passed"),LESSON_STATUS_PASSED):r==SCORM_FAILED?(WriteToDebug("Returning Failed"),LESSON_STATUS_FAILED):r==SCORM_COMPLETED?(WriteToDebug("Returning Completed"),LESSON_STATUS_COMPLETED):r==SCORM_BROWSED?(WriteToDebug("Returning Browsed"),LESSON_STATUS_BROWSED):r==SCORM_INCOMPLETE?(WriteToDebug("Returning Incomplete"),LESSON_STATUS_INCOMPLETE):r==SCORM_NOT_ATTEMPTED||""==r?(WriteToDebug("Returning Not Attempted"),LESSON_STATUS_NOT_ATTEMPTED):(WriteToDebug("ERROR - status not found"),SCORM_SetErrorInfoManually(SCORM_ERROR_INVALID_STATUS,"Invalid objective status received from LMS or initial status not yet recorded for objective","strStatus="+r),null)}function SCORM_FindObjectiveIndexFromID(e){var t,r,n;if(WriteToDebug("In SCORM_FindObjectiveIndexFromID"),""==(t=SCORM_CallLMSGetValue("cmi.objectives._count")))return WriteToDebug("Setting intCount=0"),0;for(WriteToDebug("intCount="+(t=parseInt(t,10))),r=0;r0&&(WriteToDebug("Saving Interactions"),KillTime(),AICC_SendInteractions()),ClearDirtyAICCData()),!0)}function KillTime(){if(WriteToDebug("In KillTime"),!1!==USE_AICC_KILL_TIME){var e=new Date;if(0==window.AICCComm.blnCanUseXMLHTTP)if(1==window.AICCComm.blnXMLHTTPIsAvailable)for(var t=0;t<3;t++)window.AICCComm.GetBlankHtmlPage(t);else{window.NothingFrame.document.open();for(t=0;t<1e3;t++)window.NothingFrame.document.write("waiting");window.NothingFrame.document.close()}WriteToDebug("Killed "+(new Date-e)+"milliseconds.")}else WriteToDebug("Configuration disallows use of KillTime, exiting")}function AICC_SendInteractions(){if(WriteToDebug("In AICC_SendInteractions."),!0===blnReviewModeSoReadOnly)return WriteToDebug("Mode is Review and configuration setting dictates this should be read only so exiting."),!0;var e=FormAICCInteractionsData();window.AICCComm.MakePutInteractionsRequest(e),AICC_aryInteractions=new Array}function AICC_GetStudentID(){return WriteToDebug("In AICC_GetStudentID, Returning "+AICC_Student_ID),AICC_Student_ID}function AICC_GetStudentName(){return WriteToDebug("In AICC_GetStudentName, Returning "+AICC_Student_Name),AICC_Student_Name}function AICC_GetBookmark(){return WriteToDebug("In AICC_GetBookmark, Returning "+AICC_Lesson_Location),AICC_Lesson_Location}function AICC_SetBookmark(e){return WriteToDebug("In AICC_SetBookmark, strBookmark="+e),SetDirtyAICCData(),AICC_Lesson_Location=e,!0}function AICC_GetDataChunk(){return WriteToDebug("In AICC_GetDataChunk, Returning "+AICC_Data_Chunk),AICC_Data_Chunk}function AICC_SetDataChunk(e){return WriteToDebug("In AICC_SetDataChunk, strData="+e),1==USE_STRICT_SUSPEND_DATA_LIMITS&&e.length>4096?(WriteToDebug("SCORM_SetDataChunk - suspend_data too large (4096 character limit for AICC)"),!1):(SetDirtyAICCData(),AICC_Data_Chunk=e,!0)}function AICC_GetLaunchData(){return WriteToDebug("In AICC_GetLaunchData, Returning "+AICC_Launch_Data),AICC_Launch_Data}function AICC_GetComments(){return WriteToDebug("In AICC_GetComments, Returning "+AICC_aryCommentsFromLearner.join(" | ")),AICC_aryCommentsFromLearner.join(" | ")}function AICC_WriteComment(e){var t;return WriteToDebug("In AICC_WriteComment, strComment="+e),0==e.search(/ \| /)&&(e=e.substr(3)),e.replace(/\|\|/g,"|"),t=AICC_aryCommentsFromLearner.length,WriteToDebug("Adding comment to array"),AICC_aryCommentsFromLearner[t]=e,SetDirtyAICCData(),!0}function AICC_GetLMSComments(){return WriteToDebug("In AICC_GetLMSComments, Returning "+AICC_Comments),AICC_Comments}function AICC_GetAudioPlayPreference(){return WriteToDebug("In AICC_GetAudioPlayPreference, Returning "+AICC_AudioPlayPreference),AICC_AudioPlayPreference}function AICC_GetAudioVolumePreference(){return WriteToDebug("In AICC_GetAudioVolumePreference, Returning "+AICC_intAudioVolume),AICC_intAudioVolume}function AICC_SetAudioPreference(e,t){return WriteToDebug("In AICC_SetAudioPreference, Returning true"),AICC_AudioPlayPreference=e,AICC_intAudioVolume=t,SetDirtyAICCData(),!0}function AICC_SetLanguagePreference(e){return WriteToDebug("In AICC_SetLanguagePreference, Returning true"),SetDirtyAICCData(),AICC_Language=e,!0}function AICC_GetLanguagePreference(){return WriteToDebug("In AICC_GetLanguagePreference, Returning "+AICC_Language),AICC_Language}function AICC_SetSpeedPreference(e){return WriteToDebug("In AICC_SetSpeedPreference, Returning true"),AICC_intPercentOfMaxSpeed=e,SetDirtyAICCData(),!0}function AICC_GetSpeedPreference(){return WriteToDebug("In AICC_GetSpeedPreference, Returning "+AICC_intPercentOfMaxSpeed),AICC_intPercentOfMaxSpeed}function AICC_SetTextPreference(e){return WriteToDebug("In AICC_SetTextPreference, Returning true"),AICC_TextPreference=e,SetDirtyAICCData(),!0}function AICC_GetTextPreference(){return WriteToDebug("In AICC_GetTextPreference, Returning "+AICC_TextPreference),AICC_TextPreference}function AICC_GetPreviouslyAccumulatedTime(){return WriteToDebug("In AICC_GetPreviouslyAccumulatedTime, Returning "+AICC_intPreviouslyAccumulatedMilliseconds),AICC_intPreviouslyAccumulatedMilliseconds}function AICC_SaveTime(e){return WriteToDebug("In intMilliSeconds, Returning true"),AICC_intSessionTimeMilliseconds=e,SetDirtyAICCData(),!0}function AICC_GetMaxTimeAllowed(){return WriteToDebug("In AICC_GetMaxTimeAllowed, Returning "+AICC_intMaxTimeAllowedMilliseconds),AICC_intMaxTimeAllowedMilliseconds}function AICC_DisplayMessageOnTimeout(){return WriteToDebug("In AICC_DisplayMessageOnTimeout, Returning "+AICC_blnShowMessageOnTimeout),AICC_blnShowMessageOnTimeout}function AICC_ExitOnTimeout(){return WriteToDebug("In AICC_ExitOnTimeout, Returning "+AICC_blnExitOnTimeout),AICC_blnExitOnTimeout}function AICC_GetPassingScore(){return WriteToDebug("In AICC_GetPassingScore, Returning "+AICC_Mastery_Score),AICC_Mastery_Score}function AICC_GetScore(){return WriteToDebug("In AICC_GetScore, Returning "+AICC_fltScoreRaw),AICC_fltScoreRaw}function AICC_SetScore(e,t,r){return WriteToDebug("In AICC_SetScore, fltScore="+e+", fltMaxScore="+t+", fltMinScore="+r),AICC_fltScoreRaw=e,AICC_fltScoreMax=t,AICC_fltScoreMin=r,SetDirtyAICCData(),!0}function AICC_RecordTrueFalseInteraction(e,t,r,n,i,o,a,s,u){var c;WriteToDebug("In AICC_RecordTrueFalseInteraction strID="+e+", blnResponse="+t+", blnCorrect="+r+", blnCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var l=new Array(10);c=AICC_aryInteractions.length,null!=o&&null!=o||(o=""),null!=a&&null!=a||(a=""),null!=r&&null!=r||(r="");var C="",I="";return null!==t&&(C=t?"t":"f"),1==n?I="t":0==n&&(I="f"),l[AICC_INTERACTIONS_ID]=e,l[AICC_INTERACTIONS_RESPONSE]=C,l[AICC_INTERACTIONS_CORRECT]=r,l[AICC_INTERACTIONS_CORRECT_RESPONSE]=I,l[AICC_INTERACTIONS_TIME_STAMP]=u,l[AICC_INTERACTIONS_TYPE]=AICC_INTERACTION_TYPE_TRUE_FALSE,l[AICC_INTERACTIONS_WEIGHTING]=o,l[AICC_INTERACTIONS_LATENCY]=a,l[AICC_INTERACTIONS_RESPONSE_LONG]=C,l[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]=I,AICC_aryInteractions[c]=l,WriteToDebug("Added to interactions array, index="+c),SetDirtyAICCData(),!0}function AICC_RecordMultipleChoiceInteraction(e,t,r,n,i,o,a,s,u){var c;WriteToDebug("In AICC_RecordMultipleChoiceInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var l=new Array(10);c=AICC_aryInteractions.length,null!=o&&null!=o||(o=""),null!=a&&null!=a||(a=""),null!=r&&null!=r||(r="");var C="",I="",_="",S="";if(null!==t)for(var T=0;T0&&(C+=","),I.length>0&&(I+=","),C+=t[T].Short.replace(",",""),I+=t[T].Long.replace(",","");for(T=0;T0&&(_+=","),S.length>0&&(S+=","),_+=n[T].Short.replace(",",""),S+=n[T].Long.replace(",","");return l[AICC_INTERACTIONS_ID]=e,l[AICC_INTERACTIONS_RESPONSE]=C,l[AICC_INTERACTIONS_CORRECT]=r,l[AICC_INTERACTIONS_CORRECT_RESPONSE]=_,l[AICC_INTERACTIONS_TIME_STAMP]=u,l[AICC_INTERACTIONS_TYPE]=AICC_INTERACTION_TYPE_CHOICE,l[AICC_INTERACTIONS_WEIGHTING]=o,l[AICC_INTERACTIONS_LATENCY]=a,l[AICC_INTERACTIONS_RESPONSE_LONG]=I,l[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]=S,AICC_aryInteractions[c]=l,WriteToDebug("Added to interactions array, index="+c),SetDirtyAICCData(),!0}function AICC_RecordFillInInteraction(e,t,r,n,i,o,a,s,u){var c;WriteToDebug("In AICC_RecordFillInInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var l=new Array(10);return c=AICC_aryInteractions.length,null!=o&&null!=o||(o=""),null!=a&&null!=a||(a=""),null!=r&&null!=r||(r=""),null===t&&(t=""),null!=n&&null!=n||(n=""),(t=new String(t)).length>255&&(t=t.substr(0,255)),(n=new String(n)).length>255&&(n=n.substr(0,255)),l[AICC_INTERACTIONS_ID]=e,l[AICC_INTERACTIONS_RESPONSE]=t,l[AICC_INTERACTIONS_CORRECT]=r,l[AICC_INTERACTIONS_CORRECT_RESPONSE]=n,l[AICC_INTERACTIONS_TIME_STAMP]=u,l[AICC_INTERACTIONS_TYPE]=AICC_INTERACTION_TYPE_FILL_IN,l[AICC_INTERACTIONS_WEIGHTING]=o,l[AICC_INTERACTIONS_LATENCY]=a,l[AICC_INTERACTIONS_RESPONSE_LONG]=t,l[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]=n,AICC_aryInteractions[c]=l,WriteToDebug("Added to interactions array, index="+c),SetDirtyAICCData(),!0}function AICC_RecordMatchingInteraction(e,t,r,n,i,o,a,s,u){var c;WriteToDebug("In AICC_RecordMatchingInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var l=new Array(10);c=AICC_aryInteractions.length,null!=o&&null!=o||(o=""),null!=a&&null!=a||(a=""),null!=r&&null!=r||(r="");var C="",I="",_="",S="";if(null!==t)for(var T=0;T0&&(C+=","),I.length>0&&(I+=","),C+=t[T].Source.Short.replace(",","").replace(".","")+"."+t[T].Target.Short.replace(",","").replace(".",""),I+=t[T].Source.Long.replace(",","").replace(".","")+"."+t[T].Target.Long.replace(",","").replace(".","");for(T=0;T0&&(_+=","),S.length>0&&(S+=","),""!=n[T].Source.Short&&""!=n[T].Source.Long&&(_+=n[T].Source.Short.replace(",","").replace(".","")+"."+n[T].Target.Short.replace(",","").replace(".",""),S+=n[T].Source.Long.replace(",","").replace(".","")+"."+n[T].Target.Long.replace(",","").replace(".",""));return l[AICC_INTERACTIONS_ID]=e,l[AICC_INTERACTIONS_RESPONSE]=C,l[AICC_INTERACTIONS_CORRECT]=r,l[AICC_INTERACTIONS_CORRECT_RESPONSE]=_,l[AICC_INTERACTIONS_TIME_STAMP]=u,l[AICC_INTERACTIONS_TYPE]=AICC_INTERACTION_TYPE_MATCHING,l[AICC_INTERACTIONS_WEIGHTING]=o,l[AICC_INTERACTIONS_LATENCY]=a,l[AICC_INTERACTIONS_RESPONSE_LONG]=I,l[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]=S,AICC_aryInteractions[c]=l,WriteToDebug("Added to interactions array, index="+c),SetDirtyAICCData(),!0}function AICC_RecordPerformanceInteraction(e,t,r,n,i,o,a,s,u){var c;WriteToDebug("In AICC_RecordPerformanceInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var l=new Array(10);return c=AICC_aryInteractions.length,null!=o&&null!=o||(o=""),null!=a&&null!=a||(a=""),null!=r&&null!=r||(r=""),null===t&&(t=""),null!=n&&null!=n||(n=""),(t=new String(t)).length>255&&(t=t.substr(0,255)),(n=new String(n)).length>255&&(n=n.substr(0,255)),l[AICC_INTERACTIONS_ID]=e,l[AICC_INTERACTIONS_RESPONSE]=t,l[AICC_INTERACTIONS_CORRECT]=r,l[AICC_INTERACTIONS_CORRECT_RESPONSE]=n,l[AICC_INTERACTIONS_TIME_STAMP]=u,l[AICC_INTERACTIONS_TYPE]=AICC_INTERACTION_TYPE_PERFORMANCE,l[AICC_INTERACTIONS_WEIGHTING]=o,l[AICC_INTERACTIONS_LATENCY]=a,l[AICC_INTERACTIONS_RESPONSE_LONG]=t,l[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]=n,AICC_aryInteractions[c]=l,WriteToDebug("Added to interactions array, index="+c),SetDirtyAICCData(),!0}function AICC_RecordSequencingInteraction(e,t,r,n,i,o,a,s,u){var c;WriteToDebug("In AICC_RecordSequencingInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var l=new Array(10);c=AICC_aryInteractions.length,null!=o&&null!=o||(o=""),null!=a&&null!=a||(a=""),null!=r&&null!=r||(r="");var C="",I="",_="",S="";if(null!==t)for(var T=0;T0&&(C+=","),I.length>0&&(I+=","),C+=t[T].Short.replace(",",""),I+=t[T].Long.replace(",","");for(T=0;T0&&(_+=","),S.length>0&&(S+=","),_+=n[T].Short.replace(",",""),S+=n[T].Long.replace(",","");return l[AICC_INTERACTIONS_ID]=e,l[AICC_INTERACTIONS_RESPONSE]=C,l[AICC_INTERACTIONS_CORRECT]=r,l[AICC_INTERACTIONS_CORRECT_RESPONSE]=_,l[AICC_INTERACTIONS_TIME_STAMP]=u,l[AICC_INTERACTIONS_TYPE]=AICC_INTERACTION_TYPE_SEQUENCING,l[AICC_INTERACTIONS_WEIGHTING]=o,l[AICC_INTERACTIONS_LATENCY]=a,l[AICC_INTERACTIONS_RESPONSE_LONG]=I,l[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]=S,AICC_aryInteractions[c]=l,WriteToDebug("Added to interactions array, index="+c),SetDirtyAICCData(),!0}function AICC_RecordLikertInteraction(e,t,r,n,i,o,a,s,u){var c;WriteToDebug("In RecordLikertInteraction strID="+e+", response="+t+", blnCorrect="+r+", correctResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var l=new Array(10);c=AICC_aryInteractions.length,null!=o&&null!=o||(o=""),null!=a&&null!=a||(a=""),null!=r&&null!=r||(r="");var C="",I="";null!==t&&(C=t.Short,I=t.Long);var _="",S="";return null!=n&&(_=n.Short,S=n.Long),l[AICC_INTERACTIONS_ID]=e,l[AICC_INTERACTIONS_RESPONSE]=C,l[AICC_INTERACTIONS_CORRECT]=r,l[AICC_INTERACTIONS_CORRECT_RESPONSE]=_,l[AICC_INTERACTIONS_TIME_STAMP]=u,l[AICC_INTERACTIONS_TYPE]=AICC_INTERACTION_TYPE_LIKERT,l[AICC_INTERACTIONS_WEIGHTING]=o,l[AICC_INTERACTIONS_LATENCY]=a,l[AICC_INTERACTIONS_RESPONSE_LONG]=I,l[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]=S,AICC_aryInteractions[c]=l,WriteToDebug("Added to interactions array, index="+c),SetDirtyAICCData(),!0}function AICC_RecordNumericInteraction(e,t,r,n,i,o,a,s,u){var c;WriteToDebug("In AICC_RecordNumericInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var l=new Array(10);if(c=AICC_aryInteractions.length,null!=o&&null!=o||(o=""),null!=a&&null!=a||(a=""),null!=r&&null!=r||(r=""),null===t&&(t=""),null!=n&&null!=n){if(IsValidDecimalRange(n)&&(n=ConvertDecimalRangeToDecimalBasedOnLearnerResponse(n,t,r)),!IsValidDecimal(n))return WriteToDebug("Returning False - AICC_RecordNumericInteraction received invalid correct response (not a decimal), strCorrectResponse="+n),!1}else n="";return l[AICC_INTERACTIONS_ID]=e,l[AICC_INTERACTIONS_RESPONSE]=t,l[AICC_INTERACTIONS_CORRECT]=r,l[AICC_INTERACTIONS_CORRECT_RESPONSE]=n,l[AICC_INTERACTIONS_TIME_STAMP]=u,l[AICC_INTERACTIONS_TYPE]=AICC_INTERACTION_TYPE_NUMERIC,l[AICC_INTERACTIONS_WEIGHTING]=o,l[AICC_INTERACTIONS_LATENCY]=a,l[AICC_INTERACTIONS_RESPONSE_LONG]=t,l[AICC_INTERACTIONS_CORRECT_RESPONSE_LONG]=n,AICC_aryInteractions[c]=l,WriteToDebug("Added to interactions array, index="+c),SetDirtyAICCData(),!0}function AICC_GetEntryMode(){return WriteToDebug("In AICC_GetEntryMode, Returning "+AICC_Entry),AICC_Entry}function AICC_GetLessonMode(){return WriteToDebug("In AICC_GetLessonMode, Returning "+AICC_strLessonMode),AICC_strLessonMode}function AICC_GetTakingForCredit(){return WriteToDebug("In AICC_GetTakingForCredit, Returning "+AICC_blnCredit),AICC_blnCredit}function AICC_SetObjectiveScore(e,t,r,n){var i,o;WriteToDebug("In AICC_SetObjectiveScore, strObjectiveID="+e+", intScore="+t+", intMaxScore="+r+", intMinScore="+n);var a="";return null!=(o=FindObjectiveById(e,AICC_aryObjectivesRead))?(WriteToDebug("Found read objective"),AICC_aryObjectivesRead[o][AICC_OBJ_ARRAY_SCORE]=t):(WriteToDebug("Adding new read objective"),i=AICC_aryObjectivesRead.length,AICC_aryObjectivesRead[parseInt(i,10)]=new Array(3),AICC_aryObjectivesRead[parseInt(i,10)][AICC_OBJ_ARRAY_ID]=e,AICC_aryObjectivesRead[parseInt(i,10)][AICC_OBJ_ARRAY_SCORE]=t,AICC_aryObjectivesRead[parseInt(i,10)][AICC_OBJ_ARRAY_STATUS]=""),null!=(o=FindObjectiveById(e,AICC_aryObjectivesWrite))?(WriteToDebug("Found write objective"),AICC_aryObjectivesWrite[o][AICC_OBJ_ARRAY_SCORE]=t):(WriteToDebug("Adding new write objective"),i=AICC_aryObjectivesWrite.length,AICC_aryObjectivesWrite[parseInt(i,10)]=new Array(3),a=t,AICC_LMS_Version<3&&""!=a&&(a=parseInt(a,10)),(null==AICC_REPORT_MIN_MAX_SCORE||!0===AICC_REPORT_MIN_MAX_SCORE)&&AICC_LMS_Version>=3&&(""==r&&""==n||(WriteToDebug("Appending Max and Min scores"),a+=","+r+","+n)),AICC_aryObjectivesWrite[parseInt(i,10)][AICC_OBJ_ARRAY_ID]=e,AICC_aryObjectivesWrite[parseInt(i,10)][AICC_OBJ_ARRAY_SCORE]=a,AICC_aryObjectivesWrite[parseInt(i,10)][AICC_OBJ_ARRAY_STATUS]=""),SetDirtyAICCData(),!0}function AICC_SetObjectiveStatus(e,t){var r,n;return WriteToDebug("In AICC_SetObjectiveStatus, strObjectiveID="+e+", Lesson_Status="+t),null!=(n=FindObjectiveById(e,AICC_aryObjectivesRead))?(WriteToDebug("Found read objective"),AICC_aryObjectivesRead[n][AICC_OBJ_ARRAY_STATUS]=t):(WriteToDebug("Adding new read objective"),r=AICC_aryObjectivesRead.length,AICC_aryObjectivesRead[parseInt(r,10)]=new Array(3),AICC_aryObjectivesRead[parseInt(r,10)][AICC_OBJ_ARRAY_ID]=e,AICC_aryObjectivesRead[parseInt(r,10)][AICC_OBJ_ARRAY_STATUS]=t,AICC_aryObjectivesRead[parseInt(r,10)][AICC_OBJ_ARRAY_SCORE]=""),null!=(n=FindObjectiveById(e,AICC_aryObjectivesWrite))?(WriteToDebug("Found write objective"),AICC_aryObjectivesWrite[n][AICC_OBJ_ARRAY_STATUS]=t):(WriteToDebug("Adding new write objective"),r=AICC_aryObjectivesWrite.length,AICC_aryObjectivesWrite[parseInt(r,10)]=new Array(3),AICC_aryObjectivesWrite[parseInt(r,10)][AICC_OBJ_ARRAY_ID]=e,AICC_aryObjectivesWrite[parseInt(r,10)][AICC_OBJ_ARRAY_STATUS]=t,AICC_aryObjectivesWrite[parseInt(r,10)][AICC_OBJ_ARRAY_SCORE]=""),SetDirtyAICCData(),!0}function AICC_SetObjectiveDescription(e,t){return WriteToDebug("In AICC_SetObjectiveDescription, strObjectiveID="+e+", strObjectiveDescription="+t),WriteToDebug("Objective descriptions are not supported prior to SCORM 2004"),!0}function AICC_GetObjectiveScore(e){WriteToDebug("In AICC_SetObjectiveScore, strObjectiveID="+e);var t=FindObjectiveById(e,AICC_aryObjectivesRead);return null!=t?(WriteToDebug("Found objective, returning "+AICC_aryObjectivesRead[t][AICC_OBJ_ARRAY_SCORE]),AICC_aryObjectivesRead[t][AICC_OBJ_ARRAY_SCORE]):(WriteToDebug("Did not find objective, returning ''"),"")}function AICC_GetObjectiveDescription(e){return WriteToDebug("In AICC_GetObjectiveDescription, strObjectiveID="+e),WriteToDebug("Objective descriptions are not supported prior to SCORM 2004"),""}function AICC_GetObjectiveStatus(e){WriteToDebug("In AICC_SetObjectiveStatus, strObjectiveID="+e);var t=FindObjectiveById(e,AICC_aryObjectivesRead);return null!=t?(WriteToDebug("Found objective, returning "+AICC_aryObjectivesRead[t][AICC_OBJ_ARRAY_STATUS]),AICC_aryObjectivesRead[t][AICC_OBJ_ARRAY_STATUS]):(WriteToDebug("Did not find objective, returning "+LESSON_STATUS_NOT_ATTEMPTED),LESSON_STATUS_NOT_ATTEMPTED)}function AICC_SetFailed(){return WriteToDebug("In AICC_SetFailed, Returning true"),AICC_Status=LESSON_STATUS_FAILED,SetDirtyAICCData(),!0}function AICC_SetPassed(){return WriteToDebug("In AICC_SetPassed, Returning true"),AICC_Status=LESSON_STATUS_PASSED,SetDirtyAICCData(),!0}function AICC_SetCompleted(){return WriteToDebug("In AICC_SetCompleted, Returning true"),AICC_Status=LESSON_STATUS_COMPLETED,SetDirtyAICCData(),!0}function AICC_ResetStatus(){return WriteToDebug("In AICC_ResetStatus, Returning true"),AICC_Status=LESSON_STATUS_INCOMPLETE,SetDirtyAICCData(),!0}function AICC_GetStatus(){return WriteToDebug("In AICC_GetStatus, Returning "+AICC_Status),AICC_Status}function AICC_GetProgressMeasure(){return WriteToDebug("AICC_GetProgressMeasure - AICC does not support progress_measure, returning false"),!1}function AICC_SetProgressMeasure(){return WriteToDebug("AICC_SetProgressMeasure - AICC does not support progress_measure, returning false"),!1}function AICC_GetObjectiveProgressMeasure(){return WriteToDebug("AICC_GetObjectiveProgressMeasure - AICC does not support progress_measure, returning false"),!1}function AICC_SetObjectiveProgressMeasure(){return WriteToDebug("AICC_SetObjectiveProgressMeasure - AICC does not support progress_measure, returning false"),!1}function AICC_SetPointBasedScore(e,t,r){return WriteToDebug("AICC_SetPointBasedScore - AICC does not support SetPointBasedScore, falling back to SetScore"),AICC_SetScore(e,t,r)}function AICC_GetScaledScore(e,t,r){return WriteToDebug("AICC_GetScaledScore - AICC does not support GetScaledScore, returning false"),!1}function AICC_GetLastError(){return WriteToDebug("In AICC_GetLastError, Returning "+intAICCErrorNum),intAICCErrorNum}function AICC_GetLastErrorDesc(){return WriteToDebug("In AICC_GetLastErrorDesc, Returning '"+strAICCErrorDesc+"'"),strAICCErrorDesc}function AICC_PutParamFailed(){WriteToDebug("ERROR: In AICC_PutParamFailed"),SetDirtyAICCData()}function AICC_PutInteractionsFailed(){WriteToDebug("ERROR: In AICC_PutInteractionsFailed"),SetDirtyAICCData(),1==parent.blnUseLongInteractionResultValues&&(parent.blnUseLongInteractionResultValues=!1,parent.AICC_CommitData())}function AICC_SetErrorInfo(e,t){WriteToDebug("ERROR: In AICC_SetErrorInfo, strErrorNumLine="+e+", strErrorDescLine="+t),-1==e.toLowerCase().search(/error\s*=\s*0/)?(WriteToDebug("Detected No Error"),intAICCErrorNum=NO_ERROR,strAICCErrorDesc=""):(WriteToDebug("Setting Error Info"),AICC_SetError(GetValueFromAICCLine(strAICCErrorLine),GetValueFromAICCLine(strAICCErrorDesc)))}function AICC_SetError(e,t){WriteToDebug("ERROR: In AICC_SetError, intErrorNum="+e+", strErrorDesc="+t),intAICCErrorNum=e}function SetDirtyAICCData(){WriteToDebug("In SetDirtyAICCData"),blnDirtyAICCData=!0}function ClearDirtyAICCData(){WriteToDebug("In ClearDirtyAICCData"),blnDirtyAICCData=!1}function IsThereDirtyAICCData(){return WriteToDebug("In IsThereDirtyAICCData, returning "+blnDirtyAICCData),blnDirtyAICCData}function GetValueFromAICCLine(e){var t;WriteToDebug("In GetValueFromAICCLine, strLine="+e);var r,n="";return WriteToDebug("intPos="+(t=(e=new String(e)).indexOf("="))),t>-1&&t+1-1&&t-1&&(WriteToDebug("Replacing []"),WriteToDebug("strTemp="+(r=e.replace(/[\[|\]]/g,""))),n=r=(r=r.replace(/^\s*/,"")).replace(/\s*$/,""))),WriteToDebug("returning "+n),n}function GetIndexFromAICCName(e){var t;WriteToDebug("In GetIndexFromAICCName, strLineName="+e);var r="",n="";return strLine=new String(e),WriteToDebug("intPos="+(t=strLine.indexOf("."))),t>-1&&t+1-1&&t0&&(WriteToDebug("Found non-zero length string"),"\r"==r.charAt(0)&&(WriteToDebug("Detected leading \\r"),r=r.substr(1)),"\r"==r.charAt(r.length-1)&&(WriteToDebug("Detected trailing \\r"),r=r.substr(0,r.length-1)),";"!=r.charAt(0)&&(WriteToDebug("Found non-comment line"),WriteToDebug("strLineName="+(n=GetNameFromAICCLine(r))+", strLineValue="+(i=GetValueFromAICCLine(r))))),!AICC_HasItemBeenFound(n=n.toLowerCase()))switch(WriteToDebug("Detected an un-found item"),AICC_FoundItem(n),n){case"version":WriteToDebug("Item is version");var s=parseFloat(i);isNaN(s)&&(s=0),AICC_LMS_Version=s;break;case"student_id":WriteToDebug("Item is student_id"),AICC_Student_ID=i;break;case"student_name":WriteToDebug("Item is student_name"),AICC_Student_Name=i;break;case"lesson_location":WriteToDebug("Item is lesson_location"),AICC_Lesson_Location=i;break;case"score":WriteToDebug("Item is score"),AICC_SeperateScoreValues(AICC_Score=i);break;case"credit":WriteToDebug("Item is credit"),AICC_TranslateCredit(AICC_Credit=i);break;case"lesson_status":WriteToDebug("Item is lesson_status"),AICC_TranslateLessonStatus(AICC_Lesson_Status=i);break;case"time":WriteToDebug("Item is time"),AICC_TranslateTimeToMilliseconds(AICC_Time=i);break;case"mastery_score":WriteToDebug("Item is mastery_score"),AICC_ValidateMasteryScore(AICC_Mastery_Score=i);break;case"lesson_mode":WriteToDebug("Item is lesson_mode"),AICC_TranslateLessonMode(AICC_Lesson_Mode=i);break;case"max_time_allowed":WriteToDebug("Item is max_time_allowed"),AICC_TranslateMaxTimeToMilliseconds(AICC_Max_Time_Allowed=i);break;case"time_limit_action":WriteToDebug("Item is time_limit_action"),AICC_TranslateTimeLimitAction(AICC_Time_Limit_Action=i);break;case"audio":WriteToDebug("Item is audio"),AICC_TranslateAudio(AICC_Audio=i);break;case"speed":WriteToDebug("Item is speed"),AICC_TranslateSpeed(AICC_Speed=i);break;case"language":WriteToDebug("Item is language"),AICC_Language=i;break;case"text":WriteToDebug("Item is text"),AICC_TranslateTextPreference(AICC_Text=i);break;case"course_id":WriteToDebug("Item is course id"),AICC_CourseID=i;break;case"core_vendor":for(WriteToDebug("Item is core_vendor"),AICC_Launch_Data="",r="",o+(a=1)1&&(WriteToDebug("Max score found"),IsValidDecimal(AICC_fltScoreMax=aryScore[1])?(WriteToDebug("Found a valid decimal"),AICC_fltScoreMax=parseFloat(AICC_fltScoreMax)):(WriteToDebug("ERROR - max score from LMS is not a valid decimal"),AICC_SetError(AICC_ERROR_INVALID_DECIMAL,"max score is not a valid decimal"))),aryScore.length>2&&(WriteToDebug("Max score found"),IsValidDecimal(AICC_fltScoreMin=aryScore[2])?(WriteToDebug("Found a valid decimal"),AICC_fltScoreMin=parseFloat(AICC_fltScoreMin)):(WriteToDebug("ERROR - min score from LMS is not a valid decimal"),AICC_SetError(AICC_ERROR_INVALID_DECIMAL,"min score is not a valid decimal")))}function AICC_ValidateMasteryScore(e){WriteToDebug("In AICC_ValidateMasteryScore, strScore="+e),IsValidDecimal(e)?AICC_Mastery_Score=parseFloat(e):(WriteToDebug("ERROR - mastery score from LMS is not a valid decimal"),AICC_SetError(AICC_ERROR_INVALID_DECIMAL,"mastery score is not a valid decimal"))}function AICC_TranslateCredit(e){var t;WriteToDebug("In AICC_TranslateCredit, strCredit="+e),"c"==(t=e.toLowerCase().charAt(0))?(WriteToDebug("Credit = true"),AICC_blnCredit=!0):"n"==t?(WriteToDebug("Credit = false"),AICC_blnCredit=!1):(WriteToDebug("ERROR - credit value from LMS is not a valid"),AICC_SetError(AICC_ERROR_INVALID_CREDIT,"credit value from LMS is not a valid"))}function AICC_TranslateLessonMode(e){var t;WriteToDebug("In AICC_TranslateLessonMode, strMode="+e),"b"==(t=e.toLowerCase().charAt(0))?(WriteToDebug("Lesson Mode = Browse"),AICC_strLessonMode=MODE_BROWSE):"n"==t?(WriteToDebug("Lesson Mode = normal"),AICC_strLessonMode=MODE_NORMAL):"r"==t?(WriteToDebug("Lesson Mode = review"),AICC_strLessonMode=MODE_REVIEW,void 0!==REVIEW_MODE_IS_READ_ONLY&&!0===REVIEW_MODE_IS_READ_ONLY&&(blnReviewModeSoReadOnly=!0)):(WriteToDebug("ERROR - lesson_mode value from LMS is not a valid"),AICC_SetError(AICC_ERROR_INVALID_LESSON_MODE,"lesson_mode value from LMS is not a valid"))}function AICC_TranslateTimeToMilliseconds(e){WriteToDebug("In AICC_TranslateTimeToMilliseconds, strCMITime="+e),IsValidCMITimeSpan(e)?AICC_intPreviouslyAccumulatedMilliseconds=ConvertCMITimeSpanToMS(e):(WriteToDebug("ERROR - Invalid CMITimeSpan"),AICC_SetError(AICC_ERROR_INVALID_TIMESPAN,"Invalid timespan (previously accumulated time) received from LMS"))}function AICC_TranslateMaxTimeToMilliseconds(e){WriteToDebug("In AICC_TranslateMaxTimeToMilliseconds, strCMITime="+e),IsValidCMITimeSpan(e)?AICC_intMaxTimeAllowedMilliseconds=ConvertCMITimeSpanToMS(e):(WriteToDebug("ERROR - Invalid CMITimeSpan"),AICC_SetError(AICC_ERROR_INVALID_TIMESPAN,"Invalid timespan (max time allowed) received from LMS"))}function AICC_TranslateTimeLimitAction(e){var t;WriteToDebug("In AICC_TranslateTimeLimitAction, strTimeLimitAction="+e);var r=!1,n="",i="";2==(t=e.split(",")).length?(WriteToDebug("Found 2 elements"),WriteToDebug("Got characters, strChar1="+(n=t[0].charAt(0).toLowerCase())+", strChar2="+(i=t[1].charAt(0).toLowerCase())),("e"!=n&&"c"!=n&&"m"!=n&&"n"!=n||"e"!=i&&"c"!=i&&"m"!=i&&"n"!=i||n==i)&&(r=!0,WriteToDebug("Found an invalid character, or 2 identical characters")),"e"!=n&&"e"!=i||(AICC_blnExitOnTimeout=!0),"c"!=n&&"c"!=i||(AICC_blnExitOnTimeout=!1),"n"!=n&&"n"!=i||(AICC_blnShowMessageOnTimeout=!1),"m"!=n&&"m"!=i||(AICC_blnShowMessageOnTimeout=!0),WriteToDebug("AICC_blnExitOnTimeout="+AICC_blnExitOnTimeout+", AICC_blnShowMessageOnTimeout"+AICC_blnShowMessageOnTimeout)):(WriteToDebug("Line does not contain two comma-delimited elements"),r=!0),r&&(WriteToDebug("ERROR - Invalid Time Limit Action"),AICC_SetError(AICC_ERROR_INVALID_TIME_LIMIT_ACTION,"Invalid time limit action received from LMS"))}function AICC_TranslateTextPreference(e){WriteToDebug("In AICC_TranslateTextPreference, strPreference="+e),-1==e?(WriteToDebug("Text Preference = off"),AICC_TextPreference=PREFERENCE_OFF):0==e?(WriteToDebug("Text Preference = default"),AICC_TextPreference=PREFERENCE_DEFAULT):1==e?(WriteToDebug("Text Preference = on"),AICC_TextPreference=PREFERENCE_ON):(WriteToDebug("ERROR - Invalid Text Preference"),AICC_SetError(AICC_ERROR_INVALID_PREFERENCE,"Invalid Text Preference received from LMS"))}function AICC_TranslateLessonStatus(e){var t,r;WriteToDebug("In AICC_TranslateLessonStatus, strStatus="+e),t=e.charAt(0).toLowerCase(),WriteToDebug("AICC_Status="+(AICC_Status=AICC_ConvertAICCStatusIntoLocalStatus(t))),(r=e.indexOf(","))>0&&("a"==(t=e.substr(r).replace(/,/,"").charAt(0).toLowerCase())?(WriteToDebug("Entry is Ab initio"),AICC_Entry=ENTRY_FIRST_TIME):"r"==t?(WriteToDebug("Entry is Resume"),AICC_Entry=ENTRY_RESUME):(WriteToDebug("ERROR - entry not found"),AICC_SetError(AICC_ERROR_INVALID_ENTRY,"Invalid lesson status received from LMS")))}function AICC_ConvertAICCStatusIntoLocalStatus(e){return WriteToDebug("In AICC_ConvertAICCStatusIntoLocalStatus, strFirstCharOfAICCStatus="+e),"p"==e?(WriteToDebug("Status is Passed"),LESSON_STATUS_PASSED):"f"==e?(WriteToDebug("Status is Failed"),LESSON_STATUS_FAILED):"c"==e?(WriteToDebug("Status is Completed"),LESSON_STATUS_COMPLETED):"b"==e?(WriteToDebug("Status is Browsed"),LESSON_STATUS_BROWSED):"i"==e?(WriteToDebug("Status is Incomplete"),LESSON_STATUS_INCOMPLETE):"n"==e?(WriteToDebug("Status is Not Attempted"),LESSON_STATUS_NOT_ATTEMPTED):(WriteToDebug("ERROR - status not found"),AICC_SetError(SCORM_ERROR_INVALID_STATUS,"Invalid status"),LESSON_STATUS_NOT_ATTEMPTED)}function AICC_TranslateAudio(e){WriteToDebug("In AICC_TranslateAudio, strAudio="+e);var t=parseInt(e,10);WriteToDebug("intTempPreference="+t),t>0&&t<=100?(WriteToDebug("Returning On"),AICC_AudioPlayPreference=PREFERENCE_ON,AICC_intAudioVolume=t):0==t?(WriteToDebug("Returning Default"),AICC_AudioPlayPreference=PREFERENCE_DEFAULT):t<0?(WriteToDebug("returning Off"),AICC_AudioPlayPreference=PREFERENCE_OFF):(WriteToDebug("Error: Invalid preference"),AICC_SetError(AICC_ERROR_INVALID_PREFERENCE,"Invalid audio preference received from LMS"))}function AICC_TranslateSpeed(e){var t;return WriteToDebug("In AICC_TranslateSpeed, intAICCSpeed="+e),ValidInteger(e)?(e=parseInt(e,10))<-100||e>100?(WriteToDebug("ERROR - out of range"),void AICC_SetError(AICC_ERROR_INVALID_SPEED,"Invalid speed preference received from LMS - out of range")):(AICC_Speed=e,t=(e+100)/2,WriteToDebug("Returning "+(t=parseInt(t,10))),void(AICC_intPercentOfMaxSpeed=t)):(WriteToDebug("ERROR - invalid integer"),void AICC_SetError(AICC_ERROR_INVALID_SPEED,"Invalid speed preference received from LMS - not an integer"))}function AICC_FormatObjectives(e){var t,r,n,i,o,a;for(WriteToDebug("In AICC_FormatObjectives, strObjectivesFromLMS="+e),t=e.split("\n"),r=0;r=3&&(WriteToDebug("Using max and min values if available."),""==AICC_fltScoreMax&&""==AICC_fltScoreMin||(WriteToDebug("Appending Max and Min scores"),AICC_Score+=","+AICC_fltScoreMax+","+AICC_fltScoreMin)),WriteToDebug("AICC_Score="+AICC_Score),AICC_Score}function AICC_TranslateTimeToAICC(){return WriteToDebug("In AICC_TranslateTimeToAICC"),ConvertMilliSecondsToSCORMTime(AICC_intSessionTimeMilliseconds,!1)}function AICC_TranslateCommentsToAICC(){WriteToDebug("In AICC_TranslateCommentsToAICC");for(var e="",t=0;t"+AICC_aryCommentsFromLearner[t]+"";return e}function AICC_TranslateObjectivesToAICC(){WriteToDebug("In AICC_TranslateObjectivesToAICC");for(var e="",t=0;t>>2]|=(r[i>>>2]>>>24-i%4*8&255)<<24-(n+i)%4*8;else if(65535>>2]=r[i>>>2];else t.push.apply(t,r);return this.sigBytes+=e,this},clamp:function(){var t=this.words,r=this.sigBytes;t[r>>>2]&=4294967295<<32-r%4*8,t.length=e.ceil(r/4)},clone:function(){var e=i.clone.call(this);return e.words=this.words.slice(0),e},random:function(t){for(var r=[],n=0;n>>2]>>>24-n%4*8&255;r.push((i>>>4).toString(16)),r.push((15&i).toString(16))}return r.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n>>3]|=parseInt(e.substr(n,2),16)<<24-n%8*4;return new o.init(r,t/2)}},u=a.Latin1={stringify:function(e){var t=e.words;e=e.sigBytes;for(var r=[],n=0;n>>2]>>>24-n%4*8&255));return r.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n>>2]|=(255&e.charCodeAt(n))<<24-n%4*8;return new o.init(r,t)}},c=a.Utf8={stringify:function(e){try{return decodeURIComponent(escape(u.stringify(e)))}catch(e){throw Error("Malformed UTF-8 data")}},parse:function(e){return u.parse(unescape(encodeURIComponent(e)))}},l=r.BufferedBlockAlgorithm=i.extend({reset:function(){this._data=new o.init,this._nDataBytes=0},_append:function(e){"string"==typeof e&&(e=c.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function(t){var r=this._data,n=r.words,i=r.sigBytes,a=this.blockSize,s=i/(4*a);if(t=(s=t?e.ceil(s):e.max((0|s)-this._minBufferSize,0))*a,i=e.min(4*t,i),t){for(var u=0;uc;c++){if(16>c)n[c]=0|e[t+c];else{var l=n[c-3]^n[c-8]^n[c-14]^n[c-16];n[c]=l<<1|l>>>31}l=(i<<5|i>>>27)+u+n[c],l=20>c?l+(1518500249+(o&a|~o&s)):40>c?l+(1859775393+(o^a^s)):60>c?l+((o&a|o&s|a&s)-1894007588):l+((o^a^s)-899497514),u=s,s=a,a=o<<30|o>>>2,o=i,i=l}r[0]=r[0]+i|0,r[1]=r[1]+o|0,r[2]=r[2]+a|0,r[3]=r[3]+s|0,r[4]=r[4]+u|0},_doFinalize:function(){var e=this._data,t=e.words,r=8*this._nDataBytes,n=8*e.sigBytes;return t[n>>>5]|=128<<24-n%32,t[14+(n+64>>>9<<4)]=Math.floor(r/4294967296),t[15+(n+64>>>9<<4)]=r,e.sigBytes=4*t.length,this._process(),this._hash},clone:function(){var e=r.clone.call(this);return e._hash=this._hash.clone(),e}});e.SHA1=r._createHelper(i),e.HmacSHA1=r._createHmacHelper(i)}();var TinCan,Cmi5,TC_COURSE_ID,TC_COURSE_NAME,TC_COURSE_DESC,TC_RECORD_STORES;CryptoJS=CryptoJS||function(e){var t={},r=t.lib={},n=function(){},i=r.Base={extend:function(e){n.prototype=this;var t=new n;return e&&t.mixIn(e),t.hasOwnProperty("init")||(t.init=function(){t.$super.init.apply(this,arguments)}),t.init.prototype=t,t.$super=this,t},create:function(){var e=this.extend();return e.init.apply(e,arguments),e},init:function(){},mixIn:function(e){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);e.hasOwnProperty("toString")&&(this.toString=e.toString)},clone:function(){return this.init.prototype.extend(this)}},o=r.WordArray=i.extend({init:function(e,t){e=this.words=e||[],this.sigBytes=null!=t?t:4*e.length},toString:function(e){return(e||s).stringify(this)},concat:function(e){var t=this.words,r=e.words,n=this.sigBytes;if(e=e.sigBytes,this.clamp(),n%4)for(var i=0;i>>2]|=(r[i>>>2]>>>24-i%4*8&255)<<24-(n+i)%4*8;else if(65535>>2]=r[i>>>2];else t.push.apply(t,r);return this.sigBytes+=e,this},clamp:function(){var t=this.words,r=this.sigBytes;t[r>>>2]&=4294967295<<32-r%4*8,t.length=e.ceil(r/4)},clone:function(){var e=i.clone.call(this);return e.words=this.words.slice(0),e},random:function(t){for(var r=[],n=0;n>>2]>>>24-n%4*8&255;r.push((i>>>4).toString(16)),r.push((15&i).toString(16))}return r.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n>>3]|=parseInt(e.substr(n,2),16)<<24-n%8*4;return new o.init(r,t/2)}},u=a.Latin1={stringify:function(e){var t=e.words;e=e.sigBytes;for(var r=[],n=0;n>>2]>>>24-n%4*8&255));return r.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n>>2]|=(255&e.charCodeAt(n))<<24-n%4*8;return new o.init(r,t)}},c=a.Utf8={stringify:function(e){try{return decodeURIComponent(escape(u.stringify(e)))}catch(e){throw Error("Malformed UTF-8 data")}},parse:function(e){return u.parse(unescape(encodeURIComponent(e)))}},l=r.BufferedBlockAlgorithm=i.extend({reset:function(){this._data=new o.init,this._nDataBytes=0},_append:function(e){"string"==typeof e&&(e=c.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function(t){var r=this._data,n=r.words,i=r.sigBytes,a=this.blockSize,s=i/(4*a);if(t=(s=t?e.ceil(s):e.max((0|s)-this._minBufferSize,0))*a,i=e.min(4*t,i),t){for(var u=0;uc;){var l;e:{l=u;for(var C=e.sqrt(l),I=2;I<=C;I++)if(!(l%I)){l=!1;break e}l=!0}l&&(8>c&&(o[c]=s(e.pow(u,.5))),a[c]=s(e.pow(u,1/3)),c++),u++}var _=[];i=i.SHA256=n.extend({_doReset:function(){this._hash=new r.init(o.slice(0))},_doProcessBlock:function(e,t){for(var r=this._hash.words,n=r[0],i=r[1],o=r[2],s=r[3],u=r[4],c=r[5],l=r[6],C=r[7],I=0;64>I;I++){if(16>I)_[I]=0|e[t+I];else{var S=_[I-15],T=_[I-2];_[I]=((S<<25|S>>>7)^(S<<14|S>>>18)^S>>>3)+_[I-7]+((T<<15|T>>>17)^(T<<13|T>>>19)^T>>>10)+_[I-16]}S=C+((u<<26|u>>>6)^(u<<21|u>>>11)^(u<<7|u>>>25))+(u&c^~u&l)+a[I]+_[I],T=((n<<30|n>>>2)^(n<<19|n>>>13)^(n<<10|n>>>22))+(n&i^n&o^i&o),C=l,l=c,c=u,u=s+S|0,s=o,o=i,i=n,n=S+T|0}r[0]=r[0]+n|0,r[1]=r[1]+i|0,r[2]=r[2]+o|0,r[3]=r[3]+s|0,r[4]=r[4]+u|0,r[5]=r[5]+c|0,r[6]=r[6]+l|0,r[7]=r[7]+C|0},_doFinalize:function(){var t=this._data,r=t.words,n=8*this._nDataBytes,i=8*t.sigBytes;return r[i>>>5]|=128<<24-i%32,r[14+(i+64>>>9<<4)]=e.floor(n/4294967296),r[15+(i+64>>>9<<4)]=n,t.sigBytes=4*r.length,this._process(),this._hash},clone:function(){var e=n.clone.call(this);return e._hash=this._hash.clone(),e}});t.SHA256=n._createHelper(i),t.HmacSHA256=n._createHmacHelper(i)}(Math),function(){var e=CryptoJS,t=e.lib.WordArray;e.enc.Base64={stringify:function(e){var t=e.words,r=e.sigBytes,n=this._map;e.clamp();for(var i=[],o=0;o>>2]>>>24-o%4*8&255)<<16|(t[o+1>>>2]>>>24-(o+1)%4*8&255)<<8|t[o+2>>>2]>>>24-(o+2)%4*8&255,s=0;s<4&&o+.75*s>>6*(3-s)&63));var u=n.charAt(64);if(u)for(;i.length%4;)i.push(u);return i.join("")},parse:function(e){var r=e.length,n=this._map,i=n.charAt(64);if(i){var o=e.indexOf(i);-1!=o&&(r=o)}for(var a=[],s=0,u=0;u>>6-u%4*2;a[s>>>2]|=(c|l)<<24-s%4*8,s++}return t.create(a,s)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(),function(){if("function"==typeof ArrayBuffer){var e=CryptoJS.lib.WordArray,t=e.init,r=e.init=function(e){if(e instanceof ArrayBuffer&&(e=new Uint8Array(e)),(e instanceof Int8Array||e instanceof Uint8ClampedArray||e instanceof Int16Array||e instanceof Uint16Array||e instanceof Int32Array||e instanceof Uint32Array||e instanceof Float32Array||e instanceof Float64Array)&&(e=new Uint8Array(e.buffer,e.byteOffset,e.byteLength)),e instanceof Uint8Array){for(var r=e.byteLength,n=[],i=0;i>>2]|=e[i]<<24-i%4*8;t.call(this,n,r)}else t.apply(this,arguments)};r.prototype=e}}(),function(){"use strict";var e={statementId:!0,voidedStatementId:!0,verb:!0,object:!0,registration:!0,context:!0,actor:!0,since:!0,until:!0,limit:!0,authoritative:!0,sparse:!0,instructor:!0,ascending:!0,continueToken:!0,agent:!0,activityId:!0,stateId:!0,profileId:!0,activity_platform:!0,grouping:!0,"Accept-Language":!0};(TinCan=function(e){this.log("constructor"),this.recordStores=[],this.actor=null,this.activity=null,this.registration=null,this.context=null,this.init(e)}).prototype={LOG_SRC:"TinCan",log:function(e,t){TinCan.DEBUG&&"undefined"!=typeof console&&console.log&&(t=t||this.LOG_SRC||"TinCan",console.log("TinCan."+t+": "+e))},init:function(e){var t;if(this.log("init"),(e=e||{}).hasOwnProperty("url")&&""!==e.url&&this._initFromQueryString(e.url),e.hasOwnProperty("recordStores")&&void 0!==e.recordStores)for(t=0;t0)for("function"==typeof t&&(i=function(e,r){var n;o.log("sendStatement - callbackWrapper: "+s),s>1?(s-=1,c.push({err:e,xhr:r})):1===s?(c.push({err:e,xhr:r}),n=[c,a],t.apply(this,n)):o.log("sendStatement - unexpected record store count: "+s)}),n=0;n0)return this.recordStores[0].retrieveStatement(e,{callback:t,params:r.params});this.log("[warning] getStatement: No LRSs added yet (statement not retrieved)")},voidStatement:function(e,t,r){this.log("voidStatement");var n,i,o,a,s,u=this,c=this.recordStores.length,l=[],C=[];if(e instanceof TinCan.Statement&&(e=e.id),void 0!==r.actor?i=r.actor:null!==this.actor&&(i=this.actor),o=new TinCan.Statement({actor:i,verb:{id:"http://adlnet.gov/expapi/verbs/voided"},target:{objectType:"StatementRef",id:e}}),c>0)for("function"==typeof t&&(s=function(e,r){var n;u.log("voidStatement - callbackWrapper: "+c),c>1?(c-=1,C.push({err:e,xhr:r})):1===c?(C.push({err:e,xhr:r}),n=[C,o],t.apply(this,n)):u.log("voidStatement - unexpected record store count: "+c)}),a=0;a0)return this.recordStores[0].retrieveVoidedStatement(e,{callback:t});this.log("[warning] getVoidedStatement: No LRSs added yet (statement not retrieved)")},sendStatements:function(e,t){this.log("sendStatements");var r,n,i,o=this,a=[],s=this.recordStores.length,u=[],c=[];if(0===e.length)"function"==typeof t&&t.apply(this,[null,a]);else{for(n=0;n0)for("function"==typeof t&&(i=function(e,r){var n;o.log("sendStatements - callbackWrapper: "+s),s>1?(s-=1,c.push({err:e,xhr:r})):1===s?(c.push({err:e,xhr:r}),n=[c,a],t.apply(this,n)):o.log("sendStatements - unexpected record store count: "+s)}),n=0;n0)return t=this.recordStores[0],r=(e=e||{}).params||{},e.sendActor&&null!==this.actor&&("0.9"===t.version||"0.95"===t.version?r.actor=this.actor:r.agent=this.actor),e.sendActivity&&null!==this.activity&&("0.9"===t.version||"0.95"===t.version?r.target=this.activity:r.activity=this.activity),void 0===r.registration&&null!==this.registration&&(r.registration=this.registration),n={params:r},void 0!==e.callback&&(n.callback=e.callback),t.queryStatements(n);this.log("[warning] getStatements: No LRSs added yet (statements not read)")},getState:function(e,t){var r,n;if(this.log("getState"),this.recordStores.length>0)return n=this.recordStores[0],r={agent:void 0!==(t=t||{}).agent?t.agent:this.actor,activity:void 0!==t.activity?t.activity:this.activity},void 0!==t.registration?r.registration=t.registration:null!==this.registration&&(r.registration=this.registration),void 0!==t.callback&&(r.callback=t.callback),n.retrieveState(e,r);this.log("[warning] getState: No LRSs added yet (state not retrieved)")},setState:function(e,t,r){var n,i;if(this.log("setState"),this.recordStores.length>0)return i=this.recordStores[0],n={agent:void 0!==(r=r||{}).agent?r.agent:this.actor,activity:void 0!==r.activity?r.activity:this.activity},void 0!==r.registration?n.registration=r.registration:null!==this.registration&&(n.registration=this.registration),void 0!==r.lastSHA1&&(n.lastSHA1=r.lastSHA1),void 0!==r.contentType&&(n.contentType=r.contentType,void 0!==r.overwriteJSON&&!r.overwriteJSON&&TinCan.Utils.isApplicationJSON(r.contentType)&&(n.method="POST")),void 0!==r.callback&&(n.callback=r.callback),i.saveState(e,t,n);this.log("[warning] setState: No LRSs added yet (state not saved)")},deleteState:function(e,t){var r,n;if(this.log("deleteState"),this.recordStores.length>0)return n=this.recordStores[0],r={agent:void 0!==(t=t||{}).agent?t.agent:this.actor,activity:void 0!==t.activity?t.activity:this.activity},void 0!==t.registration?r.registration=t.registration:null!==this.registration&&(r.registration=this.registration),void 0!==t.callback&&(r.callback=t.callback),n.dropState(e,r);this.log("[warning] deleteState: No LRSs added yet (state not deleted)")},getActivityProfile:function(e,t){var r,n;if(this.log("getActivityProfile"),this.recordStores.length>0)return n=this.recordStores[0],r={activity:void 0!==(t=t||{}).activity?t.activity:this.activity},void 0!==t.callback&&(r.callback=t.callback),n.retrieveActivityProfile(e,r);this.log("[warning] getActivityProfile: No LRSs added yet (activity profile not retrieved)")},setActivityProfile:function(e,t,r){var n,i;if(this.log("setActivityProfile"),this.recordStores.length>0)return i=this.recordStores[0],n={activity:void 0!==(r=r||{}).activity?r.activity:this.activity},void 0!==r.callback&&(n.callback=r.callback),void 0!==r.lastSHA1&&(n.lastSHA1=r.lastSHA1),void 0!==r.contentType&&(n.contentType=r.contentType,void 0!==r.overwriteJSON&&!r.overwriteJSON&&TinCan.Utils.isApplicationJSON(r.contentType)&&(n.method="POST")),i.saveActivityProfile(e,t,n);this.log("[warning] setActivityProfile: No LRSs added yet (activity profile not saved)")},deleteActivityProfile:function(e,t){var r,n;if(this.log("deleteActivityProfile"),this.recordStores.length>0)return n=this.recordStores[0],r={activity:void 0!==(t=t||{}).activity?t.activity:this.activity},void 0!==t.callback&&(r.callback=t.callback),n.dropActivityProfile(e,r);this.log("[warning] deleteActivityProfile: No LRSs added yet (activity profile not deleted)")},getAgentProfile:function(e,t){var r,n;if(this.log("getAgentProfile"),this.recordStores.length>0)return n=this.recordStores[0],r={agent:void 0!==(t=t||{}).agent?t.agent:this.actor},void 0!==t.callback&&(r.callback=t.callback),n.retrieveAgentProfile(e,r);this.log("[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)")},setAgentProfile:function(e,t,r){var n,i;if(this.log("setAgentProfile"),this.recordStores.length>0)return i=this.recordStores[0],n={agent:void 0!==(r=r||{}).agent?r.agent:this.actor},void 0!==r.callback&&(n.callback=r.callback),void 0!==r.lastSHA1&&(n.lastSHA1=r.lastSHA1),void 0!==r.contentType&&(n.contentType=r.contentType,void 0!==r.overwriteJSON&&!r.overwriteJSON&&TinCan.Utils.isApplicationJSON(r.contentType)&&(n.method="POST")),i.saveAgentProfile(e,t,n);this.log("[warning] setAgentProfile: No LRSs added yet (agent profile not saved)")},deleteAgentProfile:function(e,t){var r,n;if(this.log("deleteAgentProfile"),this.recordStores.length>0)return n=this.recordStores[0],r={agent:void 0!==(t=t||{}).agent?t.agent:this.actor},void 0!==t.callback&&(r.callback=t.callback),n.dropAgentProfile(e,r);this.log("[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)")}},TinCan.DEBUG=!1,TinCan.enableDebug=function(){TinCan.DEBUG=!0},TinCan.disableDebug=function(){TinCan.DEBUG=!1},TinCan.versions=function(){return["1.0.2","1.0.1","1.0.0","0.95","0.9"]},"object"==typeof module&&(module.exports=TinCan)}(),function(){"use strict";TinCan.Utils={defaultEncoding:"utf8",getUUID:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return("x"==e?t:3&t|8).toString(16)})},getISODateString:function(e){function t(e,t){var r,n;for(null==e&&(e=0),null==t&&(t=2),r=Math.pow(10,t-1),n=e.toString();e1;)n="0"+n,r/=10;return n}return e.getUTCFullYear()+"-"+t(e.getUTCMonth()+1)+"-"+t(e.getUTCDate())+"T"+t(e.getUTCHours())+":"+t(e.getUTCMinutes())+":"+t(e.getUTCSeconds())+"."+t(e.getUTCMilliseconds(),3)+"Z"},convertISO8601DurationToMilliseconds:function(e){var t,r,n,i,o=e.indexOf("-")>=0,a=e.indexOf("T"),s=e.indexOf("H"),u=e.indexOf("M"),c=e.indexOf("S");if(-1===a||-1!==u&&u0&&(a+=t+"H"),(r=parseInt(n%36e4/6e3,10))>0&&(a+=r+"M"),a+=n%36e4%6e3/100+"S"},getSHA1String:function(e){return CryptoJS.SHA1(e).toString(CryptoJS.enc.Hex)},getSHA256String:function(e){return"[object ArrayBuffer]"===Object.prototype.toString.call(e)&&(e=CryptoJS.lib.WordArray.create(e)),CryptoJS.SHA256(e).toString(CryptoJS.enc.Hex)},getBase64String:function(e){return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(e))},getLangDictionaryValue:function(e,t){var r,n=this[e];if(void 0!==t&&void 0!==n[t])return n[t];if(void 0!==n.und)return n.und;if(void 0!==n["en-US"])return n["en-US"];for(r in n)if(n.hasOwnProperty(r))return n[r];return""},parseURL:function(e,t){var r,n,i,o,a="/"===e.charAt(0),s=["(/[^?#]*)","(\\?[^#]*|)","(#.*|)$"],u=/\+/g,c=/([^&=]+)=?([^&]*)/g,l=function(e){return decodeURIComponent(e.replace(u," "))};if(t=t||{},a){if(void 0===t.allowRelative||!t.allowRelative)throw new Error("Refusing to parse relative URL without 'allowRelative' option")}else s.unshift("^(https?:)//","(([^:/?#]*)(?::([0-9]+))?)"),-1===e.indexOf("/",8)&&(e+="/");if(r=new RegExp(s.join("")),null===(n=e.match(r)))throw new Error("Unable to parse URL regular expression did not match: '"+e+"'");if(a?(i={protocol:null,host:null,hostname:null,port:null,path:null,pathname:n[1],search:n[2],hash:n[3],params:{}}).path=i.pathname:(i={protocol:n[1],host:n[2],hostname:n[3],port:n[4],pathname:n[5],search:n[6],hash:n[7],params:{}}).path=i.protocol+"//"+i.host+i.pathname,""!==i.search)for(;o=c.exec(i.search.substring(1));)i.params[l(o[1])]=l(o[2]);return i},getServerRoot:function(e){var t=e.split("/");return t[0]+"//"+t[2]},getContentTypeFromHeader:function(e){return String(e).split(";")[0]},isApplicationJSON:function(e){return 0===TinCan.Utils.getContentTypeFromHeader(e).toLowerCase().indexOf("application/json")},stringToArrayBuffer:function(){TinCan.prototype.log("stringToArrayBuffer not overloaded - no environment loaded?")},stringFromArrayBuffer:function(){TinCan.prototype.log("stringFromArrayBuffer not overloaded - no environment loaded?")}}}(),function(){"use strict";var e=TinCan.LRS=function(e){this.log("constructor"),this.endpoint=null,this.version=null,this.auth=null,this.allowFail=!0,this.extended=null,this.init(e)};e.prototype={LOG_SRC:"LRS",log:TinCan.prototype.log,init:function(e){this.log("init");var t,r=TinCan.versions(),n=!1;if((e=e||{}).hasOwnProperty("alertOnRequestFailure")&&this.log("'alertOnRequestFailure' is deprecated (alerts have been removed) no need to set it now"),!e.hasOwnProperty("endpoint")||null===e.endpoint||""===e.endpoint)throw this.log("[error] LRS invalid: no endpoint"),{code:3,mesg:"LRS invalid: no endpoint"};if(this.endpoint=String(e.endpoint),"/"!==this.endpoint.slice(-1)&&(this.log("adding trailing slash to endpoint"),this.endpoint+="/"),e.hasOwnProperty("allowFail")&&(this.allowFail=e.allowFail),e.hasOwnProperty("auth")?this.auth=e.auth:e.hasOwnProperty("username")&&e.hasOwnProperty("password")&&(this.auth="Basic "+TinCan.Utils.getBase64String(e.username+":"+e.password)),e.hasOwnProperty("extended")&&(this.extended=e.extended),this._initByEnvironment(e),void 0!==e.version){for(this.log("version: "+e.version),t=0;t0&&(e.name=e.firstName[0],e.firstName.length>1&&(this.degraded=!0)),""!==e.name&&(e.name+=" "),void 0!==e.lastName&&e.lastName.length>0&&(e.name+=e.lastName[0],e.lastName.length>1&&(this.degraded=!0))):void 0===e.familyName&&void 0===e.givenName||(e.name="",void 0!==e.givenName&&e.givenName.length>0&&(e.name=e.givenName[0],e.givenName.length>1&&(this.degraded=!0)),""!==e.name&&(e.name+=" "),void 0!==e.familyName&&e.familyName.length>0&&(e.name+=e.familyName[0],e.familyName.length>1&&(this.degraded=!0))),"object"==typeof e.name&&null!==e.name&&(e.name.length>1&&(this.degraded=!0),e.name=e.name[0]),"object"==typeof e.mbox&&null!==e.mbox&&(e.mbox.length>1&&(this.degraded=!0),e.mbox=e.mbox[0]),"object"==typeof e.mbox_sha1sum&&null!==e.mbox_sha1sum&&(e.mbox_sha1sum.length>1&&(this.degraded=!0),e.mbox_sha1sum=e.mbox_sha1sum[0]),"object"==typeof e.openid&&null!==e.openid&&(e.openid.length>1&&(this.degraded=!0),e.openid=e.openid[0]),"object"==typeof e.account&&null!==e.account&&void 0===e.account.homePage&&void 0===e.account.name&&(0===e.account.length?delete e.account:(e.account.length>1&&(this.degraded=!0),e.account=e.account[0])),e.hasOwnProperty("account")&&(e.account instanceof TinCan.AgentAccount?this.account=e.account:this.account=new TinCan.AgentAccount(e.account)),t=0;t0)for(t.member=[],r=0;r0))for(r=0;r0)if("0.9"===e||"0.95"===e)this[i[t]].length>1&&this.log("[warning] version does not support multiple values in: "+i[t]),n[i[t]]=this[i[t]][0].asVersion(e);else for(n[i[t]]=[],r=0;r0){if("0.9"===e||"0.95"===e)throw this.log("[error] version does not support the 'category' property: "+e),new Error(e+" does not support the 'category' property");for(n.category=[],t=0;t>>0;if("function"!=typeof e)throw new TypeError;var n,i=arguments[1];for(n=0;n=200&&o<400||i?t.callback?void t.callback(null,e):n={err:null,xhr:e}:(n={err:o,xhr:e},c(0===o?"[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint ("+o+")":"[warning] There was a problem communicating with the Learning Record Store. ("+o+" | "+e.responseText+")",s),t.callback&&t.callback(o,e),n))},t=function(e,t,r,n){var i;for(i in t)t.hasOwnProperty(i)&&r.push(i+"="+encodeURIComponent(t[i]));return void 0!==n.data&&r.push("content="+encodeURIComponent(n.data)),t["Content-Type"]="application/x-www-form-urlencoded",e+="?method="+n.method,n.method="POST",n.params={},r.length>0&&(n.data=r.join("&")),e},r=function(r,n,i){c("sendRequest using XMLHttpRequest",s);var o,a,u,l,C=this,I=[],_={finished:!1,fakeStatus:null},S=void 0!==i.callback,T=r;for(a in c("sendRequest using XMLHttpRequest - async: "+S,s),i.params)i.params.hasOwnProperty(a)&&I.push(a+"="+encodeURIComponent(i.params[a]));if(I.length>0&&(T+="?"+I.join("&")),T.length>=2048){if(void 0===i.method)return l=new Error("method must not be undefined for an IE Mode Request conversion"),void 0!==i.callback&&i.callback(l,null),{err:l,xhr:null};r=t(r,n,I,i)}else r=T;if("undefined"!=typeof XMLHttpRequest)o=new XMLHttpRequest;else if(o=new ActiveXObject("Microsoft.XMLHTTP"),i.expectMultipart)return l=new Error("Attachment support not available"),void 0!==i.callback&&i.callback(l,null),{err:l,xhr:null};for(a in o.open(i.method,r,S),i.expectMultipart&&(o.responseType="arraybuffer"),n)n.hasOwnProperty(a)&&o.setRequestHeader(a,n[a]);u=i.data,S&&(o.onreadystatechange=function(){c("xhr.onreadystatechange - xhr.readyState: "+o.readyState,s),4===o.readyState&&e.call(C,o,i,_)});try{o.send(u)}catch(e){c("sendRequest caught send exception: "+e,s)}return S?o:e.call(this,o,i,_)},n=function(t,r,n){c("sendRequest using XDomainRequest",s);var i,o,u,l,C,I=this,_=[],S={finished:!1,fakeStatus:null};if(n.expectMultipart)return C=new Error("Attachment support not available"),void 0!==n.callback&&n.callback(C,null),{err:C,xhr:null};for(u in t+="?method="+n.method,n.params)n.params.hasOwnProperty(u)&&_.push(u+"="+encodeURIComponent(n.params[u]));for(u in r)r.hasOwnProperty(u)&&_.push(u+"="+encodeURIComponent(r[u]));void 0!==n.data&&_.push("content="+encodeURIComponent(n.data)),o=_.join("&"),(i=new XDomainRequest).open("POST",t),n.callback?(i.onload=function(){S.fakeStatus=200,e.call(I,i,n,S)},i.onerror=function(){S.fakeStatus=400,e.call(I,i,n,S)},i.ontimeout=function(){S.fakeStatus=0,e.call(I,i,n,S)}):(i.onload=function(){S.fakeStatus=200},i.onerror=function(){S.fakeStatus=400},i.ontimeout=function(){S.fakeStatus=0}),i.onprogress=function(){},i.timeout=0;try{i.send(o)}catch(e){c("sendRequest caught send exception: "+e,s)}if(!n.callback){for(l=1e4+Date.now(),c("sendRequest - until: "+l+", finished: "+S.finished,s);Date.now()>>0}var u,c,l=Math.LN2,C=Math.abs,I=Math.floor,_=Math.log,S=Math.max,T=Math.min,d=Math.pow,g=Math.round;function R(e,t){var r=32-t;return e<>r}function f(e,t){var r=32-t;return e<>>r}function E(e){return[255&e]}function p(e){return R(e[0],8)}function O(e){return[255&e]}function h(e){return f(e[0],8)}function A(e){return[(e=g(Number(e)))<0?0:e>255?255:255&e]}function m(e){return[255&e,e>>8&255]}function M(e){return R(e[1]<<8|e[0],16)}function b(e){return[255&e,e>>8&255]}function D(e){return f(e[1]<<8|e[0],16)}function v(e){return[255&e,e>>8&255,e>>16&255,e>>24&255]}function N(e){return R(e[3]<<24|e[2]<<16|e[1]<<8|e[0],32)}function P(e){return[255&e,e>>8&255,e>>16&255,e>>24&255]}function y(e){return f(e[3]<<24|e[2]<<16|e[1]<<8|e[0],32)}function L(e,t,r){var n,i,o,a=(1<.5||t%2?t+1:t}if(e!=e)i=(1<=d(2,1-a)){i=T(I(_(e)/l),1023);var u=e/d(2,i);u<1&&(i-=1,u*=2),u>=2&&(i+=1,u/=2);var c=d(2,r);i+=a,(o=s(u*c)-c)/c>=1&&(i+=1,o=0),i>2*a&&(i=(1<>=1;return C.reverse(),a=C.join(""),s=(1<0?u*d(2,c-s)*(1+l/d(2,r)):0!==l?u*d(2,-(s-1))*(l/d(2,r)):u<0?-0:0}function w(e){return W(e,11,52)}function G(e){return L(e,11,52)}function x(e){return W(e,8,23)}function U(e){return L(e,8,23)}u=Object.defineProperty,c=!function(){try{return Object.defineProperty({},"x",{})}catch(e){return!1}}(),u&&!c||(Object.defineProperty=function(e,t,r){if(u)try{return u(e,t,r)}catch(e){}if(e!==Object(e))throw TypeError("Object.defineProperty called on non-object");return Object.prototype.__defineGetter__&&"get"in r&&Object.prototype.__defineGetter__.call(e,t,r.get),Object.prototype.__defineSetter__&&"set"in r&&Object.prototype.__defineSetter__.call(e,t,r.set),"value"in r&&(e[t]=r.value),e}),function(){function u(e){if((e=a(e))<0)throw RangeError("ArrayBuffer size is not a small enough positive integer.");Object.defineProperty(this,"byteLength",{value:e}),Object.defineProperty(this,"_bytes",{value:Array(e)});for(var t=0;t=1&&"object"===r(arguments[0])&&arguments[0]instanceof c)return function(e){if(this.constructor!==e.constructor)throw TypeError();var t=e.length*this.BYTES_PER_ELEMENT;Object.defineProperty(this,"buffer",{value:new u(t)}),Object.defineProperty(this,"byteLength",{value:t}),Object.defineProperty(this,"byteOffset",{value:0}),Object.defineProperty(this,"length",{value:e.length});for(var r=0;r=1&&"object"===r(arguments[0])&&!(arguments[0]instanceof c)&&!(arguments[0]instanceof u||"ArrayBuffer"===n(arguments[0])))return function(e){var t=e.length*this.BYTES_PER_ELEMENT;Object.defineProperty(this,"buffer",{value:new u(t)}),Object.defineProperty(this,"byteLength",{value:t}),Object.defineProperty(this,"byteOffset",{value:0}),Object.defineProperty(this,"length",{value:e.length});for(var r=0;r=1&&"object"===r(arguments[0])&&(arguments[0]instanceof u||"ArrayBuffer"===n(arguments[0])))return function(e,r,n){if((r=s(r))>e.byteLength)throw RangeError("byteOffset out of range");if(r%this.BYTES_PER_ELEMENT)throw RangeError("buffer length minus the byteOffset is not a multiple of the element size.");if(n===t){var i=e.byteLength-r;if(i%this.BYTES_PER_ELEMENT)throw RangeError("length of buffer minus byteOffset not a multiple of the element size");n=i/this.BYTES_PER_ELEMENT}else i=(n=s(n))*this.BYTES_PER_ELEMENT;if(r+i>e.byteLength)throw RangeError("byteOffset and length reference an area beyond the end of the buffer");Object.defineProperty(this,"buffer",{value:e}),Object.defineProperty(this,"byteLength",{value:i}),Object.defineProperty(this,"byteOffset",{value:r}),Object.defineProperty(this,"length",{value:n})}.apply(this,arguments);throw TypeError()}e.ArrayBuffer=e.ArrayBuffer||u,Object.defineProperty(c,"from",{value:function(e){return new this(e)}}),Object.defineProperty(c,"of",{value:function(){return new this(arguments)}});var l={};function _(t,r,n){var i=function(){Object.defineProperty(this,"constructor",{value:i}),c.apply(this,arguments),function(t){if(!("TYPED_ARRAY_POLYFILL_NO_ARRAY_ACCESSORS"in e)){if(t.length>1e5)throw RangeError("Array too large for polyfill");var r;for(r=0;r=this.length)return t;var r,n,i=[];for(r=0,n=this.byteOffset+e*this.BYTES_PER_ELEMENT;r=this.length)){var r,n,i=this._pack(t);for(r=0,n=this.byteOffset+e*this.BYTES_PER_ELEMENT;r0;)i._setter(c,i._getter(C)),C+=g,c+=g,R-=1;return i}}),Object.defineProperty(c.prototype,"every",{value:function(e){if(this===t||null===this)throw TypeError();var r=Object(this),n=s(r.length);if(!i(e))throw TypeError();for(var o=arguments[1],a=0;a1?arguments[1]:t,u=0;u1?arguments[1]:t,u=0;u0&&((i=Number(arguments[1]))!=i?i=0:0!==i&&i!==1/0&&i!==-1/0&&(i=(i>0||-1)*I(C(i)))),i>=n)return-1;for(var o=i>=0?i:S(n-C(i),0);o1&&((i=Number(arguments[1]))!=i?i=0:0!==i&&i!==1/0&&i!==-1/0&&(i=(i>0||-1)*I(C(i))));for(var o=i>=0?T(i,n-1):n-C(i);o>=0;o--)if(r._getter(o)===e)return o;return-1}}),Object.defineProperty(c.prototype,"map",{value:function(e){if(this===t||null===this)throw TypeError();var r=Object(this),n=s(r.length);if(!i(e))throw TypeError();var o=[];o.length=n;for(var a=arguments[1],u=0;u=2?arguments[1]:r._getter(a++);a=2?arguments[1]:r._getter(a--);a>=0;)o=e.call(t,o,r._getter(a),a,r),a--;return o}}),Object.defineProperty(c.prototype,"reverse",{value:function(){if(this===t||null===this)throw TypeError();for(var e=Object(this),r=s(e.length),n=I(r/2),i=0,o=r-1;ithis.length)throw RangeError("Offset plus length of array is out of range");if(l=this.byteOffset+i*this.BYTES_PER_ELEMENT,C=r.length*this.BYTES_PER_ELEMENT,r.buffer===this.buffer){for(I=[],a=0,u=r.byteOffset;athis.length)throw RangeError("Offset plus length of array is out of range");for(a=0;ar?r:e}e=a(e),t=a(t),arguments.length<1&&(e=0),arguments.length<2&&(t=this.length),e<0&&(e=this.length+e),t<0&&(t=this.length+t),e=r(e,0,this.length);var n=(t=r(t,0,this.length))-e;return n<0&&(n=0),new this.constructor(this.buffer,this.byteOffset+e*this.BYTES_PER_ELEMENT,n)}});var d=_(1,E,p),g=_(1,O,h),R=_(1,A,h),f=_(2,m,M),L=_(2,b,D),W=_(4,v,N),F=_(4,P,y),j=_(4,U,x),k=_(8,G,w);e.Int8Array=e.Int8Array||d,e.Uint8Array=e.Uint8Array||g,e.Uint8ClampedArray=e.Uint8ClampedArray||R,e.Int16Array=e.Int16Array||f,e.Uint16Array=e.Uint16Array||L,e.Int32Array=e.Int32Array||W,e.Uint32Array=e.Uint32Array||F,e.Float32Array=e.Float32Array||j,e.Float64Array=e.Float64Array||k}(),function(){function r(e,t){return i(e.get)?e.get(t):e[t]}var o,a=(o=new Uint16Array([4660]),18===r(new Uint8Array(o.buffer),0));function u(e,r,i){if(!(e instanceof ArrayBuffer||"ArrayBuffer"===n(e)))throw TypeError();if((r=s(r))>e.byteLength)throw RangeError("byteOffset out of range");if(r+(i=i===t?e.byteLength-r:s(i))>e.byteLength)throw RangeError("byteOffset and length reference an area beyond the end of the buffer");Object.defineProperty(this,"buffer",{value:e}),Object.defineProperty(this,"byteLength",{value:i}),Object.defineProperty(this,"byteOffset",{value:r})}function c(e){return function(t,n){if((t=s(t))+e.BYTES_PER_ELEMENT>this.byteLength)throw RangeError("Array index out of range");t+=this.byteOffset;for(var i=new Uint8Array(this.buffer,t,e.BYTES_PER_ELEMENT),o=[],u=0;uthis.byteLength)throw RangeError("Array index out of range");var o,u=new e([n]),c=new Uint8Array(u.buffer),l=[];for(o=0;oo)return new ArrayBuffer(0);var a=o-i,s=new ArrayBuffer(a),u=new Uint8Array(s),c=new Uint8Array(this,i,a);return u.set(c),s})}(),"undefined"!=typeof module&&module.exports&&(this["encoding-indexes"]=require("./encoding-indexes.js")["encoding-indexes"]),function(e){"use strict";function t(e,t,r){return t<=e&&e<=r}var r=Math.floor;function n(e){if(void 0===e)return{};if(e===Object(e))return e;throw TypeError("Could not convert argument to dictionary")}function i(e){return 0<=e&&e<=127}var o=i,a=-1;function s(e){this.tokens=[].slice.call(e),this.tokens.reverse()}s.prototype={endOfStream:function(){return!this.tokens.length},read:function(){return this.tokens.length?this.tokens.pop():a},prepend:function(e){if(Array.isArray(e))for(var t=e;t.length;)this.tokens.push(t.pop());else this.tokens.push(e)},push:function(e){if(Array.isArray(e))for(var t=e;t.length;)this.tokens.unshift(t.shift());else this.tokens.unshift(e)}};var u=-1;function c(e,t){if(e)throw TypeError("Decoder error");return t||65533}function l(e){throw TypeError("The code point "+e+" could not be encoded.")}function C(e){return e=String(e).trim().toLowerCase(),Object.prototype.hasOwnProperty.call(_,e)?_[e]:null}var I=[{encodings:[{labels:["unicode-1-1-utf-8","utf-8","utf8"],name:"UTF-8"}],heading:"The Encoding"},{encodings:[{labels:["866","cp866","csibm866","ibm866"],name:"IBM866"},{labels:["csisolatin2","iso-8859-2","iso-ir-101","iso8859-2","iso88592","iso_8859-2","iso_8859-2:1987","l2","latin2"],name:"ISO-8859-2"},{labels:["csisolatin3","iso-8859-3","iso-ir-109","iso8859-3","iso88593","iso_8859-3","iso_8859-3:1988","l3","latin3"],name:"ISO-8859-3"},{labels:["csisolatin4","iso-8859-4","iso-ir-110","iso8859-4","iso88594","iso_8859-4","iso_8859-4:1988","l4","latin4"],name:"ISO-8859-4"},{labels:["csisolatincyrillic","cyrillic","iso-8859-5","iso-ir-144","iso8859-5","iso88595","iso_8859-5","iso_8859-5:1988"],name:"ISO-8859-5"},{labels:["arabic","asmo-708","csiso88596e","csiso88596i","csisolatinarabic","ecma-114","iso-8859-6","iso-8859-6-e","iso-8859-6-i","iso-ir-127","iso8859-6","iso88596","iso_8859-6","iso_8859-6:1987"],name:"ISO-8859-6"},{labels:["csisolatingreek","ecma-118","elot_928","greek","greek8","iso-8859-7","iso-ir-126","iso8859-7","iso88597","iso_8859-7","iso_8859-7:1987","sun_eu_greek"],name:"ISO-8859-7"},{labels:["csiso88598e","csisolatinhebrew","hebrew","iso-8859-8","iso-8859-8-e","iso-ir-138","iso8859-8","iso88598","iso_8859-8","iso_8859-8:1988","visual"],name:"ISO-8859-8"},{labels:["csiso88598i","iso-8859-8-i","logical"],name:"ISO-8859-8-I"},{labels:["csisolatin6","iso-8859-10","iso-ir-157","iso8859-10","iso885910","l6","latin6"],name:"ISO-8859-10"},{labels:["iso-8859-13","iso8859-13","iso885913"],name:"ISO-8859-13"},{labels:["iso-8859-14","iso8859-14","iso885914"],name:"ISO-8859-14"},{labels:["csisolatin9","iso-8859-15","iso8859-15","iso885915","iso_8859-15","l9"],name:"ISO-8859-15"},{labels:["iso-8859-16"],name:"ISO-8859-16"},{labels:["cskoi8r","koi","koi8","koi8-r","koi8_r"],name:"KOI8-R"},{labels:["koi8-ru","koi8-u"],name:"KOI8-U"},{labels:["csmacintosh","mac","macintosh","x-mac-roman"],name:"macintosh"},{labels:["dos-874","iso-8859-11","iso8859-11","iso885911","tis-620","windows-874"],name:"windows-874"},{labels:["cp1250","windows-1250","x-cp1250"],name:"windows-1250"},{labels:["cp1251","windows-1251","x-cp1251"],name:"windows-1251"},{labels:["ansi_x3.4-1968","ascii","cp1252","cp819","csisolatin1","ibm819","iso-8859-1","iso-ir-100","iso8859-1","iso88591","iso_8859-1","iso_8859-1:1987","l1","latin1","us-ascii","windows-1252","x-cp1252"],name:"windows-1252"},{labels:["cp1253","windows-1253","x-cp1253"],name:"windows-1253"},{labels:["cp1254","csisolatin5","iso-8859-9","iso-ir-148","iso8859-9","iso88599","iso_8859-9","iso_8859-9:1989","l5","latin5","windows-1254","x-cp1254"],name:"windows-1254"},{labels:["cp1255","windows-1255","x-cp1255"],name:"windows-1255"},{labels:["cp1256","windows-1256","x-cp1256"],name:"windows-1256"},{labels:["cp1257","windows-1257","x-cp1257"],name:"windows-1257"},{labels:["cp1258","windows-1258","x-cp1258"],name:"windows-1258"},{labels:["x-mac-cyrillic","x-mac-ukrainian"],name:"x-mac-cyrillic"}],heading:"Legacy single-byte encodings"},{encodings:[{labels:["chinese","csgb2312","csiso58gb231280","gb2312","gb_2312","gb_2312-80","gbk","iso-ir-58","x-gbk"],name:"GBK"},{labels:["gb18030"],name:"gb18030"}],heading:"Legacy multi-byte Chinese (simplified) encodings"},{encodings:[{labels:["big5","big5-hkscs","cn-big5","csbig5","x-x-big5"],name:"Big5"}],heading:"Legacy multi-byte Chinese (traditional) encodings"},{encodings:[{labels:["cseucpkdfmtjapanese","euc-jp","x-euc-jp"],name:"EUC-JP"},{labels:["csiso2022jp","iso-2022-jp"],name:"ISO-2022-JP"},{labels:["csshiftjis","ms932","ms_kanji","shift-jis","shift_jis","sjis","windows-31j","x-sjis"],name:"Shift_JIS"}],heading:"Legacy multi-byte Japanese encodings"},{encodings:[{labels:["cseuckr","csksc56011987","euc-kr","iso-ir-149","korean","ks_c_5601-1987","ks_c_5601-1989","ksc5601","ksc_5601","windows-949"],name:"EUC-KR"}],heading:"Legacy multi-byte Korean encodings"},{encodings:[{labels:["csiso2022kr","hz-gb-2312","iso-2022-cn","iso-2022-cn-ext","iso-2022-kr"],name:"replacement"},{labels:["utf-16be"],name:"UTF-16BE"},{labels:["utf-16","utf-16le"],name:"UTF-16LE"},{labels:["x-user-defined"],name:"x-user-defined"}],heading:"Legacy miscellaneous encodings"}],_={};I.forEach(function(e){e.encodings.forEach(function(e){e.labels.forEach(function(t){_[t]=e})})});var S={},T={};function d(e,t){return t&&t[e]||null}function g(e,t){var r=t.indexOf(e);return-1===r?null:r}function R(t){if(!("encoding-indexes"in e))throw Error("Indexes missing. Did you forget to include encoding-indexes.js?");return e["encoding-indexes"][t]}var f="utf-8";function E(e,t){if(!(this instanceof E))throw TypeError("Called as a function. Did you forget 'new'?");e=void 0!==e?String(e):f,t=n(t),this._encoding=null,this._decoder=null,this._ignoreBOM=!1,this._BOMseen=!1,this._error_mode="replacement",this._do_not_flush=!1;var r=C(e);if(null===r||"replacement"===r.name)throw RangeError("Unknown encoding: "+e);if(!T[r.name])throw Error("Decoder not present. Did you forget to include encoding-indexes.js?");var i=this;return i._encoding=r,Boolean(t.fatal)&&(i._error_mode="fatal"),Boolean(t.ignoreBOM)&&(i._ignoreBOM=!0),Object.defineProperty||(this.encoding=i._encoding.name.toLowerCase(),this.fatal="fatal"===i._error_mode,this.ignoreBOM=i._ignoreBOM),i}function p(t,r){if(!(this instanceof p))throw TypeError("Called as a function. Did you forget 'new'?");r=n(r),this._encoding=null,this._encoder=null,this._do_not_flush=!1,this._fatal=Boolean(r.fatal)?"fatal":"replacement";var i=this;if(Boolean(r.NONSTANDARD_allowLegacyEncoding)){var o=C(t=void 0!==t?String(t):f);if(null===o||"replacement"===o.name)throw RangeError("Unknown encoding: "+t);if(!S[o.name])throw Error("Encoder not present. Did you forget to include encoding-indexes.js?");i._encoding=o}else i._encoding=C("utf-8"),void 0!==t&&"console"in e&&console.warn("TextEncoder constructor called with encoding label, which is ignored.");return Object.defineProperty||(this.encoding=i._encoding.name.toLowerCase()),i}function O(e){var r=e.fatal,n=0,i=0,o=0,s=128,l=191;this.handler=function(e,C){if(C===a&&0!==o)return o=0,c(r);if(C===a)return u;if(0===o){if(t(C,0,127))return C;if(t(C,194,223))o=1,n=C-192;else if(t(C,224,239))224===C&&(s=160),237===C&&(l=159),o=2,n=C-224;else{if(!t(C,240,244))return c(r);240===C&&(s=144),244===C&&(l=143),o=3,n=C-240}return n<<=6*o,null}if(!t(C,s,l))return n=o=i=0,s=128,l=191,e.prepend(C),c(r);if(s=128,l=191,n+=C-128<<6*(o-(i+=1)),i!==o)return null;var I=n;return n=o=i=0,I}}function h(e){e.fatal;this.handler=function(e,r){if(r===a)return u;if(t(r,0,127))return r;var n,i;t(r,128,2047)?(n=1,i=192):t(r,2048,65535)?(n=2,i=224):t(r,65536,1114111)&&(n=3,i=240);for(var o=[(r>>6*n)+i];n>0;){var s=r>>6*(n-1);o.push(128|63&s),n-=1}return o}}function A(e,t){var r=t.fatal;this.handler=function(t,n){if(n===a)return u;if(i(n))return n;var o=e[n-128];return null===o?c(r):o}}function m(e,t){t.fatal;this.handler=function(t,r){if(r===a)return u;if(o(r))return r;var n=g(r,e);return null===n&&l(r),n+128}}function M(e){var r=e.fatal,n=0,o=0,s=0;this.handler=function(e,l){if(l===a&&0===n&&0===o&&0===s)return u;var C;if(l!==a||0===n&&0===o&&0===s||(n=0,o=0,s=0,c(r)),0!==s){C=null,t(l,48,57)&&(C=function(e){if(e>39419&&e<189e3||e>1237575)return null;if(7457===e)return 59335;var t,r=0,n=0,i=R("gb18030");for(t=0;t>8,n=255&e;return t?[r,n]:[n,r]}function F(e,r){var n=r.fatal,i=null,o=null;this.handler=function(r,s){if(s===a&&(null!==i||null!==o))return c(n);if(s===a&&null===i&&null===o)return u;if(null===i)return i=s,null;var l;if(l=e?(i<<8)+s:(s<<8)+i,i=null,null!==o){var C=o;return o=null,t(l,56320,57343)?65536+1024*(C-55296)+(l-56320):(r.prepend(U(l,e)),c(n))}return t(l,55296,56319)?(o=l,null):t(l,56320,57343)?c(n):l}}function j(e,r){r.fatal;this.handler=function(r,n){if(n===a)return u;if(t(n,0,65535))return U(n,e);var i=U(55296+(n-65536>>10),e),o=U(56320+(n-65536&1023),e);return i.concat(o)}}function k(e){e.fatal;this.handler=function(e,t){return t===a?u:i(t)?t:63360+t-128}}function V(e){e.fatal;this.handler=function(e,r){return r===a?u:o(r)?r:t(r,63360,63487)?r-63360+128:l(r)}}Object.defineProperty&&(Object.defineProperty(E.prototype,"encoding",{get:function(){return this._encoding.name.toLowerCase()}}),Object.defineProperty(E.prototype,"fatal",{get:function(){return"fatal"===this._error_mode}}),Object.defineProperty(E.prototype,"ignoreBOM",{get:function(){return this._ignoreBOM}})),E.prototype.decode=function(e,t){var r;r="object"==typeof e&&e instanceof ArrayBuffer?new Uint8Array(e):"object"==typeof e&&"buffer"in e&&e.buffer instanceof ArrayBuffer?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(0),t=n(t),this._do_not_flush||(this._decoder=T[this._encoding.name]({fatal:"fatal"===this._error_mode}),this._BOMseen=!1),this._do_not_flush=Boolean(t.stream);for(var i,o=new s(r),c=[];;){var l=o.read();if(l===a)break;if((i=this._decoder.handler(o,l))===u)break;null!==i&&(Array.isArray(i)?c.push.apply(c,i):c.push(i))}if(!this._do_not_flush){do{if((i=this._decoder.handler(o,o.read()))===u)break;null!==i&&(Array.isArray(i)?c.push.apply(c,i):c.push(i))}while(!o.endOfStream());this._decoder=null}return function(e){var t,r;return t=["UTF-8","UTF-16LE","UTF-16BE"],r=this._encoding.name,-1===t.indexOf(r)||this._ignoreBOM||this._BOMseen||(e.length>0&&65279===e[0]?(this._BOMseen=!0,e.shift()):e.length>0&&(this._BOMseen=!0)),function(e){for(var t="",r=0;r>10),56320+(1023&n)))}return t}(e)}.call(this,c)},Object.defineProperty&&Object.defineProperty(p.prototype,"encoding",{get:function(){return this._encoding.name.toLowerCase()}}),p.prototype.encode=function(e,t){e=e?String(e):"",t=n(t),this._do_not_flush||(this._encoder=S[this._encoding.name]({fatal:"fatal"===this._fatal})),this._do_not_flush=Boolean(t.stream);for(var r,i=new s(function(e){for(var t=String(e),r=t.length,n=0,i=[];n57343)i.push(o);else if(56320<=o&&o<=57343)i.push(65533);else if(55296<=o&&o<=56319)if(n===r-1)i.push(65533);else{var a=e.charCodeAt(n+1);if(56320<=a&&a<=57343){var s=1023&o,u=1023&a;i.push(65536+(s<<10)+u),n+=1}else i.push(65533)}n+=1}return i}(e)),o=[];;){var c=i.read();if(c===a)break;if((r=this._encoder.handler(i,c))===u)break;Array.isArray(r)?o.push.apply(o,r):o.push(r)}if(!this._do_not_flush){for(;(r=this._encoder.handler(i,i.read()))!==u;)Array.isArray(r)?o.push.apply(o,r):o.push(r);this._encoder=null}return new Uint8Array(o)},S["UTF-8"]=function(e){return new h(e)},T["UTF-8"]=function(e){return new O(e)},"encoding-indexes"in e&&I.forEach(function(e){"Legacy single-byte encodings"===e.heading&&e.encodings.forEach(function(e){var t=e.name,r=R(t.toLowerCase());T[t]=function(e){return new A(r,e)},S[t]=function(e){return new m(r,e)}})}),T.GBK=function(e){return new M(e)},S.GBK=function(e){return new b(e,!0)},S.gb18030=function(e){return new b(e)},T.gb18030=function(e){return new M(e)},S.Big5=function(e){return new v(e)},T.Big5=function(e){return new D(e)},S["EUC-JP"]=function(e){return new P(e)},T["EUC-JP"]=function(e){return new N(e)},S["ISO-2022-JP"]=function(e){return new L(e)},T["ISO-2022-JP"]=function(e){return new y(e)},S.Shift_JIS=function(e){return new w(e)},T.Shift_JIS=function(e){return new W(e)},S["EUC-KR"]=function(e){return new x(e)},T["EUC-KR"]=function(e){return new G(e)},S["UTF-16BE"]=function(e){return new j(!0,e)},T["UTF-16BE"]=function(e){return new F(!0,e)},S["UTF-16LE"]=function(e){return new j(!1,e)},T["UTF-16LE"]=function(e){return new F(!1,e)},S["x-user-defined"]=function(e){return new V(e)},T["x-user-defined"]=function(e){return new k(e)},e.TextEncoder||(e.TextEncoder=p),e.TextDecoder||(e.TextDecoder=E),"undefined"!=typeof module&&module.exports&&(module.exports={TextEncoder:e.TextEncoder,TextDecoder:e.TextDecoder,EncodingIndexes:e["encoding-indexes"]})}(this),function(){var e;"function"==typeof fetch&&"keepalive"in new Request("")&&(e=TinCan.LRS.prototype._initByEnvironment,TinCan.LRS.prototype._initByEnvironment=function(t){var r,n=this;e.call(this,t),r=this._makeRequest,this._makeRequest=function(e,t,i){var o,a=r.apply(n,arguments),s=[],u=e;if(void 0!==i.callback)return a;if(!blnUnloading)return a;if(void 0===a.err||null===a.err||0!==a.err)return a;for(prop in i.params)i.params.hasOwnProperty(prop)&&s.push(prop+"="+encodeURIComponent(i.params[prop]));u=n._IEModeConversion(u,t,s,i),o={mode:"cors",cache:"no-cache",credentials:"include",keepalive:!0,method:i.method,headers:{"Content-Type":t["Content-Type"]},body:i.data};try{return fetch(u,o).then(function(e){n.log("Overridden request Fetch with KeepAlive finished with status "+e.status+":"+e.statusText)}).catch(function(e){n.log("Overridden request Fetch with KeepAlive returned error: "+e.message)}),{err:null}}catch(e){return n.log("Overridden request Fetch with KeepAlive threw error: "+e.message),{err:e.message,xhr:null}}}})}(),function(e){var t="object"==typeof exports&&exports,r="object"==typeof module&&module&&module.exports==t&&module,n="object"==typeof global&&global;n.global!==n&&n.window!==n||(e=n);var i,o,a=2147483647,s=36,u=/^xn--/,c=/[^ -~]/,l=/\x2E|\u3002|\uFF0E|\uFF61/g,C={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},I=Math.floor,_=String.fromCharCode;function S(e){throw RangeError(C[e])}function T(e,t){for(var r=e.length;r--;)e[r]=t(e[r]);return e}function d(e,t){return T(e.split(l),t).join(".")}function g(e){for(var t,r,n=[],i=0,o=e.length;i=55296&&t<=56319&&i65535&&(t+=_((e-=65536)>>>10&1023|55296),e=56320|1023&e),t+=_(e)}).join("")}function f(e){return e-48<10?e-22:e-65<26?e-65:e-97<26?e-97:s}function E(e,t){return e+22+75*(e<26)-((0!=t)<<5)}function p(e,t,r){var n=0;for(e=r?I(e/700):e>>1,e+=I(e/t);e>455;n+=s)e=I(e/35);return I(n+36*e/(e+38))}function O(e){var t,r,n,i,o,u,c,l,C,_,T=[],d=e.length,g=0,E=128,O=72;for((r=e.lastIndexOf("-"))<0&&(r=0),n=0;n=128&&S("not-basic"),T.push(e.charCodeAt(n));for(i=r>0?r+1:0;i=d&&S("invalid-input"),((l=f(e.charCodeAt(i++)))>=s||l>I((a-g)/u))&&S("overflow"),g+=l*u,!(l<(C=c<=O?1:c>=O+26?26:c-O));c+=s)u>I(a/(_=s-C))&&S("overflow"),u*=_;O=p(g-o,t=T.length+1,0==o),I(g/t)>a-E&&S("overflow"),E+=I(g/t),g%=t,T.splice(g++,0,E)}return R(T)}function h(e){var t,r,n,i,o,u,c,l,C,T,d,R,f,O,h,A=[];for(R=(e=g(e)).length,t=128,r=0,o=72,u=0;u=t&&dI((a-r)/(f=n+1))&&S("overflow"),r+=(c-t)*f,t=c,u=0;ua&&S("overflow"),d==t){for(l=r,C=s;!(l<(T=C<=o?1:C>=o+26?26:C-o));C+=s)h=l-T,O=s-T,A.push(_(E(T+h%O,0))),l=I(h/O);A.push(_(E(l,0))),o=p(r,f,n==i),r=0,++n}++r,++t}return A.join("")}if(i={version:"1.2.3",ucs2:{decode:g,encode:R},decode:O,encode:h,toASCII:function(e){return d(e,function(e){return c.test(e)?"xn--"+h(e):e})},toUnicode:function(e){return d(e,function(e){return u.test(e)?O(e.slice(4).toLowerCase()):e})}},"function"==typeof define&&"object"==typeof define.amd&&define.amd)define(function(){return i});else if(t&&!t.nodeType)if(r)r.exports=i;else for(o in i)i.hasOwnProperty(o)&&(t[o]=i[o]);else e.punycode=i}(this), +/*! + * URI.js - Mutating URLs + * + * Version: 1.14.2 + * + * Author: Rodney Rehm + * Web: http://medialize.github.io/URI.js/ + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * GPL v3 http://opensource.org/licenses/GPL-3.0 + * + */ +function(e,t){"use strict";"object"==typeof exports?module.exports=t(require("./punycode"),require("./IPv6"),require("./SecondLevelDomains")):"function"==typeof define&&define.amd?define(["./punycode","./IPv6","./SecondLevelDomains"],t):e.URI=t(e.punycode,e.IPv6,e.SecondLevelDomains,e)}(this,function(e,t,r,n){"use strict";var i=n&&n.URI;function o(e,t){if(!(this instanceof o))return new o(e,t);if(void 0===e){if(arguments.length)throw new TypeError("undefined is not a valid argument for URI");e="undefined"!=typeof location?location.href+"":""}return this.href(e),void 0!==t?this.absoluteTo(t):this}o.version="1.14.2";var a=o.prototype,s=Object.prototype.hasOwnProperty;function u(e){return e.replace(/([.*+?^=!:${}()|[\]\/\\])/g,"\\$1")}function c(e){return void 0===e?"Undefined":String(Object.prototype.toString.call(e)).slice(8,-1)}function l(e){return"Array"===c(e)}function C(e,t){var r,n;if(l(t)){for(r=0,n=t.length;r=48&&e<64||e>=65&&e<91||e>=97&&e<123||e>=160&&e<55296||e>=57344&&e<63743||e>=63744&&e<64976||e>=65008&&e<65520||e>=65536&&e<131070||e>=131072&&e<196606||e>=196608&&e<262142||e>=262144&&e<327678||e>=327680&&e<393214||e>=393216&&e<458750||e>=458752&&e<524286||e>=524288&&e<589822||e>=589824&&e<655358||e>=655360&&e<720894||e>=720896&&e<786430||e>=786432&&e<851966||e>=851968&&e<917502||e>=917504&&e<983038||e>=983040&&e<1048574||e>=1048576&&e<1114110}function d(t){for(var r=e.ucs2.decode(t),n="",i=0;i]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»]))/gi,o.findUri={start:/\b(?:([a-z][a-z0-9.+-]*:\/\/)|www\.)/gi,end:/[\s\r\n]|$/,trim:/[`!()\[\]{};:'".,<>?«»]+$/},o.defaultPorts={http:"80",https:"443",ftp:"21",gopher:"70",ws:"80",wss:"443"},o.invalid_hostname_characters=/[^a-zA-Z0-9\.-]/,o.domAttributes={a:"href",blockquote:"cite",link:"href",base:"href",script:"src",form:"action",img:"src",area:"href",iframe:"src",embed:"src",source:"src",track:"src",input:"src",audio:"src",video:"src"},o.getDomAttribute=function(e){if(e&&e.nodeName){var t=e.nodeName.toLowerCase();if("input"!==t||"image"===e.type)return o.domAttributes[t]}},o._defaultRecodeHostname=e?e.toASCII:function(e){return e},o.iso8859=function(){o.recodeHostname=o._defaultRecodeHostname,o.encode=escape,o.decode=unescape},o.unicode=function(){o.recodeHostname=o._defaultRecodeHostname,o.encode=S,o.decode=decodeURIComponent},o.iri=function(){o.recodeHostname=g,o.encode=d,o.decode=decodeURIComponent},o.unicode(),o.characters={pathname:{encode:{expression:/%(24|26|2B|2C|3B|3D|3A|40)/gi,map:{"%24":"$","%26":"&","%2B":"+","%2C":",","%3B":";","%3D":"=","%3A":":","%40":"@"}},decode:{expression:/[\/\?#]/g,map:{"/":"%2F","?":"%3F","#":"%23"}}},reserved:{encode:{expression:/%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/gi,map:{"%3A":":","%2F":"/","%3F":"?","%23":"#","%5B":"[","%5D":"]","%40":"@","%21":"!","%24":"$","%26":"&","%27":"'","%28":"(","%29":")","%2A":"*","%2B":"+","%2C":",","%3B":";","%3D":"="}}},urnpath:{encode:{expression:/%(21|24|27|28|29|2A|2B|2C|3B|3D|40)/gi,map:{"%21":"!","%24":"$","%27":"'","%28":"(","%29":")","%2A":"*","%2B":"+","%2C":",","%3B":";","%3D":"=","%40":"@"}},decode:{expression:/[\/\?#:]/g,map:{"/":"%2F","?":"%3F","#":"%23",":":"%3A"}}}},o.encodeQuery=function(e,t){var r=o.encode(e+"");return void 0===t&&(t=o.escapeQuerySpace),t?r.replace(/%20/g,"+"):r},o.decodeQuery=function(e,t){e+="",void 0===t&&(t=o.escapeQuerySpace);try{return o.decode(t?e.replace(/\+/g,"%20"):e)}catch(t){return e}};var R,f={encode:"encode",decode:"decode"},E=function(e,t){return function(r){try{return o[t](r+"").replace(o.characters[e][t].expression,function(r){return o.characters[e][t].map[r]})}catch(e){return r}}};for(R in f)o[R+"PathSegment"]=E("pathname",f[R]),o[R+"UrnPathSegment"]=E("urnpath",f[R]);var p=function(e,t,r){return function(n){var i;i=r?function(e){return o[t](o[r](e))}:o[t];for(var a=(n+"").split(e),s=0,u=a.length;s-1&&(t.fragment=e.substring(r+1)||null,e=e.substring(0,r)),(r=e.indexOf("?"))>-1&&(t.query=e.substring(r+1)||null,e=e.substring(0,r)),"//"===e.substring(0,2)?(t.protocol=null,e=e.substring(2),e=o.parseAuthority(e,t)):(r=e.indexOf(":"))>-1&&(t.protocol=e.substring(0,r)||null,t.protocol&&!t.protocol.match(o.protocol_expression)?t.protocol=void 0:"//"===e.substring(r+1,r+3)?(e=e.substring(r+3),e=o.parseAuthority(e,t)):(e=e.substring(r+1),t.urn=!0)),t.path=e,t},o.parseHost=function(e,t){var r,n,i=e.indexOf("/");if(-1===i&&(i=e.length),"["===e.charAt(0))r=e.indexOf("]"),t.hostname=e.substring(1,r)||null,t.port=e.substring(r+2,i)||null,"/"===t.port&&(t.port=null);else{var o=e.indexOf(":"),a=e.indexOf("/"),s=e.indexOf(":",o+1);-1!==s&&(-1===a||s-1?n:e.length-1);return i>-1&&(-1===n||i100)throw new Error("Invalid progress measure must be greater than or equal to 0 and less than or equal to 100: "+e)}this._progress=e},getProgress:function(){return this.log("getProgress"),this._progress},setFetch:function(r){var n,i,o;if(this.log("setFetch: ",r),this._fetch=r,this._fetchRequest=e,null===(n=r.toLowerCase().match(/([A-Za-z]+:)\/\/([^:\/]+):?(\d+)?(\/.*)?$/)))throw new Error("URL invalid: failed to divide URL parts");if(o=location.port,i=location.protocol.toLowerCase()===n[1],""===o&&(o="http:"===location.protocol.toLowerCase()?"80":"https:"===location.protocol.toLowerCase()?"443":""),!i||location.hostname.toLowerCase()!==n[2]||o!==(null!==n[3]&&void 0!==n[3]&&""!==n[3]?n[3]:"http:"===n[1]?"80":"https:"===n[1]?"443":"")){if(!u.hasCORS)throw this.log("[error] URL invalid: cross domain requests not supported in this browser"),new Error("URL invalid: cross domain requests not supported in this browser");if(u.useXDR&&i)this._fetchRequest=t;else if(u.useXDR&&!i)throw this.log("[error] URL invalid: cross domain request for differing scheme in IE with XDR"),new Error("URL invalid: cross domain request for differing scheme in IE with XDR")}},getFetch:function(){return this._fetch},setLRS:function(e,t){this.log("setLRS: ",e,t),null!==this._lrs?((void 0===t&&null===e||null!==e)&&(this._endpoint=this._lrs.endpoint=e),null!=t&&(this._lrs.auth=t)):this._lrs=new TinCan.LRS({endpoint:e,auth:t,allowFail:!1})},getLRS:function(){return this._lrs},setActor:function(e){if(e instanceof TinCan.Agent||(e=TinCan.Agent.fromJSON(e)),!(null!==e.account&&e.account instanceof TinCan.AgentAccount))throw new Error("Invalid actor: missing or invalid account");if(null===e.account.name)throw new Error("Invalid actor: name is null");if(""===e.account.name)throw new Error("Invalid actor: name is empty");if(null===e.account.homePage)throw new Error("Invalid actor: homePage is null");if(""===e.account.homePage)throw new Error("Invalid actor: homePage is empty");this._actor=e},getActor:function(){return this._actor},setActivity:function(e){if(e instanceof TinCan.Activity||(e=new TinCan.Activity({id:e})),null===e.id)throw new Error("Invalid activity: id is null");if(""===e.id)throw new Error("Invalid activity: id is empty");this._activity=e},getActivity:function(){return this._activity},setRegistration:function(e){if(null===e)throw new Error("Invalid registration: null");if(""===e)throw new Error("Invalid registration: empty");this._registration=e},getRegistration:function(){return this._registration},validateScore:function(e){if(null==e)throw new Error("cannot validate score (score not provided): "+e);if(void 0!==e.min&&!i(e.min))throw new Error("score.min is not an integer");if(void 0!==e.max&&!i(e.max))throw new Error("score.max is not an integer");if(void 0!==e.scaled){if(!/^(\-|\+)?[01]+(\.[0-9]+)?$/.test(e.scaled))throw new Error("scaled score not a recognized number: "+e.scaled);if(e.scaled<0)throw new Error("scaled score must be greater than or equal to 0");if(e.scaled>1)throw new Error("scaled score must be less than or equal to 1")}if(void 0!==e.raw){if(!i(e.raw))throw new Error("score.raw is not an integer");if(void 0===e.min)throw new Error("minimum score must be provided when including a raw score");if(void 0===e.max)throw new Error("maximum score must be provided when including a raw score");if(e.rawe.max)throw new Error("raw score must be less than or equal to maximum score")}return!0},_getLanguageTag:function(){return TCAPI_GetLanguageTag()},_updateVerbDisplayLanguage:function(e,t){return CMI5_UpdateVerbDisplayLanguage(e,t)},prepareStatement:function(e){var t={actor:this._actor,verb:{id:e},target:this._activity,context:this._prepareContext()},r=this.getProgress(),n=this._getLanguageTag();return void 0!==p[e]&&(t.verb.display=this._updateVerbDisplayLanguage(p[e],n)),e!==R&&null!==r&&(t.result={extensions:{"https://w3id.org/xapi/cmi5/result/extensions/progress":r}}),new TinCan.Statement(t)},sendStatement:function(e,t){var r,n;if(t&&(r=function(r,n){null===r?t(r,n,e):t(new Error(r),n)}),n=this._lrs.saveStatement(e,{callback:r}),!t)return{response:n,statement:e}},initializedStatement:function(){return this.log("initializedStatement"),this._prepareStatement(d)},terminatedStatement:function(){this.log("terminatedStatement");var e=this._prepareStatement(g);return e.result=e.result||new TinCan.Result,e.result.duration=TinCan.Utils.convertMillisecondsToISO8601Duration(this.getDuration()),e},passedStatement:function(e){this.log("passedStatement");var t,r=this._prepareStatement(f);if(r.result=r.result||new TinCan.Result,r.result.success=!0,r.result.duration=TinCan.Utils.convertMillisecondsToISO8601Duration(this.getDuration()),e){try{this.validateScore(e)}catch(e){throw new Error("Invalid score - "+e)}if(null!==(t=this.getMasteryScore())&&void 0!==e.scaled){if(e.scaled=t)throw new Error("Invalid score - scaled score exceeds mastery score ("+e.scaled+" >= "+t+")");r.context.extensions=r.context.extensions||{},r.context.extensions[T]=t}r.result.score=new TinCan.Score(e)}return r.context.contextActivities.category.push(_),r},completedStatement:function(){this.log("completedStatement");var e=this._prepareStatement(R);return e.result=e.result||new TinCan.Result,e.result.completion=!0,e.result.duration=TinCan.Utils.convertMillisecondsToISO8601Duration(this.getDuration()),e.context.contextActivities.category.push(_),e},_prepareContext:function(){var e=JSON.parse(this._contextTemplate);return e.registration=this._registration,this._includeSourceActivity&&(e.contextActivities=e.contextActivities||new TinCan.ContextActivities,e.contextActivities.other=e.contextActivities.other||[],e.contextActivities.other.push(S)),e},_prepareStatement:function(e){var t=this.prepareStatement(e);return t.context.contextActivities=t.context.contextActivities||new TinCan.ContextActivities,t.context.contextActivities.category=t.context.contextActivities.category||[],t.context.contextActivities.category.push(I),t}},Cmi5.enableDebug=function(e){Cmi5.DEBUG=!0,e&&TinCan.enableDebug()},Cmi5.disableDebug=function(e){Cmi5.DEBUG=!1,e&&TinCan.disableDebug()},r=function(e,t,r,n){var i,o,a;return this.log("requestComplete: "+r.finished+", xhr.status: "+e.status),a=void 0===e.status?r.fakeStatus:1223===e.status?204:e.status,r.finished?i:(r.finished=!0,o=t.ignore404&&404===a,a>=200&&a<400||o?n?void n(null,e):i={err:null,xhr:e}:(i={err:a,xhr:e},0===a?this.log("[warning] There was a problem communicating with the server. Aborted, offline, or invalid CORS endpoint ("+a+")"):this.log("[warning] There was a problem communicating with the server. ("+a+" | "+e.responseText+")"),n&&n(a,e),i))},e=function(e,t,n){this.log("sendRequest using XMLHttpRequest");var i,o,a,s,u=this,c=[],l={finished:!1,fakeStatus:null},C=e;for(o in this.log("sendRequest using XMLHttpRequest - async: "+s),(t=t||{}).params=t.params||{},t.headers=t.headers||{},s=void 0!==n,t.params)t.params.hasOwnProperty(o)&&c.push(o+"="+encodeURIComponent(t.params[o]));for(o in c.length>0&&(C+="?"+c.join("&")),(i=new XMLHttpRequest).open(t.method,C,s),t.headers)t.headers.hasOwnProperty(o)&&i.setRequestHeader(o,t.headers[o]);void 0!==t.data&&(t.data+=""),a=t.data,s&&(i.onreadystatechange=function(){u.log("xhr.onreadystatechange - xhr.readyState: "+i.readyState),4===i.readyState&&r.call(u,i,t,l,n)});try{i.send(a)}catch(e){this.log("sendRequest caught send exception: "+e)}if(!s)return r.call(this,i,t,l)},t=function(e,t,i){this.log("sendRequest using XDomainRequest");var o,a,s,u,c=this,l=[],C={finished:!1,fakeStatus:null};if((t=t||{}).params=t.params||{},t.headers=t.headers||{},void 0!==t.headers["Content-Type"]&&"application/json"!==t.headers["Content-Type"])return u=new Error("Unsupported content type for IE Mode request"),i?(i(u,null),null):{err:u,xhr:null};for(a in t.params)t.params.hasOwnProperty(a)&&l.push(a+"="+encodeURIComponent(t.params[a]));l.length>0&&(e+="?"+l.join("&")),(o=new XDomainRequest).open("POST",e),i?(o.onload=function(){C.fakeStatus=200,r.call(c,o,t,C,i)},o.onerror=function(){C.fakeStatus=400,r.call(c,o,t,C,i)},o.ontimeout=function(){C.fakeStatus=0,r.call(c,o,t,C,i)}):(o.onload=function(){C.fakeStatus=200},o.onerror=function(){C.fakeStatus=400},o.ontimeout=function(){C.fakeStatus=0}),o.onprogress=function(){},o.timeout=0;try{o.send(undefined)}catch(e){this.log("sendRequest caught send exception: "+e)}if(!i){for(s=1e4+Date.now(),this.log("sendRequest - until: "+s+", finished: "+C.finished);Date.now()0){if(void 0!==(t=tincan.sendStatements(tcapi_cache.statementQueue)).results&&t.results.length>0&&null!==(r=t.results[0]).err)return _TCAPI_HandleRequestErrorResult(r,"Failed to commit data: statements"),n&&(tcapi_cache.statementQueue.pop(),TCAPI_UPDATES_PENDING=!0),!1;tcapi_cache.statementQueue=[]}return!0}function TCAPI_Finish(e,t){return WriteToDebug("In TCAPI_Finish - exitType: "+e),TCAPI_ClearErrorInfo(),e===EXIT_TYPE_SUSPEND&&(_TCAPI_SetStateSafe(TCAPI_STATE_TOTAL_TIME,TCAPI_GetPreviouslyAccumulatedTime()+GetSessionAccumulatedTime()),TCAPI_SetSuspended()),TCAPI_CommitData(),!0}function TCAPI_GetAudioPlayPreference(){WriteToDebug("In TCAPI_GetAudioPlayPreference");var e,t=0;return TCAPI_ClearErrorInfo(),null!==(e=tincan.getState(TCAPI_STATE_AUDIO_PREFERENCE)).state&&(t=e.state.contents),WriteToDebug("intTempPreference="+(t=parseInt(t,10))),t>0?(WriteToDebug("Returning On"),PREFERENCE_ON):0==t?(WriteToDebug("Returning Default"),PREFERENCE_DEFAULT):t<0?(WriteToDebug("returning Off"),PREFERENCE_OFF):(WriteToDebug("Error: Invalid preference"),TCAPI_SetErrorInfoManually(TCAPI_ERROR_INVALID_PREFERENCE,"Invalid audio preference received from LMS","intTempPreference="+t),null)}function TCAPI_GetAudioVolumePreference(){WriteToDebug("In TCAPI_GetAudioVollumePreference");var e,t=100;return TCAPI_ClearErrorInfo(),null!==(e=tincan.getState(TCAPI_STATE_AUDIO_PREFERENCE)).state&&(t=e.state.contents),WriteToDebug("intTempPreference="+t),(t=parseInt(t,10))<=0&&(WriteToDebug("Setting to 100"),t=100),t>100?(WriteToDebug("ERROR: invalid preference"),TCAPI_SetErrorInfoManually(TCAPI_ERROR_INVALID_PREFERENCE,"Invalid audio preference received from LMS","intTempPreference="+t),null):(WriteToDebug("Returning "+t),t)}function TCAPI_SetAudioPreference(e,t){return WriteToDebug("In TCAPI_SetAudioPreference PlayPreference="+e+", intPercentOfMaxVolume="+t),TCAPI_ClearErrorInfo(),e==PREFERENCE_OFF&&(WriteToDebug("Setting percent to -1 - OFF"),t=-1),_TCAPI_SetStateSafe(TCAPI_STATE_AUDIO_PREFERENCE,t)}function TCAPI_SetLanguagePreference(e){return WriteToDebug("In TCAPI_SetLanguagePreference strLanguage="+e),TCAPI_ClearErrorInfo(),_TCAPI_SetStateSafe(TCAPI_STATE_LANGUAGE_PREFERENCE,e)}function TCAPI_GetLanguagePreference(){var e,t;return WriteToDebug("In TCAPI_GetLanguagePreference"),TCAPI_ClearErrorInfo(),null!==(t=tincan.getState(TCAPI_STATE_LANGUAGE_PREFERENCE)).state&&(e=t.state.contents),e}function TCAPI_SetSpeedPreference(e){var t;return WriteToDebug("In TCAPI_SetSpeedPreference intPercentOfMax="+e),TCAPI_ClearErrorInfo(),WriteToDebug("intTCAPISpeed="+(t=2*e-100)),_TCAPI_SetStateSafe(TCAPI_STATE_SPEED_PREFERENCE,t)}function TCAPI_GetSpeedPreference(){WriteToDebug("In TCAPI_GetSpeedPreference");var e,t,r=100;return TCAPI_ClearErrorInfo(),null!==(t=tincan.getState(TCAPI_STATE_SPEED_PREFERENCE)).state&&(r=t.state.contents),WriteToDebug("intTCAPISpeed="+r),ValidInteger(r)?(r=parseInt(r,10))<-100||r>100?(WriteToDebug("ERROR - out of range"),TCAPI_SetErrorInfoManually(TCAPI_ERROR_INVALID_SPEED,"Invalid speed preference received from LMS - out of range","intTCAPISpeed="+r),null):(e=(r+100)/2,WriteToDebug("Returning "+(e=parseInt(e,10))),e):(WriteToDebug("ERROR - invalid integer"),TCAPI_SetErrorInfoManually(TCAPI_ERROR_INVALID_SPEED,"Invalid speed preference received from LMS - not an integer","intTCAPISpeed="+r),null)}function TCAPI_SetTextPreference(e){return WriteToDebug("In TCAPI_SetTextPreference intPreference="+e),TCAPI_ClearErrorInfo(),_TCAPI_SetStateSafe(TCAPI_STATE_TEXT_PREFERENCE,e)}function TCAPI_GetTextPreference(){WriteToDebug("In TCAPI_GetTextPreference");var e,t=0;return TCAPI_ClearErrorInfo(),null!==(e=tincan.getState(TCAPI_STATE_TEXT_PREFERENCE)).state&&(t=e.state.contents),WriteToDebug("intTempPreference="+(t=parseInt(t,10))),t>0?(WriteToDebug("Returning On"),PREFERENCE_ON):0==t||""==t?(WriteToDebug("Returning Default"),PREFERENCE_DEFAULT):t<0?(WriteToDebug("returning Off"),PREFERENCE_OFF):(WriteToDebug("Error: Invalid preference"),TCAPI_SetErrorInfoManually(TCAPI_ERROR_INVALID_PREFERENCE,"Invalid text preference received from LMS","intTempPreference="+t),null)}function TCAPI_GetPreviouslyAccumulatedTime(){WriteToDebug("In TCAPI_GetPreviouslyAccumulatedTime");var e,t=0;return WriteToDebug("In TCAPI_GetPreviouslyAccumulatedTime - cached: "+tcapi_cache.totalPrevDuration),null===tcapi_cache.totalPrevDuration&&(null!==(e=tincan.getState(TCAPI_STATE_TOTAL_TIME)).state&&(t=Number(e.state.contents)),tcapi_cache.totalPrevDuration=NaN===t?0:t),tcapi_cache.totalPrevDuration}function TCAPI_SaveTime(e){return WriteToDebug("In TCAPI_SaveTime"),!0}function TCAPI_GetMaxTimeAllowed(){return WriteToDebug("In TCAPI_GetMaxTimeAllowed"),null}function TCAPI_SetScore(e,t,r){return WriteToDebug("In TCAPI_SetScore intScore="+e+", intMaxScore="+t+", intMinScore="+r),TCAPI_ClearErrorInfo(),TCAPI_SCORE.raw=e,TCAPI_SCORE.max=t,TCAPI_SCORE.min=r,WriteToDebug("Returning "+TCAPI_SCORE),TCAPI_UPDATES_PENDING=!0,!0}function TCAPI_GetScore(){return WriteToDebug("In TCAPI_GetScore"),TCAPI_ClearErrorInfo(),WriteToDebug("Returning "+TCAPI_SCORE.raw),TCAPI_SCORE.raw}function TCAPI_SetPointBasedScore(e,t,r){return WriteToDebug("TCAPI_SetPointBasedScore - TCAPI does not support SetPointBasedScore, falling back to SetScore"),TCAPI_SetScore(e,t,r)}function TCAPI_GetScaledScore(e,t,r){return WriteToDebug("TCAPI_GetScaledScore - TCAPI does not support GetScaledScore, returning false"),!1}function TCAPI_RecordInteraction(e,t,r,n,i,o,a,s,u,c,l,C,I){var _,S,T={},d=TCAPI_GetLanguageTag();switch(interactionActivityId=e,interactionActivityType=TCAPI_INTERACTION,TCAPI_ClearErrorInfo(),S=0==e.indexOf(tincan.activity.id+"-")?e.substring(tincan.activity.id.length+1):e,TCAPI_DONT_USE_BROKEN_URN_IDS||(interactionActivityId=tincan.activity.id+"-urn:scormdriver:"+S),S&&""!==S||(S=e),c){case"true-false":(T={id:interactionActivityId,definition:{description:{},type:interactionActivityType,interactionType:"true-false",name:{}}}).definition.description[d]=i,T.definition.name[d]=i;break;case"choice":(T={id:interactionActivityId,definition:{description:{},type:interactionActivityType,interactionType:"choice",name:{}}}).definition.description[d]=i,T.definition.name[d]=i,I&&I.length>0&&(T.definition.choices=I.map(function(e){var t={};return t[d]=e.description,new TinCan.InteractionComponent({id:e.id,description:t})}));break;case"fill-in":(T={id:interactionActivityId,definition:{description:{},type:interactionActivityType,interactionType:"fill-in",name:{}}}).definition.description[d]=i,T.definition.name[d]=i;break;case"matching":(T={id:interactionActivityId,definition:{description:{},type:interactionActivityType,interactionType:"matching",name:{}}}).definition.description[d]=i,T.definition.name[d]=i,I&&"object"==typeof I&&(I.source&&I.source.length>0&&(T.definition.source=I.source.map(function(e){var t={};return t[d]=e.description,new TinCan.InteractionComponent({id:e.id,description:t})})),I.target&&I.target.length>0&&(T.definition.target=I.target.map(function(e){var t={};return t[d]=e.description,new TinCan.InteractionComponent({id:e.id,description:t})})));break;case"performance":(T={id:interactionActivityId,definition:{description:{},type:interactionActivityType,interactionType:"performance",name:{}}}).definition.description[d]=i,T.definition.name[d]=i;break;case"sequencing":(T={id:interactionActivityId,definition:{description:{},type:interactionActivityType,interactionType:"sequencing",name:{}}}).definition.description[d]=i,T.definition.name[d]=i;break;case"likert":(T={id:interactionActivityId,definition:{description:{},type:interactionActivityType,interactionType:"likert",name:{}}}).definition.description[d]=i,T.definition.name[d]=i;break;case"numeric":(T={id:interactionActivityId,definition:{description:{},type:interactionActivityType,interactionType:"numeric",name:{}}}).definition.description[d]=i,T.definition.name[d]=i;break;case"other":(T={id:interactionActivityId,definition:{description:{},type:interactionActivityType,interactionType:"other",name:{}}}).definition.description[d]=i,T.definition.name[d]=i;break;default:return WriteToDebug("TCAPI_RecordInteraction received an invalid TCPAIInteractionType of "+c),!1}if(null!==T.id){null!==n&&""!==n&&(T.definition.correctResponsesPattern=[n]);var g={id:"http://adlnet.gov/expapi/verbs/answered",display:{}};g.display[d]="answered",_={verb:g,object:T,context:{contextActivities:{parent:tincan.activity,grouping:{id:tincan.activity.id+"-"+s}}}},(null!==t||null!==a&&""!==a||null!==o&&""!==o)&&(_.result={},null!==t&&(_.result.response=t,!0===r||r===INTERACTION_RESULT_CORRECT?_.result.success=!0:!1!==r&&""!==r&&"false"!==r&&r!==INTERACTION_RESULT_WRONG||(_.result.success=!1)),null!==a&&""!==a&&(_.result.duration=TinCan.Utils.convertMillisecondsToISO8601Duration(a)),null!==o&&""!==o&&(_.result.extensions=_.result.extensions||{},_.result.extensions["http://id.tincanapi.com/extension/cmi-interaction-weighting"]=o)),tcapi_cache.statementQueue.push(_)}return!0}function TCAPI_RecordTrueFalseInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In TCAPI_RecordTrueFalseInteraction strID="+e+", blnResponse="+t+", blnCorrect="+r+", blnCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l=null;return!0===t?c="true":!1===t&&(c="false"),!0===n?l="true":!1===n&&(l="false"),TCAPI_RecordInteraction(e,c,r,l,i,o,a,s,u,TCAPI_INTERACTION_TYPE_TRUE_FALSE,c,l)}function TCAPI_RecordMultipleChoiceInteraction(e,t,r,n,i,o,a,s,u,c){WriteToDebug("In TCAPI_RecordMultipleChoiceInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u+", choices="+JSON.stringify(c));var l=null,C=null,I="",_="";if(null!==t){l="",C="";for(var S=0;S0&&(l+="[,]"),C.length>0&&(C+="[,]");var T=SafeExtractResponseId(t[S],!0);l+=T,C+=T}}for(S=0;S0&&(I+="[,]"),_.length>0&&(_+="[,]");var d=SafeExtractResponseId(n[S],!0);I+=d,_+=d}return TCAPI_RecordInteraction(e,C,r,_,i,o,a,s,u,TCAPI_INTERACTION_TYPE_CHOICE,l,I,c)}function TCAPI_RecordFillInInteraction(e,t,r,n,i,o,a,s,u){return WriteToDebug("In TCAPI_RecordFillInInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u),null!==t&&(t=new String(t)).length>255&&(t=t.substr(0,255)),null!==n&&(n=new String(n)).length>255&&(n=n.substr(0,255)),TCAPI_RecordInteraction(e,t,r,n,i,o,a,s,u,TCAPI_INTERACTION_TYPE_FILL_IN,t,n)}function TCAPI_RecordMatchingInteraction(e,t,r,n,i,o,a,s,u,c){WriteToDebug("In TCAPI_RecordMatchingInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u+", choices="+JSON.stringify(c));var l=null,C=null,I="",_="";if(null!==t){l="",C="";for(var S=0;S0&&(l+="[,]"),C.length>0&&(C+="[,]"),l+=(T=SafeExtractResponseId(t[S].Source,!0))+"[.]"+(d=SafeExtractResponseId(t[S].Target,!0)),C+=T+"[.]"+d}}for(S=0;S0&&(I+="[,]"),_.length>0&&(_+="[,]"),I+=(T=SafeExtractResponseId(n[S].Source,!0))+"[.]"+(d=SafeExtractResponseId(n[S].Target,!0)),_+=T+"[.]"+d}return TCAPI_RecordInteraction(e,C,r,_,i,o,a,s,u,TCAPI_INTERACTION_TYPE_MATCHING,l,I,c)}function TCAPI_RecordPerformanceInteraction(e,t,r,n,i,o,a,s,u){return WriteToDebug("In TCAPI_RecordPerformanceInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u),null!==t&&(t=new String(t)).length>255&&(t=t.substr(0,255)),null!==n&&(n=new String(n)).length>255&&(n=n.substr(0,255)),TCAPI_RecordInteraction(e,t,r,n,i,o,a,s,u,TCAPI_INTERACTION_TYPE_PERFORMANCE,t,n)}function TCAPI_RecordSequencingInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In TCAPI_RecordSequencingInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l=null,C="",I="";if(null!==t){c="",l="";for(var _=0;_0&&(c+="[,]"),l.length>0&&(l+="[,]");var S=SafeExtractResponseId(t[_],!0);c+=S,l+=S}}for(_=0;_0&&(C+="[,]"),I.length>0&&(I+="[,]");var T=SafeExtractResponseId(n[_],!0);C+=T,I+=T}return TCAPI_RecordInteraction(e,l,r,I,i,o,a,s,u,TCAPI_INTERACTION_TYPE_SEQUENCING,c,C)}function TCAPI_RecordLikertInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In TCAPI_RecordLikertInteraction strID="+e+", response="+t+", blnCorrect="+r+", correctResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l=null,C=null,I=null;return null!==t&&(c=SafeExtractResponseId(t,!0),l=SafeExtractResponseId(t,!0)),null!==n&&(C=SafeExtractResponseId(n,!0),I=SafeExtractResponseId(n,!0)),TCAPI_RecordInteraction(e,l,r,I,i,o,a,s,u,TCAPI_INTERACTION_TYPE_LIKERT,c,C)}function TCAPI_RecordNumericInteraction(e,t,r,n,i,o,a,s,u){return WriteToDebug("In TCAPI_RecordNumericInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u),null==n||IsValidDecimalRange(n)||IsValidDecimal(n)?TCAPI_RecordInteraction(e,t,r,n,i,o,a,s,u,TCAPI_INTERACTION_TYPE_NUMERIC,t,n):(WriteToDebug("Returning False - TCAPI_RecordNumericInteraction received invalid correct response (not a decimal or range), strCorrectResponse="+n),!1)}function TCAPI_GetEntryMode(){return WriteToDebug("In TCAPI_GetEntryMode"),null}function TCAPI_GetLessonMode(){return WriteToDebug("In TCAPI_GetLessonMode"),null}function TCAPI_GetTakingForCredit(){return WriteToDebug("In TCAPI_GetTakingForCredit"),null}function TCAPI_SetObjectiveScore(e,t,r,n){return WriteToDebug("In TCAPI_SetObjectiveScore, strObejctiveID="+e+", intScore="+t+", intMaxScore="+r+", intMinScore="+n),!1}function TCAPI_SetObjectiveDescription(e,t){return WriteToDebug("In TCAPI_SetObjectiveDescription, strObjectiveDescription="+t),!1}function TCAPI_SetObjectiveStatus(e,t){return WriteToDebug("In TCAPI_SetObjectiveStatus strObjectiveID="+e+", Lesson_Status="+t),!1}function TCAPI_GetObjectiveScore(e){return WriteToDebug("In TCAPI_GetObjectiveScore, strObejctiveID="+e),!1}function TCAPI_GetObjectiveDescription(e){return WriteToDebug("In TCAPI_GetObjectiveDescription, strObejctiveID="+e),!1}function TCAPI_GetObjectiveStatus(e){return WriteToDebug("In TCAPI_GetObjectiveStatus, strObejctiveID="+e),!1}function TCAPI_SetSuspended(){return WriteToDebug("In TCAPI_SetSuspended"),TCAPI_IN_PROGRESS&&(TCAPI_IN_PROGRESS=!1,TCAPI_UPDATES_PENDING=!0),!0}function TCAPI_SetFailed(){return WriteToDebug("In TCAPI_SetFailed"),TCAPI_STATUS=TCAPI_VERB_FAILED,TCAPI_STATUS_CHANGED=!0,TCAPI_SATISFACTION_STATUS=!1,TCAPI_IN_PROGRESS=!1,TCAPI_UPDATES_PENDING=!0,!0}function TCAPI_SetPassed(){return WriteToDebug("In TCAPI_SetPassed"),TCAPI_STATUS=TCAPI_VERB_PASSED,TCAPI_STATUS_CHANGED=!0,TCAPI_SATISFACTION_STATUS=!0,TCAPI_IN_PROGRESS=!1,TCAPI_UPDATES_PENDING=!0,!0}function TCAPI_SetCompleted(){return WriteToDebug("In TCAPI_SetCompleted"),TCAPI_ClearErrorInfo(),TCAPI_STATUS===TCAPI_INIT_VERB&&(TCAPI_STATUS=TCAPI_VERB_COMPLETED,TCAPI_STATUS_CHANGED=!0),TCAPI_COMPLETION_STATUS=TCAPI_VERB_COMPLETED,TCAPI_IN_PROGRESS=!1,TCAPI_UPDATES_PENDING=!0,TCAPI_SendProgressed(100),!0}function TCAPI_ResetStatus(){return WriteToDebug("In TCAPI_ResetStatus"),TCAPI_ClearErrorInfo(),TCAPI_STATUS=TCAPI_INIT_VERB,TCAPI_STATUS_CHANGED=!0,TCAPI_COMPLETION_STATUS="",TCAPI_SATISFACTION_STATUS=null,TCAPI_IN_PROGRESS=!0,TCAPI_UPDATES_PENDING=!0,!0}function TCAPI_GetStatus(){WriteToDebug("In TCAPI_GetStatus");var e="";return TCAPI_ClearErrorInfo(),WriteToDebug("In TCAPI_GetStatus - strStatus="+(e=TCAPI_STATUS===TCAPI_VERB_COMPLETED?"completed":TCAPI_STATUS===TCAPI_VERB_ATTEMPTED?"attempted":TCAPI_STATUS===TCAPI_VERB_PASSED?"passed":TCAPI_STATUS===TCAPI_VERB_FAILED?"failed":TCAPI_STATUS)),e}function TCAPI_CreateValidIdentifier(e){return"TCAPI"===strLMSStandard?tincan.activity.id+"/"+e:tincan.activity.id+"-"+encodeURIComponent(e)}function TCAPI_SetNavigationRequest(e){return WriteToDebug("TCAPI_GetNavigationRequest - TCAPI does not support navigation requests, returning false"),!1}function TCAPI_GetNavigationRequest(){return WriteToDebug("TCAPI_GetNavigationRequest - TCAPI does not support navigation requests, returning false"),!1}function TCAPI_CreateDataBucket(e,t,r){return WriteToDebug("TCAPI_CreateDataBucket - TCAPI does not support SSP, returning false"),!1}function TCAPI_GetDataFromBucket(e){return WriteToDebug("TCAPI_GetDataFromBucket - TCAPI does not support SSP, returning empty string"),""}function TCAPI_PutDataInBucket(e,t,r){return WriteToDebug("TCAPI_PutDataInBucket - TCAPI does not support SSP, returning false"),!1}function TCAPI_DetectSSPSupport(){return WriteToDebug("TCAPI_DetectSSPSupport - TCAPI does not support SSP, returning false"),!1}function TCAPI_GetBucketInfo(e){return WriteToDebug("TCAPI_GetBucketInfo - TCAPI does not support SSP, returning empty SSPBucketSize"),new SSPBucketSize(0,0)}function TCAPI_WriteComment(e){return WriteToDebug("In TCAPI_WriteComment - TCAPI does not support comments"),!1}function TCAPI_GetComments(){return WriteToDebug("In TCAPI_GetComments - TCAPI does not support comments"),""}function TCAPI_GetLMSComments(){return WriteToDebug("In TCAPI_GetLMSComments - TCAPI does not support LMS comments"),""}function TCAPI_GetLaunchData(){return WriteToDebug("In TCAPI_GetLaunchData - TCAPI does not support launch data"),!1}function TCAPI_DisplayMessageOnTimeout(){return TCAPI_ClearErrorInfo(),WriteToDebug("In TCAPI_DisplayMessageOnTimeout - TCAPI does not support MessageOnTimeout"),!1}function TCAPI_ExitOnTimeout(){return WriteToDebug("In TCAPI_ExitOnTimeout - TCAPI does not support ExitOnTimeout"),!1}function TCAPI_GetPassingScore(){return WriteToDebug("In TCAPI_GetPassingScore - TCAPI does not support GetPassingScore"),!1}function TCAPI_GetProgressMeasure(){return WriteToDebug("TCAPI_GetProgressMeasure - TCAPI does not support progress_measure, returning false"),!1}function TCAPI_SetProgressMeasure(e){return WriteToDebug("In TCAPI_SetProgressMeasure - numMeasure: "+e),TCAPI_ClearErrorInfo(),TCAPI_SendProgressed(e)}function TCAPI_GetObjectiveProgressMeasure(){return WriteToDebug("TCAPI_GetObjectiveProgressMeasure - TCAPI does not support progress_measure, returning false"),!1}function TCAPI_SetObjectiveProgressMeasure(){return WriteToDebug("TCAPI_SetObjectiveProgressMeasure - TCAPI does not support progress_measure, returning false"),!1}function TCAPI_GetInteractionType(e){return WriteToDebug("TCAPI_GetInteractionType - TCAPI does not support interaction retrieval, returning empty string"),""}function TCAPI_GetInteractionTimestamp(e){return WriteToDebug("TCAPI_GetInteractionTimestamp - TCAPI does not support interaction retrieval, returning empty string"),""}function TCAPI_GetInteractionCorrectResponses(e){return WriteToDebug("TCAPI_GetInteractionCorrectResponses - TCAPI does not support interaction retrieval, returning empty array"),[]}function TCAPI_GetInteractionWeighting(e){return WriteToDebug("TCAPI_GetInteractionWeighting - TCAPI does not support interaction retrieval, returning empty string"),""}function TCAPI_GetInteractionLearnerResponses(e){return WriteToDebug("TCAPI_GetInteractionLearnerResponses - TCAPI does not support interaction retrieval, returning empty array"),[]}function TCAPI_GetInteractionResult(e){return WriteToDebug("TCAPI_GetInteractionResult - TCAPI does not support interaction retrieval, returning empty string"),""}function TCAPI_GetInteractionLatency(e){return WriteToDebug("TCAPI_GetInteractionDescription - TCAPI does not support interaction retrieval, returning empty string"),""}function TCAPI_GetInteractionDescription(e){return WriteToDebug("TCAPI_GetInteractionDescription - TCAPI does not support interaction retrieval, returning empty string"),""}function TCAPI_ClearErrorInfo(){WriteToDebug("In TCAPI_ClearErrorInfo"),intTCAPIError=TCAPI_NO_ERROR,strTCAPIErrorString="",strTCAPIErrorDiagnostic=""}function TCAPI_SetErrorInfoManually(e,t,r){WriteToDebug("In TCAPI_SetErrorInfoManually"),WriteToDebug("ERROR-Num="+e),WriteToDebug(" String="+t),WriteToDebug(" Diag="+r),intTCAPIError=e,strTCAPIErrorString=t,strTCAPIErrorDiagnostic=r}function TCAPI_GetLastError(){return WriteToDebug("In TCAPI_GetLastError"),intTCAPIError===TCAPI_NO_ERROR?(WriteToDebug("Returning No Error"),NO_ERROR):(WriteToDebug("Returning "+intTCAPIError),intTCAPIError)}function TCAPI_GetLastErrorDesc(){return WriteToDebug("In TCAPI_GetLastErrorDesc, "+strTCAPIErrorString+"\n"+strTCAPIErrorDiagnostic),strTCAPIErrorString+"\n"+strTCAPIErrorDiagnostic}var cmi5,CMI5_PENDING_STATUS={completion:null,success:null,score:null},CMI5_COMMITTED_STATUS={completion:null,success:null,score:null,launchModes:[]},CMI5_STATEMENT_QUEUE=[],CMI5_SESSION_DURATION=null,CMI5_TOTAL_PREV_DURATION=null,CMI5_ENTRY_MODE=null,CMI5_INTERACTIONS={},CMI5_SSP_BUCKETS={},CMI5_VERB_ID_FAILED="http://adlnet.gov/expapi/verbs/failed",CMI5_VERB_ID_PASSED="http://adlnet.gov/expapi/verbs/passed",CMI5_VERB_ID_COMPLETED="http://adlnet.gov/expapi/verbs/completed",CMI5_EXTENSION_MASTERY_SCORE="https://w3id.org/xapi/cmi5/context/extensions/masteryscore",CMI5_PREF_AUDIO_PLAY=null,CMI5_PREF_AUDIO_VOLUME=null,CMI5_PREF_LANGUAGE=null,CMI5_PREF_SPEED=null,CMI5_PREF_TEXT=null,CMI5_INTERACTION_ACTIVITY_TYPE="http://adlnet.gov/expapi/activities/cmi.interaction",CMI5_INTERACTION_TYPE_TRUE_FALSE="true-false",CMI5_INTERACTION_TYPE_CHOICE="choice",CMI5_INTERACTION_TYPE_FILL_IN="fill-in",CMI5_INTERACTION_TYPE_LONG_FILL_IN="long-fill-in",CMI5_INTERACTION_TYPE_MATCHING="matching",CMI5_INTERACTION_TYPE_PERFORMANCE="performance",CMI5_INTERACTION_TYPE_SEQUENCING="sequencing",CMI5_INTERACTION_TYPE_LIKERT="likert",CMI5_INTERACTION_TYPE_NUMERIC="numeric",CMI5_STATE_BOOKMARK="bookmark",CMI5_STATE_GENERIC_DATA="generic_data",CMI5_STATE_TOTAL_TIME="cumulative_time",CMI5_STATE_COMMITTED_STATUS="status",CMI5_STATE_COMMENTS="learner_comments",CMI5_STATE_SSP_BUCKET_PREFIX="sspBucket",CMI5_STATE_LANGUAGE_PREFERENCE="language_preference",CMI5_STATE_VOLUME_PREFERENCE="audio_preference",CMI5_STATE_SPEED_PREFERENCE="speed_preference",CMI5_STATE_TEXT_PREFERENCE="text_preference",CMI5_NO_ERROR="",CMI5_CONN_ERROR=998,CMI5_DEP_ERROR=999,CMI5_ERROR_NOT_IMPLEMENTED=998,CMI5_ERROR_INVALID_SCORE=1,CMI5_ERROR_UNRECOGNIZED_SSP_BUCKET=2,CMI5_ERROR_UNRECOGNIZED_INTERACTION=3,CMI5_ERROR_NON_NORMAL_MODE=4,CMI5_ERROR_ALREADY_COMMITTED=5,CMI5_ERROR_INVALID_STATUS_BASED_ON_SCORE=6,CMI5_ERROR_SSP_BUCKET_LOAD_FAILED=7,CMI5_ERROR_SSP_BUCKET_ALREADY_EXISTS=8,CMI5_ERROR_SSP_BUCKET_INVALID_JSON=9,CMI5_ERROR_INTERACTION_INVALID_JSON=10,CMI5_ERROR_INTERACTION_LOAD_FAILED=11,intCMI5Error=CMI5_NO_ERROR,strCMI5ErrorString="",strCMI5ErrorDiagnostic="";function CMI5_SaveState(e,t,r,n,i){WriteToDebug("In CMI5_SaveState - key: "+e+", value: "+t);var o,a,s,u,c={agent:cmi5.getActor(),activity:cmi5.getActivity(),contentType:r};return i&&(WriteToDebug("In CMI5_SaveState - Setting callback to async!"),c.callback=i),(null==n||n)&&(c.registration=cmi5.getRegistration()),o=cmi5.getLRS().saveState(e,t,c),void 0!==c.callback||null===o.err||(WriteToDebug("Failed to save "+e+" in state: "+o.err),s="Failed to save state: "+e,/^\d+$/.test(o.err)?(a=o.err,u=0===o.err?"Aborted, offline, or invalid CORS endpoint":o.xhr.responseText):(a=CMI5_DEP_ERROR,u=o.err),CMI5_SetErrorInfoManually(a,s,u),!1)}function CMI5_RetrieveState(e,t){WriteToDebug("In CMI5_RetrieveState - key: "+e);var r,n,i,o,a={agent:cmi5.getActor(),activity:cmi5.getActivity()},s="";return(null==t||t)&&(a.registration=cmi5.getRegistration()),null!==(r=cmi5.getLRS().retrieveState(e,a)).err?(WriteToDebug("Failed to retrieve "+e+" from state: "+r.err),i="Failed to retrieve state: "+e,/^\d+$/.test(r.err)?(n=r.err,o=0===r.err?"Aborted, offline, or invalid CORS endpoint":r.xhr.responseText):(n=CMI5_DEP_ERROR,o=r.err),CMI5_SetErrorInfoManually(n,i,o),""):(null!==r.state&&(s=r.state.contents),s)}function CMI5_GetLaunchUrl(){return WriteToDebug("In CMI5_GetLaunchUrl"),document.location.href}function CMI5_Initialize(){var e;WriteToDebug("In CMI5_Initialize"),Cmi5.prototype.log=function(){var e,t="cmi5.js:";for(e=0;e0?ENTRY_RESUME:ENTRY_FIRST_TIME,CMI5_COMMITTED_STATUS.launchModes.push(cmi5.getLaunchMode()),CMI5_SaveState(CMI5_STATE_COMMITTED_STATUS,CMI5_COMMITTED_STATUS,"application/json",void 0,function(e){if(e)return WriteToDebug("CMI5_Initialize - failed to store committed status"),void InitializeExecuted(!1,"Failed to initialize - failed to store committed status");WriteToDebug("CMI5_Initialize - succeeded"),InitializeExecuted(!0,"")}),window.setTimeout(function(){CMI5_GetPreviouslyAccumulatedTime()},200)},{postFetch:function(e,t,r){var n;null===e&&(n=cmi5.getActivity(),cmi5.getLRS().retrieveActivity(n.id,{callback:function(e,t){null===e&&cmi5.setActivity(t)}}))}}),!0}function CMI5_CommitData(){WriteToDebug("In CMI5_CommitData");var e,t,r,n,i,o=!1,a=new TinCan.Activity({id:"http://id.tincanapi.com/activity/software/scormdriver/"+VERSION,definition:{name:{"en-US":"ScormDriver ("+VERSION+")"},description:{"en-US":"ScormDriver ("+VERSION+")"},type:"http://id.tincanapi.com/activitytype/source"}});if(CMI5_ClearErrorInfo(),null!==CMI5_PENDING_STATUS.completion&&(o=!0,CMI5_COMMITTED_STATUS.completion=CMI5_PENDING_STATUS.completion),null!==CMI5_PENDING_STATUS.success&&(o=!0,CMI5_COMMITTED_STATUS.success=CMI5_PENDING_STATUS.success,null!==CMI5_PENDING_STATUS.score&&(CMI5_COMMITTED_STATUS.score=CMI5_PENDING_STATUS.score)),o&&(result=CMI5_SaveState(CMI5_STATE_COMMITTED_STATUS,CMI5_COMMITTED_STATUS,"application/json"),!result))return WriteToDebug("CMI5_CommitData - failed to commit status"),result;if(CMI5_STATEMENT_QUEUE.length>0){if(null!==CMI5_PENDING_STATUS.score&&null!==CMI5_PENDING_STATUS.success)for(r=cmi5.getActivity().id,e=0;e0&&(C+="[,]"),C+=SafeExtractResponseId(t[l]);for(l=0;l0&&(I+="[,]"),I+=SafeExtractResponseId(n[l]);return CMI5_RecordInteraction(e,C,r,I,i,o,a,s,u,CMI5_INTERACTION_TYPE_CHOICE,null,null,c)}function CMI5_RecordFillInInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In CMI5_RecordFillInInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=CMI5_INTERACTION_TYPE_FILL_IN;return null!==t&&(t=new String(t)),null!==n&&(n=new String(n)).length>250&&(c=CMI5_INTERACTION_TYPE_LONG_FILL_IN),CMI5_RecordInteraction(e,t,r,n,i,o,a,s,u,c)}function CMI5_RecordMatchingInteraction(e,t,r,n,i,o,a,s,u,c){WriteToDebug("In CMI5_RecordMatchingInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u+", choices="+JSON.stringify(c));var l,C=null,I="";if(null!==t)for(C="",l=0;l0&&(C+="[,]"),C+=SafeExtractResponseId(t[l].Source)+"[.]"+SafeExtractResponseId(t[l].Target)}for(l=0;l0&&(I+="[,]"),I+=SafeExtractResponseId(n[l].Source)+"[.]"+SafeExtractResponseId(n[l].Target)}return CMI5_RecordInteraction(e,C,r,I,i,o,a,s,u,CMI5_INTERACTION_TYPE_MATCHING,null,null,c)}function CMI5_RecordPerformanceInteraction(e,t,r,n,i,o,a,s,u){return WriteToDebug("In CMI5_RecordPerformanceInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u),null!==t&&(t=new String(t)),null!==n&&(n=new String(n)),CMI5_RecordInteraction(e,t,r,n,i,o,a,s,u,CMI5_INTERACTION_TYPE_PERFORMANCE)}function CMI5_RecordSequencingInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In CMI5_RecordSequencingInteraction strID="+e+", aryResponse="+t+", blnCorrect="+r+", aryCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c,l=null,C="";if(null!==t)for(l="",c=0;c0&&(l+="[,]"),l+=SafeExtractResponseId(t[c]);for(c=0;c0&&(C+="[,]"),C+=SafeExtractResponseId(n[c]);return CMI5_RecordInteraction(e,l,r,C,i,o,a,s,u,CMI5_INTERACTION_TYPE_SEQUENCING)}function CMI5_RecordLikertInteraction(e,t,r,n,i,o,a,s,u){WriteToDebug("In CMI5_RecordLikertInteraction strID="+e+", response="+t+", blnCorrect="+r+", correctResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u);var c=null,l="";return null!==t&&(c=""!==t.Long?t.Long:t.Short),null!==n&&(l=""!==n.Long?n.Long:n.Short),CMI5_RecordInteraction(e,c,r,l,i,o,a,s,u,CMI5_INTERACTION_TYPE_LIKERT)}function CMI5_RecordNumericInteraction(e,t,r,n,i,o,a,s,u){return WriteToDebug("In CMI5_RecordNumericInteraction strID="+e+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+s+", dtmTime="+u),null==n||IsValidDecimalRange(n)||IsValidDecimal(n)?CMI5_RecordInteraction(e,t,r,n,i,o,a,s,u,CMI5_INTERACTION_TYPE_NUMERIC):(WriteToDebug("Returning False - CMI5_RecordNumericInteraction received invalid correct response (not a decimal or range), strCorrectResponse="+n),!1)}function CMI5_CreateInteraction(e,t,r,n,i,o,a,s,u,c,l,C){CMI5_INTERACTIONS[e]={type:c,timestamp:u.toJSON(),correctResponses:n,learnerResponses:t,weighting:o,result:r,latency:a,description:i}}function CMI5_LoadInteraction(e){var t;return WriteToDebug("CMI5_LoadInteraction - strInteractionId = "+e),void 0!==CMI5_INTERACTIONS[e]?(WriteToDebug(" already locally cached, returning"),!0):(t=CMI5_RetrieveState(e),intCMI5Error?(WriteToDebug(" failed to retrieve state for interaction: "+strCMI5ErrorString),!1):""!==t&&void 0===t.timestamp?(CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_INVALID_JSON,"Invalid JSON for interaction in State","result= "+t),!1):(""===t||(CMI5_INTERACTIONS[e]=t,WriteToDebug(" cached interaction locally")),!0))}function CMI5_SaveInteraction(e){return WriteToDebug("CMI5_SaveInteraction - strInteractionId = "+e),void 0===CMI5_INTERACTIONS[e]?(CMI5_SetErrorInfoManually(CMI5_ERROR_UNRECOGNIZED_INTERACTION,"Cannot save unknown interaction: "+e),!1):CMI5_SaveState(e,CMI5_INTERACTIONS[e],"application/json",void 0,noop)}function CMI5_GetInteractionType(e){return WriteToDebug("CMI5_GetInteractionType - strInteractionID = "+e),CMI5_ClearErrorInfo(),CMI5_LoadInteraction(e)?void 0!==CMI5_INTERACTIONS[e]?CMI5_INTERACTIONS[e].type:"":(WriteToDebug("Failed to load interaction: "+e),CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED,"Cannot get interaction type: "+e,"Failed to load interaction: "+strCMI5ErrorString+" ("+strCMI5ErrorDiagnostic+")"),!1)}function CMI5_GetInteractionTimestamp(e){return WriteToDebug("CMI5_GetInteractionTimestamp - strInteractionID = "+e),CMI5_ClearErrorInfo(),CMI5_LoadInteraction(e)?void 0!==CMI5_INTERACTIONS[e]?new Date(CMI5_INTERACTIONS[e].timestamp):"":(WriteToDebug("Failed to load interaction: "+e),CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED,"Cannot get interaction timestamp: "+e,"Failed to load interaction: "+strCMI5ErrorString+" ("+strCMI5ErrorDiagnostic+")"),!1)}function CMI5_GetInteractionCorrectResponses(e){WriteToDebug("CMI5_GetInteractionCorrectResponses - strInteractionID = "+e);var t=[];return CMI5_ClearErrorInfo(),CMI5_LoadInteraction(e)?(void 0!==CMI5_INTERACTIONS[e]&&(t=CMI5_INTERACTIONS[e].correctResponses.split("[,]"),CMI5_INTERACTIONS[e].type===CMI5_INTERACTION_TYPE_MATCHING&&(t=CMI5_ProcessResponseArray(t))),t):(WriteToDebug("Failed to load interaction: "+e),CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED,"Cannot get interaction correct responses: "+e,"Failed to load interaction: "+strCMI5ErrorString+" ("+strCMI5ErrorDiagnostic+")"),!1)}function CMI5_GetInteractionLearnerResponses(e){WriteToDebug("CMI5_GetInteractionLearnerResponses - strInteractionID = "+e);var t=[];return CMI5_ClearErrorInfo(),CMI5_LoadInteraction(e)?(void 0!==CMI5_INTERACTIONS[e]&&(t=CMI5_INTERACTIONS[e].learnerResponses.split("[,]"),CMI5_INTERACTIONS[e].type===CMI5_INTERACTION_TYPE_MATCHING&&(t=CMI5_ProcessResponseArray(t))),t):(WriteToDebug("Failed to load interaction: "+e),CMI5_SetErrorInfoManually(CMI5_ERROR_INTERACTION_LOAD_FAILED,"Cannot get interaction learner responses: "+e,"Failed to load interaction: "+strCMI5ErrorString+" ("+strCMI5ErrorDiagnostic+")"),!1)}function CMI5_ProcessResponseArray(e){var t;for(t=0;t=e)return CMI5_SetErrorInfoManually(CMI5_ERROR_INVALID_STATUS_BASED_ON_SCORE,"Failed to SetFailed","Pending score conflicts with failure"),!1;if(!0===CMI5_PENDING_STATUS.success)for(t=cmi5.getActivity().id,n=0;nSCORM Driver starting up"),WriteToDebug("----------------------------------------"),WriteToDebug("----------------------------------------"),WriteToDebug("In Start - Version: "+VERSION+" Last Modified="+window.document.lastModified),WriteToDebug("Browser Info ("+navigator.appName+" "+navigator.appVersion+")"),WriteToDebug("URL: "+window.document.location.href),WriteToDebug("----------------------------------------"),WriteToDebug("----------------------------------------"),ClearErrorInfo(),WriteToDebug("strStandAlone="+(e=GetQueryStringValue("StandAlone",window.location.search))+" strShowInteractiveDebug="+(t=GetQueryStringValue("ShowDebug",window.location.search))),ConvertStringToBoolean(e)&&(WriteToDebug("Entering Stand Alone Mode"),blnStandAlone=!0),blnStandAlone)WriteToDebug("Using NONE Standard"),objLMS=new LMSStandardAPI("NONE");else if(WriteToDebug("Standard From Configuration File - "+strLMSStandard),"AUTO"==strLMSStandard.toUpperCase())if(WriteToDebug("Searching for recognized querystring parameters"),n=GetQueryStringValue("AICC_URL",document.location.search),i=GetQueryStringValue("endpoint",document.location.search),o=GetQueryStringValue("fetch",document.location.search),null!=n&&""!=n)WriteToDebug("Found AICC querystring parameters, using AICC"),objLMS=new LMSStandardAPI("AICC"),blnLmsPresent=!0;else if(null!=i&&""!=i)WriteToDebug("Found endpoint querystring parameter - checking cmi5 or Tin Can"),null!=o&&""!=o?(WriteToDebug("Found fetch querystring parameter, using cmi5"),objLMS=new LMSStandardAPI("CMI5"),blnLmsPresent=!0):(WriteToDebug("Did not find fetch querystring parameter, using Tin Can"),objLMS=new LMSStandardAPI("TCAPI"),blnLmsPresent=!0,strLMSStandard="TCAPI");else{WriteToDebug("Auto-detecting standard - Searching for SCORM 2004 API");try{r=SCORM2004_GrabAPI()}catch(e){WriteToDebug("Error grabbing 2004 API-"+e.name+":"+e.message)}if(void 0!==r&&null!=r)WriteToDebug("Found SCORM 2004 API, using SCORM 2004"),objLMS=new LMSStandardAPI("SCORM2004"),blnLmsPresent=!0;else{WriteToDebug("Searching for SCORM 1.2 API");try{r=SCORM_GrabAPI()}catch(e){WriteToDebug("Error grabbing 1.2 API-"+e.name+":"+e.message)}if(void 0!==r&&null!=r)WriteToDebug("Found SCORM API, using SCORM"),objLMS=new LMSStandardAPI("SCORM"),blnLmsPresent=!0;else{if(!0!==ALLOW_NONE_STANDARD)return WriteToDebug("Could not determine standard, Stand Alone is disabled in configuration"),void DisplayError("Could not determine standard. SCORM, AICC, Tin Can, and CMI5 APIs could not be found");WriteToDebug("Could not determine standard, defaulting to Stand Alone"),objLMS=new LMSStandardAPI("NONE")}}}else WriteToDebug("Using Standard From Configuration File - "+strLMSStandard),objLMS=new LMSStandardAPI(strLMSStandard),blnLmsPresent=!0;(ConvertStringToBoolean(t)||void 0!==SHOW_DEBUG_ON_LAUNCH&&!0===SHOW_DEBUG_ON_LAUNCH)&&(WriteToDebug("Showing Interactive Debug Windows"),ShowDebugWindow()),WriteToDebug("Calling Standard Initialize"),"TCAPI"==strLMSStandard.toUpperCase()?loadScript("../tc-config.js",objLMS.Initialize):objLMS.Initialize(),TouchCloud()}function InitializeExecuted(e,t){if(WriteToDebug("In InitializeExecuted, blnSuccess="+e+", strErrorMessage="+t),!e)return WriteToDebug("ERROR - LMS Initialize Failed"),""==t&&(t="An Error Has Occurred"),blnLmsPresent=!1,void DisplayError(t);"AICC"==objLMS.Standard&&AICC_InitializeExecuted(),blnLoaded=!0,dtmStart=new Date,LoadContent()}function ExecFinish(e){return WriteToDebug("In ExecFinish, ExiType="+e),ClearErrorInfo(),!(blnLoaded&&!blnCalledFinish)||(WriteToDebug("Haven't called finish before, finishing"),blnCalledFinish=!0,blnReachedEnd&&!EXIT_SUSPEND_IF_COMPLETED&&(WriteToDebug("Reached End, overiding exit type to FINISH"),e=EXIT_TYPE_FINISH),1==EXIT_NORMAL_IF_PASSED&&objLMS.GetStatus()==LESSON_STATUS_PASSED&&(WriteToDebug("Passed status and config value set, overiding exit type to FINISH"),e=EXIT_TYPE_FINISH),blnOverrodeTime||(WriteToDebug("Did not override time"),dtmEnd=new Date,AccumulateTime(),objLMS.SaveTime(intAccumulatedMS)),blnLoaded=!1,WriteToDebug("Calling LMS Finish"),objLMS.Finish(e,blnStatusWasSet))}function IsLoaded(){return WriteToDebug("In IsLoaded, returning -"+blnLoaded),blnLoaded}function WriteToDebug(e){if(blnDebug){var t,r=new Date;t=aryDebug.length+":"+r.toString()+" - "+e,aryDebug[aryDebug.length]=t,winDebug&&!winDebug.closed&&(winDebug.document.body.appendChild(winDebug.document.createTextNode(t)),winDebug.document.body.appendChild(winDebug.document.createElement("br")))}}function ShowDebugWindow(){var e=function(){var e,t=aryDebug.length;for(winDebug.document.body.innerHTML="",e=0;e100)return WriteToDebug("ERROR - intScore out of range"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Score passed to SetScore (must be between 0-100), intScore="+e),!1;if(t<0||t>100)return WriteToDebug("ERROR - intMaxScore out of range"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Max Score passed to SetScore (must be between 0-100), intMaxScore="+t),!1;if(r<0||r>100)return WriteToDebug("ERROR - intMinScore out of range"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Min Score passed to SetScore (must be between 0-100), intMinScore="+r),!1}if(!0===SCORE_CAN_ONLY_IMPROVE){var n=GetScore();if(null!=n&&""!=n&&n>e)return WriteToDebug("Previous score was greater than new score, configuration only allows scores to improve, returning."),!0}return WriteToDebug("Calling to LMS"),objLMS.SetScore(e,t,r)}function SetPointBasedScore(e,t,r){if(WriteToDebug("In SetPointBasedScore, intScore="+e+", intMaxScore="+t+", intMinScore="+r),ClearErrorInfo(),!IsLoaded())return SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1;if(!IsValidDecimal(e))return WriteToDebug("ERROR - intScore not a valid decimal"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Score passed to SetScore (not a valid decimal), intScore="+e),!1;if(!IsValidDecimal(t))return WriteToDebug("ERROR - intMaxScore not a valid decimal"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Max Score passed to SetScore (not a valid decimal), intMaxScore="+t),!1;if(!IsValidDecimal(r))return WriteToDebug("ERROR - intMinScore not a valid decimal"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Min Score passed to SetScore (not a valid decimal), intMinScore="+r),!1;if(WriteToDebug("Converting SCORES to floats"),e=parseFloat(e),t=parseFloat(t),r=parseFloat(r),"SCORM"==strLMSStandard){if(e<0||e>100)return WriteToDebug("ERROR - intScore out of range"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Score passed to SetScore (must be between 0-100), intScore="+e),!1;if(t<0||t>100)return WriteToDebug("ERROR - intMaxScore out of range"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Max Score passed to SetScore (must be between 0-100), intMaxScore="+t),!1;if(r<0||r>100)return WriteToDebug("ERROR - intMinScore out of range"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Min Score passed to SetScore (must be between 0-100), intMinScore="+r),!1}if(!0===SCORE_CAN_ONLY_IMPROVE){var n=GetScore();if(null!=n&&""!=n&&n>e)return WriteToDebug("Previous score was greater than new score, configuration only allows scores to improve, returning."),!0}return WriteToDebug("Calling to LMS"),objLMS.SetPointBasedScore(e,t,r)}function CreateResponseIdentifier(e,t){return""==e.replace(" ","")?(WriteToDebug("Short Identifier is empty"),SetErrorInfo(ERROR_INVALID_ID,"Invalid short identifier, strShort="+e),!1):1!=e.length?(WriteToDebug("ERROR - Short Identifier not 1 character"),SetErrorInfo(ERROR_INVALID_ID,"Invalid short identifier, strShort="+e),!1):IsAlphaNumeric(t)?new ResponseIdentifier(e=e.toLowerCase(),t=CreateValidIdentifier(t)):(WriteToDebug("ERROR - Short Identifier not alpha numeric"),SetErrorInfo(ERROR_INVALID_ID,"Invalid short identifier, strLong="+t),!1)}function ResponseIdentifier(e,t){this.Short=new String(e),this.Long=new String(t),this.toString=function(){return"[Response Identifier "+this.Short+", "+this.Long+"]"}}function MatchingResponse(e,t){e.constructor==String&&(e=CreateResponseIdentifier(e,e)),t.constructor==String&&(t=CreateResponseIdentifier(t,t)),this.Source=e,this.Target=t,this.toString=function(){return"[Matching Response "+this.Source+", "+this.Target+"]"}}function CreateMatchingResponse(e){var t=new Array,r=new Array;t=(e=new String(e)).split("[,]");for(var n=0;n0)c=t;else{if(!(window.console&&"(Internal Function)"==t.constructor.toString()&&t.length>0))return window.console&&window.console.log("ERROR_INVALID_INTERACTION_RESPONSE :: The response is not in the correct format."),SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The response is not in the correct format"),!1;c=t}if(null!=n&&null!=n&&""!=n)if(n.constructor==String){if(l=new Array,0==(C=CreateResponseIdentifier(n,n)))return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The correct response is not in the correct format"),!1;l[0]=C}else if(n.constructor==ResponseIdentifier)(l=new Array)[0]=n;else if(n.constructor==Array||n.constructor.toString().search("Array")>0)l=n;else{if(!(window.console&&"(Internal Function)"==n.constructor.toString()&&n.length>0))return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The correct response is not in the correct format"),!1;l=n}else l=new Array;var I=new Date;return WriteToDebug("Calling to LMS"),objLMS.RecordMultipleChoiceInteraction(e,c,r,l,i,o,a,s,I,u)}function RecordFillInInteraction(e,t,r,n,i,o,a,s){if(WriteToDebug("In RecordFillInInteraction strID="+(e=CreateValidIdentifier(e))+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+(s=CreateValidIdentifier(s))),void 0!==DO_NOT_REPORT_INTERACTIONS&&!0===DO_NOT_REPORT_INTERACTIONS)return WriteToDebug("Configuration specifies interactions should not be reported, exiting."),!0;if(ClearErrorInfo(),!IsLoaded())return SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1;var u=new Date;return WriteToDebug("Calling to LMS"),objLMS.RecordFillInInteraction(e,t,r,n,i,o,a,s,u)}function RecordMatchingInteraction(e,t,r,n,i,o,a,s,u){if(WriteToDebug("In RecordMatchingInteraction strID="+(e=CreateValidIdentifier(e))+", response="+t+", blnCorrect="+r+", correctResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+(s=CreateValidIdentifier(s))+", choices="+JSON.stringify(u)),void 0!==DO_NOT_REPORT_INTERACTIONS&&!0===DO_NOT_REPORT_INTERACTIONS)return WriteToDebug("Configuration specifies interactions should not be reported, exiting."),!0;if(ClearErrorInfo(),!IsLoaded())return SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1;var c,l;if(null===t){if(!ALLOW_INTERACTION_NULL_LEARNER_RESPONSE)return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The response is not in the correct format (null response not allowed)"),!1;c=null}else if(t.constructor==MatchingResponse)(c=new Array)[0]=t;else if(t.constructor==Array||t.constructor.toString().search("Array")>0)c=t;else{if(!(window.console&&"(Internal Function)"==t.constructor.toString()&&t.length>0))return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The response is not in the correct format"),!1;c=t}if(null!=n&&null!=n)if(n.constructor==MatchingResponse)(l=new Array)[0]=n;else if(n.constructor==Array||n.constructor.toString().search("Array")>0)l=n;else{if(!(window.console&&"(Internal Function)"==n.constructor.toString()&&n.length>0))return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The response is not in the correct format"),!1;l=n}else l=new Array;var C=new Date;return WriteToDebug("Calling to LMS"),objLMS.RecordMatchingInteraction(e,c,r,l,i,o,a,s,C,u)}function RecordPerformanceInteraction(e,t,r,n,i,o,a,s){if(WriteToDebug("In RecordPerformanceInteraction strID="+(e=CreateValidIdentifier(e))+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+(s=CreateValidIdentifier(s))),void 0!==DO_NOT_REPORT_INTERACTIONS&&!0===DO_NOT_REPORT_INTERACTIONS)return WriteToDebug("Configuration specifies interactions should not be reported, exiting."),!0;if(ClearErrorInfo(),!IsLoaded())return SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1;var u=new Date;return WriteToDebug("Calling to LMS"),objLMS.RecordPerformanceInteraction(e,t,r,n,i,o,a,s,u)}function RecordSequencingInteraction(e,t,r,n,i,o,a,s){if(WriteToDebug("In RecordSequencingInteraction strID="+(e=CreateValidIdentifier(e))+", response="+t+", blnCorrect="+r+", correctResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+(s=CreateValidIdentifier(s))),void 0!==DO_NOT_REPORT_INTERACTIONS&&!0===DO_NOT_REPORT_INTERACTIONS)return WriteToDebug("Configuration specifies interactions should not be reported, exiting."),!0;if(ClearErrorInfo(),!IsLoaded())return SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1;var u,c;if(null===t){if(!ALLOW_INTERACTION_NULL_LEARNER_RESPONSE)return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The response is not in the correct format (null response not allowed)"),!1;u=null}else if(t.constructor==String){u=new Array;var l=CreateResponseIdentifier(t,t);if(0==l)return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The response is not in the correct format"),!1;u[0]=l}else if(t.constructor==ResponseIdentifier)(u=new Array)[0]=t;else if(t.constructor==Array||t.constructor.toString().search("Array")>0)u=t;else{if(!(window.console&&"(Internal Function)"==t.constructor.toString()&&t.length>0))return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The response is not in the correct format"),!1;u=t}if(null!=n&&null!=n&&""!=n)if(n.constructor==String){if(c=new Array,0==(l=CreateResponseIdentifier(n,n)))return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The correct response is not in the correct format"),!1;c[0]=l}else if(n.constructor==ResponseIdentifier)(c=new Array)[0]=n;else if(n.constructor==Array||n.constructor.toString().search("Array")>0)c=n;else{if(!(window.console&&"(Internal Function)"==n.constructor.toString()&&n.length>0))return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The correct response is not in the correct format"),!1;c=n}else c=new Array;var C=new Date;return WriteToDebug("Calling to LMS"),objLMS.RecordSequencingInteraction(e,u,r,c,i,o,a,s,C)}function RecordLikertInteraction(e,t,r,n,i,o,a,s){if(WriteToDebug("In RecordLikertInteraction strID="+(e=CreateValidIdentifier(e))+", response="+t+", blnCorrect="+r+", correctResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+(s=CreateValidIdentifier(s))),void 0!==DO_NOT_REPORT_INTERACTIONS&&!0===DO_NOT_REPORT_INTERACTIONS)return WriteToDebug("Configuration specifies interactions should not be reported, exiting."),!0;if(ClearErrorInfo(),!IsLoaded())return SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1;var u,c;if(null===t){if(!ALLOW_INTERACTION_NULL_LEARNER_RESPONSE)return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The response is not in the correct format (null response not allowed)"),!1;u=null}else if(t.constructor==String)u=CreateResponseIdentifier(t,t);else{if(t.constructor!=ResponseIdentifier)return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The response is not in the correct format"),!1;u=t}if(null==n||null==n)c=null;else if(n.constructor==ResponseIdentifier)c=n;else{if(n.constructor!=String)return SetErrorInfo(ERROR_INVALID_INTERACTION_RESPONSE,"The response is not in the correct format"),!1;c=CreateResponseIdentifier(n,n)}var l=new Date;return WriteToDebug("Calling to LMS"),objLMS.RecordLikertInteraction(e,u,r,c,i,o,a,s,l)}function RecordNumericInteraction(e,t,r,n,i,o,a,s){if(WriteToDebug("In RecordNumericInteraction strID="+(e=CreateValidIdentifier(e))+", strResponse="+t+", blnCorrect="+r+", strCorrectResponse="+n+", strDescription="+i+", intWeighting="+o+", intLatency="+a+", strLearningObjectiveID="+(s=CreateValidIdentifier(s))),void 0!==DO_NOT_REPORT_INTERACTIONS&&!0===DO_NOT_REPORT_INTERACTIONS)return WriteToDebug("Configuration specifies interactions should not be reported, exiting."),!0;if(ClearErrorInfo(),!IsLoaded())return SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1;if(null===t&&!ALLOW_INTERACTION_NULL_LEARNER_RESPONSE||null!==t&&!IsValidDecimal(t))return WriteToDebug("ERROR - Invalid Response, not a valid decmial"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Response passed to RecordNumericInteraction (not a valid decimal), strResponse="+t),!1;var u=new Date;return WriteToDebug("Calling to LMS"),objLMS.RecordNumericInteraction(e,t,r,n,i,o,a,s,u)}function GetStatus(){return WriteToDebug("In GetStatus"),ClearErrorInfo(),IsLoaded()?objLMS.GetStatus():(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),LESSON_STATUS_INCOMPLETE)}function ResetStatus(){return WriteToDebug("In ResetStatus"),ClearErrorInfo(),IsLoaded()?(WriteToDebug("Setting blnStatusWasSet to false"),blnStatusWasSet=!1,objLMS.ResetStatus()):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetProgressMeasure(){return WriteToDebug("In GetProgressMeasure"),ClearErrorInfo(),IsLoaded()?objLMS.GetProgressMeasure():(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),LESSON_STATUS_INCOMPLETE)}function SetProgressMeasure(e){return WriteToDebug("In SetProgressMeasure, passing in: "+e),ClearErrorInfo(),IsLoaded()?objLMS.SetProgressMeasure(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),LESSON_STATUS_INCOMPLETE)}function SetPassed(){return WriteToDebug("In SetPassed"),ClearErrorInfo(),IsLoaded()?(WriteToDebug("Setting blnStatusWasSet to true"),blnStatusWasSet=!0,objLMS.SetPassed()):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function SetFailed(){return WriteToDebug("In SetFailed"),ClearErrorInfo(),IsLoaded()?(WriteToDebug("Setting blnStatusWasSet to true"),blnStatusWasSet=!0,objLMS.SetFailed()):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetEntryMode(){return WriteToDebug("In GetEntryMode"),ClearErrorInfo(),IsLoaded()?objLMS.GetEntryMode():(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),ENTRY_FIRST_TIME)}function GetLessonMode(){return WriteToDebug("In GetLessonMode"),ClearErrorInfo(),IsLoaded()?objLMS.GetLessonMode():(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),MODE_NORMAL)}function GetTakingForCredit(){return WriteToDebug("In GetTakingForCredit"),ClearErrorInfo(),IsLoaded()?objLMS.GetTakingForCredit():(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function SetObjectiveScore(e,t,r,n){return WriteToDebug("In SetObjectiveScore, intObjectiveID="+e+", intScore="+t+", intMaxScore="+r+", intMinScore="+n),ClearErrorInfo(),IsLoaded()?""==(e=new String(e)).replace(" ","")?(WriteToDebug("ERROR - Invalid ObjectiveID, empty string"),SetErrorInfo(ERROR_INVALID_ID,"Invalid ObjectiveID passed to SetObjectiveScore (must have a value), strObjectiveID="+e),!1):IsValidDecimal(t)?IsValidDecimal(r)?IsValidDecimal(n)?(WriteToDebug("Converting Scores to floats"),t=parseFloat(t),r=parseFloat(r),n=parseFloat(n),t<0||t>100?(WriteToDebug("ERROR - Invalid Score, out of range"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Score passed to SetObjectiveScore (must be between 0-100), intScore="+t),!1):r<0||r>100?(WriteToDebug("ERROR - Invalid Max Score, out of range"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Max Score passed to SetObjectiveScore (must be between 0-100), intMaxScore="+r),!1):n<0||n>100?(WriteToDebug("ERROR - Invalid Min Score, out of range"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Min Score passed to SetObjectiveScore (must be between 0-100), intMinScore="+n),!1):(WriteToDebug("Calling To LMS"),objLMS.SetObjectiveScore(e,t,r,n))):(WriteToDebug("ERROR - Invalid Min Score, not a valid decmial"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Min Score passed to SetObjectiveScore (not a valid decimal), intMinScore="+n),!1):(WriteToDebug("ERROR - Invalid Max Score, not a valid decmial"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Max Score passed to SetObjectiveScore (not a valid decimal), intMaxScore="+r),!1):(WriteToDebug("ERROR - Invalid Score, not a valid decmial"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Score passed to SetObjectiveScore (not a valid decimal), intScore="+t),!1):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function SetObjectiveStatus(e,t){return WriteToDebug("In SetObjectiveStatus strObjectiveID="+e+", Lesson_Status="+t),ClearErrorInfo(),IsLoaded()?""==(e=new String(e)).replace(" ","")?(WriteToDebug("ERROR - Invalid ObjectiveID, empty string"),SetErrorInfo(ERROR_INVALID_ID,"Invalid ObjectiveID passed to SetObjectiveStatus (must have a value), strObjectiveID="+e),!1):t!=LESSON_STATUS_PASSED&&t!=LESSON_STATUS_COMPLETED&&t!=LESSON_STATUS_FAILED&&t!=LESSON_STATUS_INCOMPLETE&&t!=LESSON_STATUS_BROWSED&&t!=LESSON_STATUS_NOT_ATTEMPTED?(WriteToDebug("ERROR - Invalid Status"),SetErrorInfo(ERROR_INVALID_STATUS,"Invalid status passed to SetObjectiveStatus, Lesson_Status="+t),!1):(WriteToDebug("Calling To LMS"),objLMS.SetObjectiveStatus(e,t)):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetObjectiveStatus(e){return WriteToDebug("In GetObjectiveStatus, strObjectiveID="+e),ClearErrorInfo(),IsLoaded()?objLMS.GetObjectiveStatus(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function SetObjectiveDescription(e,t){return WriteToDebug("In SetObjectiveDescription strObjectiveID="+e+", strObjectiveDescription="+t),ClearErrorInfo(),IsLoaded()?""==(e=new String(e)).replace(" ","")?(WriteToDebug("ERROR - Invalid ObjectiveID, empty string"),SetErrorInfo(ERROR_INVALID_ID,"Invalid ObjectiveID passed to SetObjectiveStatus (must have a value), strObjectiveID="+e),!1):(WriteToDebug("Calling To LMS"),objLMS.SetObjectiveDescription(e,t)):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetObjectiveDescription(e){return WriteToDebug("In GetObjectiveDescription, strObjectiveID="+e),ClearErrorInfo(),IsLoaded()?objLMS.GetObjectiveDescription(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetObjectiveScore(e){return WriteToDebug("In GetObjectiveScore, strObjectiveID="+e),ClearErrorInfo(),IsLoaded()?objLMS.GetObjectiveScore(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function IsLmsPresent(){return blnLmsPresent}function SetObjectiveProgressMeasure(e,t){return WriteToDebug("In SetObjectiveProgressMeasure strObjectiveID="+e+", strObjectiveProgressMeasure="+t),ClearErrorInfo(),IsLoaded()?""==(e=new String(e)).replace(" ","")?(WriteToDebug("ERROR - Invalid ObjectiveID, empty string"),SetErrorInfo(ERROR_INVALID_ID,"Invalid ObjectiveID passed to SetObjectiveProgressMeasure (must have a value), strObjectiveID="+e),!1):(WriteToDebug("Calling To LMS"),objLMS.SetObjectiveProgressMeasure(e,t)):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetObjectiveProgressMeasure(e){return WriteToDebug("In GetObjectiveProgressMeasure, strObjectiveID="+e),ClearErrorInfo(),IsLoaded()?objLMS.GetObjectiveProgressMeasure(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function SetNavigationRequest(e){return WriteToDebug("In SetNavigationRequest"),ClearErrorInfo(),IsLoaded()?objLMS.SetNavigationRequest(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetNavigationRequest(){return WriteToDebug("In GetNavigationRequest"),ClearErrorInfo(),IsLoaded()?objLMS.GetNavigationRequest():(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetInteractionType(e){return WriteToDebug("In GetInteractionType, strInteractionID="+(e=CreateValidIdentifier(e))),ClearErrorInfo(),IsLoaded()?objLMS.GetInteractionType(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetInteractionTimestamp(e){return WriteToDebug("In GetInteractionTimestamp, strInteractionID="+(e=CreateValidIdentifier(e))),ClearErrorInfo(),IsLoaded()?objLMS.GetInteractionTimestamp(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetInteractionCorrectResponses(e){return WriteToDebug("In GetInteractionCorrectResponses, strInteractionID="+(e=CreateValidIdentifier(e))),ClearErrorInfo(),IsLoaded()?objLMS.GetInteractionCorrectResponses(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetInteractionWeighting(e){return WriteToDebug("In GetInteractionWeighting, strInteractionID="+(e=CreateValidIdentifier(e))),ClearErrorInfo(),IsLoaded()?objLMS.GetInteractionWeighting(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetInteractionLearnerResponses(e){return WriteToDebug("In GetInteractionLearnerResponses, strInteractionID="+(e=CreateValidIdentifier(e))),ClearErrorInfo(),IsLoaded()?objLMS.GetInteractionLearnerResponses(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetInteractionResult(e){return WriteToDebug("In GetInteractionResult, strInteractionID="+(e=CreateValidIdentifier(e))),ClearErrorInfo(),IsLoaded()?objLMS.GetInteractionResult(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetInteractionLatency(e){return WriteToDebug("In GetInteractionLatency, strInteractionID="+(e=CreateValidIdentifier(e))),ClearErrorInfo(),IsLoaded()?objLMS.GetInteractionLatency(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetInteractionDescription(e){return WriteToDebug("In GetInteractionDescription, strInteractionID="+(e=CreateValidIdentifier(e))),ClearErrorInfo(),IsLoaded()?objLMS.GetInteractionDescription(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function CreateDataBucket(e,t,r){return WriteToDebug("In CreateDataBucket, strBucketId="+e+", intMinSize="+t+", intMaxSize="+r),ClearErrorInfo(),IsLoaded()?""==(e=new String(e)).replace(" ","")?(WriteToDebug("ERROR - Invalid BucketId, empty string"),SetErrorInfo(ERROR_INVALID_ID,"Invalid strBucketId passed to CreateDataBucket (must have a value), strBucketId="+e),!1):ValidInteger(t)?ValidInteger(r)?(t=parseInt(t,10),r=parseInt(r,10),t<0?(WriteToDebug("ERROR Invalid Min Size, must be greater than or equal to 0"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Min Size passed to CreateDataBucket (must be greater than or equal to 0), intMinSize="+t),!1):r<=0?(WriteToDebug("ERROR Invalid Max Size, must be greater than 0"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid Max Size passed to CreateDataBucket (must be greater than 0), intMaxSize="+r),!1):(t*=2,r*=2,objLMS.CreateDataBucket(e,t,r))):(WriteToDebug("ERROR Invalid Max Size, not an integer"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid intMaxSize passed to CreateDataBucket (not an integer), intMaxSize="+r),!1):(WriteToDebug("ERROR Invalid Min Size, not an integer"),SetErrorInfo(ERROR_INVALID_NUMBER,"Invalid intMinSize passed to CreateDataBucket (not an integer), intMinSize="+t),!1):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function GetDataFromBucket(e){return WriteToDebug("In GetDataFromBucket, strBucketId="+e),ClearErrorInfo(),IsLoaded()?""==(e=new String(e)).replace(" ","")?(WriteToDebug("ERROR - Invalid BucketId, empty string"),SetErrorInfo(ERROR_INVALID_ID,"Invalid strBucketId passed to GetDataFromBucket (must have a value), strBucketId="+e),!1):objLMS.GetDataFromBucket(e):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function PutDataInBucket(e,t,r){return WriteToDebug("In PutDataInBucket, strBucketId="+e+", blnAppendToEnd="+r+", strData="+t),ClearErrorInfo(),IsLoaded()?""==(e=new String(e)).replace(" ","")?(WriteToDebug("ERROR - Invalid BucketId, empty string"),SetErrorInfo(ERROR_INVALID_ID,"Invalid strBucketId passed to PutDataInBucket (must have a value), strBucketId="+e),!1):(1!=r&&(WriteToDebug("blnAppendToEnd was not explicitly true so setting it to false, blnAppendToEnd="+r),r=!1),objLMS.PutDataInBucket(e,t,r)):(SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1)}function DetectSSPSupport(){return objLMS.DetectSSPSupport()}function GetBucketInfo(e){if(WriteToDebug("In GetBucketInfo, strBucketId="+e),ClearErrorInfo(),!IsLoaded())return SetErrorInfo(ERROR_NOT_LOADED,"Cannot make calls to the LMS before calling Start"),!1;if(""==(e=new String(e)).replace(" ",""))return WriteToDebug("ERROR - Invalid BucketId, empty string"),SetErrorInfo(ERROR_INVALID_ID,"Invalid strBucketId passed to GetBucketInfo (must have a value), strBucketId="+e),!1;var t=objLMS.GetBucketInfo(e);return t.TotalSpace=t.TotalSpace/2,t.UsedSpace=t.UsedSpace/2,WriteToDebug("GetBucketInfo returning "+t),t}function SSPBucketSize(e,t){this.TotalSpace=e,this.UsedSpace=t,this.toString=function(){return"[SSPBucketSize "+this.TotalSpace+", "+this.UsedSpace+"]"}}function SafeExtractResponseId(e,t){if(void 0===t&&(t=!1),"string"==typeof e){if(t)if(-1!==(r=e.lastIndexOf("/"))&&0===e.indexOf("http"))return e.substring(r+1);return e}if(null==e)return"";if(t){var r,n=null;if(void 0!==e.Long&&null!==e.Long&&""!==e.Long?n=String(e.Long):void 0!==e.id&&null!==e.id&&""!==e.id?n=String(e.id):void 0!==e.Short&&null!==e.Short&&""!==e.Short&&(n=String(e.Short)),null!==n)return-1!==(r=n.lastIndexOf("/"))&&0===n.indexOf("http")?n.substring(r+1):n}else{if(void 0!==e.Long&&null!==e.Long&&""!==e.Long)return String(e.Long);if(void 0!==e.Short&&null!==e.Short&&""!==e.Short)return String(e.Short);if(void 0!==e.id&&null!==e.id&&""!==e.id)return String(e.id)}try{return String(e)}catch(e){return""}}function TCAPI_GetLanguageTag(){var e="en-US",t=null;WriteToDebug("TCAPI_GetLanguageTag - Starting language detection");try{(t=TCAPI_GetLanguagePreference())&&""!==t&&WriteToDebug("TCAPI_GetLanguageTag - Found TCAPI language preference: "+t)}catch(e){WriteToDebug("TCAPI_GetLanguageTag - TCAPI_GetLanguagePreference failed: "+e)}if(!t||""===t)try{"undefined"!=typeof Runtime&&Runtime.getLocale&&WriteToDebug("TCAPI_GetLanguageTag - Found Runtime.getLocale(): "+(t=Runtime.getLocale()))}catch(e){WriteToDebug("TCAPI_GetLanguageTag - Runtime.getLocale failed: "+e)}if(!t||""===t)try{(t=CMI5_GetLanguagePreference())&&""!==t&&WriteToDebug("TCAPI_GetLanguageTag - Found CMI5 language preference: "+t)}catch(e){WriteToDebug("TCAPI_GetLanguageTag - CMI5_GetLanguagePreference failed: "+e)}if(!t||""===t)try{"undefined"!=typeof window&&window.locale&&WriteToDebug("TCAPI_GetLanguageTag - Found window.locale: "+(t=window.locale))}catch(e){WriteToDebug("TCAPI_GetLanguageTag - window.locale failed: "+e)}return WriteToDebug(t&&"string"==typeof t&&""!==t?"TCAPI_GetLanguageTag - Final normalized language tag: "+(e=TCAPI_NormalizeLanguageTag(t)):"TCAPI_GetLanguageTag - No language preference found, using default: "+e),e}function TCAPI_NormalizeLanguageTag(e){var t={en:"en-US",jp:"ja-JP",ja:"ja-JP",es:"es-ES",fr:"fr-FR",de:"de-DE",it:"it-IT",pt:"pt-BR",ru:"ru-RU",zh:"zh-CN","zh-cn":"zh-CN","zh-tw":"zh-TW",ko:"ko-KR",ar:"ar-SA",hi:"hi-IN",th:"th-TH",vi:"vi-VN",nl:"nl-NL",sv:"sv-SE",da:"da-DK",no:"no-NO",fi:"fi-FI",pl:"pl-PL",cs:"cs-CZ",hu:"hu-HU",tr:"tr-TR",he:"he-IL",el:"el-GR"}[e=e.toLowerCase()];return t?(WriteToDebug("TCAPI_NormalizeLanguageTag - mapped '"+e+"' to '"+t+"'"),t):-1!==e.indexOf("-")?(WriteToDebug("TCAPI_NormalizeLanguageTag - using existing format: "+e),e):(WriteToDebug("TCAPI_NormalizeLanguageTag - created generic dialect: "+(t=e+"-"+e.toUpperCase())),t)}function TCAPI_GetStaticActor(){if(WriteToDebug("In TCAPI_GetStaticActor"),void 0===TCAPI_LRS_ACTOR||""===TCAPI_LRS_ACTOR)return WriteToDebug("TCAPI_GetStaticActor - No static actor configured"),null;try{var e=TinCan.Agent.fromJSON(TCAPI_LRS_ACTOR);return WriteToDebug("TCAPI_GetStaticActor - Using static actor: "+TCAPI_LRS_ACTOR),e}catch(e){return WriteToDebug("TCAPI_GetStaticActor - Failed to parse static actor: "+e),null}}function TCAPI_GetStaticLRSConfig(){if(WriteToDebug("In TCAPI_GetStaticLRSConfig"),void 0===TCAPI_LRS_ENDPOINT||""===TCAPI_LRS_ENDPOINT)return WriteToDebug("TCAPI_GetStaticLRSConfig - No static LRS endpoint configured"),null;var e={endpoint:TCAPI_LRS_ENDPOINT,allowFail:!1};return WriteToDebug("TCAPI_GetStaticLRSConfig - Using static endpoint: "+TCAPI_LRS_ENDPOINT),void 0!==TCAPI_LRS_KEY&&""!==TCAPI_LRS_KEY&&void 0!==TCAPI_LRS_SECRET&&""!==TCAPI_LRS_SECRET&&(e.auth="Basic "+btoa(TCAPI_LRS_KEY+":"+TCAPI_LRS_SECRET),WriteToDebug("TCAPI_GetStaticLRSConfig - Generated auth from key/secret")),e}function TCAPI_SendProgressed(e,t){WriteToDebug("In TCAPI_SendProgressed - courseProgress: "+e),TCAPI_ClearErrorInfo();var r=e;if("number"==typeof e&&e>0&&e<1&&WriteToDebug("TCAPI_SendProgressed - Converted decimal progress to percentage: "+(r=Math.round(100*e))),"number"!=typeof r||r<0||r>100)return WriteToDebug("TCAPI_SendProgressed - Invalid progress value: "+e),!1;var n=TCAPI_GetLanguageTag(),i={id:TCAPI_VERB_PROGRESSED_ID,display:{}};i.display[n]=TCAPI_VERB_PROGRESSED;var o={verb:i,result:{extensions:{}}};return o.result.extensions[TCAPI_PROGRESS_EXTENSION]=r,WriteToDebug("TCAPI_SendProgressed - Sending progressed statement with progress: "+r),tincan.sendStatement(o,function(e,r){if(null!==e[0].err)return WriteToDebug("TCAPI_SendProgressed - err: "+e[0].err.responseText+" ("+e[0].err.status+")"),void("function"==typeof t&&t(e[0].err,null));WriteToDebug("TCAPI_SendProgressed - success: "+r.id),"function"==typeof t&&t(null,r)}),!0}function TCAPI_GetActivityIdForStatements(e){WriteToDebug("In TCAPI_GetActivityIdForStatements");var t=TinCan.Utils.parseURL(e),r=TC_COURSE_ID;return t.params&&"activity_id"in t.params?WriteToDebug("TCAPI_GetActivityIdForStatements - using activity_id from URL: "+(r=t.params.activity_id)):WriteToDebug("TCAPI_GetActivityIdForStatements - using TC_COURSE_ID: "+r),r}function CMI5_UpdateVerbDisplayLanguage(e,t){var r,n={};if(WriteToDebug("CMI5_UpdateVerbDisplayLanguage - originalDisplay: "+JSON.stringify(e)),WriteToDebug("CMI5_UpdateVerbDisplayLanguage - target languageTag: "+t),e._template)r=e._template;else if(e["en-US"])r=e["en-US"];else if(e.und)r=e.und;else for(var i in e)if(e.hasOwnProperty(i)){r=e[i];break}return r?(n[t]=r,WriteToDebug("CMI5_UpdateVerbDisplayLanguage - updated display: "+JSON.stringify(n)),n):(WriteToDebug("CMI5_UpdateVerbDisplayLanguage - no verb text found, returning original"),e)}function CMI5_ApplyInteractionDefinitionLanguage(e,t,r,n){var i="en-US";try{i=TCAPI_GetLanguageTag()}catch(e){WriteToDebug("CMI5_ApplyInteractionDefinitionLanguage - language detection failed, using default")}e.description={},e.description[i]=t,e.name={},e.name[i]=t,n&&n.length>0&&"choice"===r&&(e.choices=n.map(function(e){var t={};return t[i]=e.description,new TinCan.InteractionComponent({id:e.id,description:t})})),n&&"object"==typeof n&&"matching"===r&&(n.source&&n.source.length>0&&(e.source=n.source.map(function(e){var t={};return t[i]=e.description,new TinCan.InteractionComponent({id:e.id,description:t})})),n.target&&n.target.length>0&&(e.target=n.target.map(function(e){var t={};return t[i]=e.description,new TinCan.InteractionComponent({id:e.id,description:t})})))} \ No newline at end of file From 188c2e23d981ebe00ac75c9b418a9852c2d7af5c Mon Sep 17 00:00:00 2001 From: swilla <304159+swilla@users.noreply.github.com> Date: Thu, 28 May 2026 18:49:18 +0000 Subject: [PATCH 16/16] Fix styling --- tests/Feature/CommonCartridgeImportTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Feature/CommonCartridgeImportTest.php b/tests/Feature/CommonCartridgeImportTest.php index 9bf13b6..1b64b14 100644 --- a/tests/Feature/CommonCartridgeImportTest.php +++ b/tests/Feature/CommonCartridgeImportTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Tapp\FilamentLms\Enums\CompletionMode; use Tapp\FilamentLms\Models\Course; use Tapp\FilamentLms\Models\Document; use Tapp\FilamentLms\Services\CommonCartridge\CommonCartridgeImportService; @@ -137,7 +138,7 @@ expect($document->package_path)->not->toBeNull(); expect($document->package_launch_path)->toBe('scormdriver/indexAPI.html'); expect($course->embedded_player)->toBeTrue(); - expect($course->completion_mode)->toBe(\Tapp\FilamentLms\Enums\CompletionMode::Scorm12); + expect($course->completion_mode)->toBe(CompletionMode::Scorm12); }); test('html5 package parser imports Storyline HTML5 without imsmanifest', function () {