scorm-again 3.1.4 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cross-frame-api.js +6 -13
- package/dist/cross-frame-api.js.map +1 -1
- package/dist/cross-frame-api.min.js +1 -1
- package/dist/cross-frame-lms.js +2 -6
- package/dist/cross-frame-lms.js.map +1 -1
- package/dist/esm/scorm-again.js +1128 -198
- package/dist/esm/scorm-again.js.map +1 -1
- package/dist/esm/scorm-again.min.js +1 -1
- package/dist/esm/scorm-again.min.js.map +1 -1
- package/dist/esm/scorm12.js +60 -13
- package/dist/esm/scorm12.js.map +1 -1
- package/dist/esm/scorm12.min.js +1 -1
- package/dist/esm/scorm12.min.js.map +1 -1
- package/dist/esm/scorm2004.js +1128 -198
- package/dist/esm/scorm2004.js.map +1 -1
- package/dist/esm/scorm2004.min.js +1 -1
- package/dist/esm/scorm2004.min.js.map +1 -1
- package/dist/scorm-again.js +2088 -1465
- package/dist/scorm-again.js.map +1 -1
- package/dist/scorm-again.min.js +1 -1
- package/dist/scorm12.js +347 -420
- package/dist/scorm12.js.map +1 -1
- package/dist/scorm12.min.js +1 -1
- package/dist/scorm2004.js +1983 -1301
- package/dist/scorm2004.js.map +1 -1
- package/dist/scorm2004.min.js +1 -1
- package/dist/types/BaseAPI.d.ts +1 -1
- package/dist/types/Scorm2004API.d.ts +11 -0
- package/dist/types/cmi/scorm2004/sequencing/activity.d.ts +47 -5
- package/dist/types/cmi/scorm2004/sequencing/handlers/rte_data_transfer.d.ts +1 -0
- package/dist/types/cmi/scorm2004/sequencing/objectives/global_objective_synchronizer.d.ts +17 -1
- package/dist/types/cmi/scorm2004/sequencing/sequencing_rules.d.ts +6 -1
- package/dist/types/cmi/scorm2004/sequencing/traversal/flow_traversal_service.d.ts +5 -2
- package/dist/types/constants/regex.d.ts +1 -0
- package/dist/types/interfaces/services.d.ts +1 -1
- package/dist/types/services/AsynchronousHttpService.d.ts +1 -0
- package/dist/types/services/ErrorHandlingService.d.ts +4 -4
- package/dist/types/services/SynchronousHttpService.d.ts +1 -0
- package/dist/types/types/api_types.d.ts +16 -0
- package/dist/types/types/sequencing_types.d.ts +7 -0
- package/dist/types/utilities/core.d.ts +6 -1
- package/package.json +4 -4
package/dist/esm/scorm12.js
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
const SECONDS_PER_MINUTE = 60;
|
|
2
2
|
const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
|
|
3
|
+
const CORS_SAFELISTED_CONTENT_TYPES = [
|
|
4
|
+
"text/plain",
|
|
5
|
+
"application/x-www-form-urlencoded",
|
|
6
|
+
"multipart/form-data"
|
|
7
|
+
];
|
|
8
|
+
function isCorsSafelistedContentType(contentType) {
|
|
9
|
+
const essence = ((contentType || "").split(";")[0] ?? "").trim().toLowerCase();
|
|
10
|
+
return CORS_SAFELISTED_CONTENT_TYPES.includes(essence);
|
|
11
|
+
}
|
|
12
|
+
function isCrossOriginUrl(url) {
|
|
13
|
+
if (typeof location === "undefined" || !location || !location.origin) return false;
|
|
14
|
+
try {
|
|
15
|
+
return new URL(url, location.href).origin !== location.origin;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
3
20
|
const getSecondsAsHHMMSS = (totalSeconds) => {
|
|
4
21
|
if (!totalSeconds || totalSeconds <= 0) {
|
|
5
22
|
return "00:00:00";
|
|
@@ -149,12 +166,18 @@ function stringMatches(str, tester) {
|
|
|
149
166
|
}
|
|
150
167
|
return new RegExp(tester).test(str);
|
|
151
168
|
}
|
|
152
|
-
function memoize(fn, keyFn) {
|
|
169
|
+
function memoize(fn, keyFn, options) {
|
|
153
170
|
const cache = /* @__PURE__ */ new Map();
|
|
154
171
|
return ((...args) => {
|
|
155
172
|
const key = keyFn ? keyFn(...args) : JSON.stringify(args);
|
|
173
|
+
if (options?.maxKeyLength !== void 0 && key.length > options.maxKeyLength) {
|
|
174
|
+
return fn(...args);
|
|
175
|
+
}
|
|
156
176
|
return cache.has(key) ? cache.get(key) : (() => {
|
|
157
177
|
const result = fn(...args);
|
|
178
|
+
if (options?.maxEntries !== void 0 && cache.size >= options.maxEntries) {
|
|
179
|
+
cache.delete(cache.keys().next().value);
|
|
180
|
+
}
|
|
158
181
|
cache.set(key, result);
|
|
159
182
|
return result;
|
|
160
183
|
})();
|
|
@@ -524,8 +547,10 @@ const DefaultSettings = {
|
|
|
524
547
|
lmsCommitUrl: false,
|
|
525
548
|
dataCommitFormat: "json",
|
|
526
549
|
commitRequestDataType: "application/json;charset=UTF-8",
|
|
550
|
+
terminationCommitContentType: "text/plain;charset=UTF-8",
|
|
527
551
|
autoProgress: false,
|
|
528
552
|
logLevel: LogLevelEnum.ERROR,
|
|
553
|
+
uninitializedGetLogLevel: LogLevelEnum.WARN,
|
|
529
554
|
selfReportSessionTime: false,
|
|
530
555
|
alwaysSendTotalTime: false,
|
|
531
556
|
renderCommonCommitFields: false,
|
|
@@ -819,6 +844,8 @@ const scorm2004_regex = {
|
|
|
819
844
|
CMILang: "^([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?$|^$",
|
|
820
845
|
/** CMILangString250 - String with optional language tag, max 250 chars (RTE C.1.3) */
|
|
821
846
|
CMILangString250: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).{0,250}$)?$",
|
|
847
|
+
/** CMILangString - Optional language tag, no length cap; RTE C.1.3 SPM is a floor and is deliberately not enforced */
|
|
848
|
+
CMILangString: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).*$)?$",
|
|
822
849
|
/** CMILangcr - Language tag pattern with content */
|
|
823
850
|
CMILangcr: "^(({lang=([a-zA-Z]{1,8}|i|x)?(-[a-zA-Z0-9-]{2,8})?}))(.*?)$",
|
|
824
851
|
/** CMILangString250cr - String with optional language tag (carriage return variant) */
|
|
@@ -1053,7 +1080,9 @@ class AsynchronousHttpService {
|
|
|
1053
1080
|
*/
|
|
1054
1081
|
async performBeacon(url, params) {
|
|
1055
1082
|
const { body, contentType } = this._prepareRequestBody(params);
|
|
1056
|
-
const
|
|
1083
|
+
const beaconContentType = Array.isArray(params) ? contentType : this.settings.terminationCommitContentType;
|
|
1084
|
+
this._warnIfBeaconContentTypeUnsafe(url, beaconContentType);
|
|
1085
|
+
const beaconSuccess = navigator.sendBeacon(url, new Blob([body], { type: beaconContentType }));
|
|
1057
1086
|
return Promise.resolve({
|
|
1058
1087
|
status: beaconSuccess ? 200 : 0,
|
|
1059
1088
|
ok: beaconSuccess,
|
|
@@ -1067,6 +1096,14 @@ class AsynchronousHttpService {
|
|
|
1067
1096
|
})
|
|
1068
1097
|
});
|
|
1069
1098
|
}
|
|
1099
|
+
_warnIfBeaconContentTypeUnsafe(url, contentType) {
|
|
1100
|
+
if (isCrossOriginUrl(url) && !isCorsSafelistedContentType(contentType)) {
|
|
1101
|
+
this.settings.onLogMessage?.(
|
|
1102
|
+
LogLevelEnum.WARN,
|
|
1103
|
+
`sendBeacon to cross-origin URL with non-CORS-safelisted Content-Type "${contentType}" may be silently dropped by the browser (Beacon cannot preflight). Use fetch (useAsynchronousCommits + asyncModeBeaconBehavior:"never") for cross-origin JSON/auth on terminate.`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1070
1107
|
/**
|
|
1071
1108
|
* Transforms the response from the LMS to a ResultObject
|
|
1072
1109
|
* @param {Response} response - The response from the LMS
|
|
@@ -1734,14 +1771,14 @@ class ErrorHandlingService {
|
|
|
1734
1771
|
* @param {string} message - The error message
|
|
1735
1772
|
* @throws {ValidationError} - If throwException is true, throws a ValidationError
|
|
1736
1773
|
*/
|
|
1737
|
-
throwSCORMError(CMIElement, errorNumber, message) {
|
|
1774
|
+
throwSCORMError(CMIElement, errorNumber, message, messageLevel = LogLevelEnum.ERROR) {
|
|
1738
1775
|
this._lastDiagnostic = message || "";
|
|
1739
1776
|
if (!message) {
|
|
1740
1777
|
message = this._getLmsErrorMessageDetails(errorNumber, true);
|
|
1741
1778
|
}
|
|
1742
1779
|
const formattedMessage = `SCORM Error ${errorNumber}: ${message}${CMIElement ? ` [Element: ${CMIElement}]` : ""}`;
|
|
1743
|
-
this._apiLog("throwSCORMError", errorNumber + ": " + message,
|
|
1744
|
-
this._loggingService.
|
|
1780
|
+
this._apiLog("throwSCORMError", errorNumber + ": " + message, messageLevel, CMIElement);
|
|
1781
|
+
this._loggingService.log(messageLevel, formattedMessage);
|
|
1745
1782
|
this._lastErrorCode = String(errorNumber);
|
|
1746
1783
|
}
|
|
1747
1784
|
/**
|
|
@@ -2424,7 +2461,9 @@ const checkValidFormat = memoize(
|
|
|
2424
2461
|
(CMIElement, value, regexPattern, errorCode, _errorClass, allowEmptyString) => {
|
|
2425
2462
|
const valueKey = typeof value === "string" ? value : `[${typeof value}]`;
|
|
2426
2463
|
return `${CMIElement}:${valueKey}:${regexPattern}:${errorCode}:${allowEmptyString || false}`;
|
|
2427
|
-
}
|
|
2464
|
+
},
|
|
2465
|
+
// Normal capped CMI values and regexes fit within 2000 characters; large uncapped values bypass caching.
|
|
2466
|
+
{ maxEntries: 1e3, maxKeyLength: 2e3 }
|
|
2428
2467
|
);
|
|
2429
2468
|
const checkValidRange = memoize(
|
|
2430
2469
|
(CMIElement, value, rangePattern, errorCode, errorClass) => {
|
|
@@ -2447,7 +2486,8 @@ const checkValidRange = memoize(
|
|
|
2447
2486
|
},
|
|
2448
2487
|
// Custom key function that excludes the error class from the cache key
|
|
2449
2488
|
// since it can't be stringified and doesn't affect the validation result
|
|
2450
|
-
(CMIElement, value, rangePattern, errorCode, _errorClass) => `${CMIElement}:${value}:${rangePattern}:${errorCode}
|
|
2489
|
+
(CMIElement, value, rangePattern, errorCode, _errorClass) => `${CMIElement}:${value}:${rangePattern}:${errorCode}`,
|
|
2490
|
+
{ maxEntries: 1e3 }
|
|
2451
2491
|
);
|
|
2452
2492
|
|
|
2453
2493
|
class SerializationService {
|
|
@@ -2737,15 +2777,22 @@ class SynchronousHttpService {
|
|
|
2737
2777
|
const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
|
|
2738
2778
|
const requestPayload = handledPayload ?? params;
|
|
2739
2779
|
const { body } = this._prepareRequestBody(requestPayload);
|
|
2740
|
-
const
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
);
|
|
2780
|
+
const beaconContentType = this.settings.terminationCommitContentType;
|
|
2781
|
+
this._warnIfBeaconContentTypeUnsafe(url, beaconContentType);
|
|
2782
|
+
const beaconSuccess = navigator.sendBeacon(url, new Blob([body], { type: beaconContentType }));
|
|
2744
2783
|
return {
|
|
2745
2784
|
result: beaconSuccess ? "true" : "false",
|
|
2746
2785
|
errorCode: beaconSuccess ? 0 : this.error_codes.GENERAL_COMMIT_FAILURE || 391
|
|
2747
2786
|
};
|
|
2748
2787
|
}
|
|
2788
|
+
_warnIfBeaconContentTypeUnsafe(url, contentType) {
|
|
2789
|
+
if (isCrossOriginUrl(url) && !isCorsSafelistedContentType(contentType)) {
|
|
2790
|
+
this.settings.onLogMessage?.(
|
|
2791
|
+
LogLevelEnum.WARN,
|
|
2792
|
+
`sendBeacon to cross-origin URL with non-CORS-safelisted Content-Type "${contentType}" may be silently dropped by the browser (Beacon cannot preflight). Use fetch (useAsynchronousCommits + asyncModeBeaconBehavior:"never") for cross-origin JSON/auth on terminate.`
|
|
2793
|
+
);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2749
2796
|
/**
|
|
2750
2797
|
* Performs a synchronous XMLHttpRequest
|
|
2751
2798
|
* @param {string} url - The URL to send the request to
|
|
@@ -3772,8 +3819,8 @@ class BaseAPI {
|
|
|
3772
3819
|
* // Throw a "not initialized" error
|
|
3773
3820
|
* this.throwSCORMError(301, "The API must be initialized before calling GetValue");
|
|
3774
3821
|
*/
|
|
3775
|
-
throwSCORMError(CMIElement, errorNumber, message) {
|
|
3776
|
-
this._errorHandlingService.throwSCORMError(CMIElement, errorNumber ?? 0, message);
|
|
3822
|
+
throwSCORMError(CMIElement, errorNumber, message, messageLevel) {
|
|
3823
|
+
this._errorHandlingService.throwSCORMError(CMIElement, errorNumber ?? 0, message, messageLevel);
|
|
3777
3824
|
}
|
|
3778
3825
|
/**
|
|
3779
3826
|
* Clears the last SCORM error code when an operation succeeds.
|