scorm-again 3.1.5 → 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.
@@ -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";
@@ -530,8 +547,10 @@ const DefaultSettings = {
530
547
  lmsCommitUrl: false,
531
548
  dataCommitFormat: "json",
532
549
  commitRequestDataType: "application/json;charset=UTF-8",
550
+ terminationCommitContentType: "text/plain;charset=UTF-8",
533
551
  autoProgress: false,
534
552
  logLevel: LogLevelEnum.ERROR,
553
+ uninitializedGetLogLevel: LogLevelEnum.WARN,
535
554
  selfReportSessionTime: false,
536
555
  alwaysSendTotalTime: false,
537
556
  renderCommonCommitFields: false,
@@ -1061,7 +1080,9 @@ class AsynchronousHttpService {
1061
1080
  */
1062
1081
  async performBeacon(url, params) {
1063
1082
  const { body, contentType } = this._prepareRequestBody(params);
1064
- const beaconSuccess = navigator.sendBeacon(url, new Blob([body], { type: contentType }));
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 }));
1065
1086
  return Promise.resolve({
1066
1087
  status: beaconSuccess ? 200 : 0,
1067
1088
  ok: beaconSuccess,
@@ -1075,6 +1096,14 @@ class AsynchronousHttpService {
1075
1096
  })
1076
1097
  });
1077
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
+ }
1078
1107
  /**
1079
1108
  * Transforms the response from the LMS to a ResultObject
1080
1109
  * @param {Response} response - The response from the LMS
@@ -1742,14 +1771,14 @@ class ErrorHandlingService {
1742
1771
  * @param {string} message - The error message
1743
1772
  * @throws {ValidationError} - If throwException is true, throws a ValidationError
1744
1773
  */
1745
- throwSCORMError(CMIElement, errorNumber, message) {
1774
+ throwSCORMError(CMIElement, errorNumber, message, messageLevel = LogLevelEnum.ERROR) {
1746
1775
  this._lastDiagnostic = message || "";
1747
1776
  if (!message) {
1748
1777
  message = this._getLmsErrorMessageDetails(errorNumber, true);
1749
1778
  }
1750
1779
  const formattedMessage = `SCORM Error ${errorNumber}: ${message}${CMIElement ? ` [Element: ${CMIElement}]` : ""}`;
1751
- this._apiLog("throwSCORMError", errorNumber + ": " + message, LogLevelEnum.ERROR, CMIElement);
1752
- this._loggingService.error(formattedMessage);
1780
+ this._apiLog("throwSCORMError", errorNumber + ": " + message, messageLevel, CMIElement);
1781
+ this._loggingService.log(messageLevel, formattedMessage);
1753
1782
  this._lastErrorCode = String(errorNumber);
1754
1783
  }
1755
1784
  /**
@@ -2748,15 +2777,22 @@ class SynchronousHttpService {
2748
2777
  const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
2749
2778
  const requestPayload = handledPayload ?? params;
2750
2779
  const { body } = this._prepareRequestBody(requestPayload);
2751
- const beaconSuccess = navigator.sendBeacon(
2752
- url,
2753
- new Blob([body], { type: "text/plain;charset=UTF-8" })
2754
- );
2780
+ const beaconContentType = this.settings.terminationCommitContentType;
2781
+ this._warnIfBeaconContentTypeUnsafe(url, beaconContentType);
2782
+ const beaconSuccess = navigator.sendBeacon(url, new Blob([body], { type: beaconContentType }));
2755
2783
  return {
2756
2784
  result: beaconSuccess ? "true" : "false",
2757
2785
  errorCode: beaconSuccess ? 0 : this.error_codes.GENERAL_COMMIT_FAILURE || 391
2758
2786
  };
2759
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
+ }
2760
2796
  /**
2761
2797
  * Performs a synchronous XMLHttpRequest
2762
2798
  * @param {string} url - The URL to send the request to
@@ -3783,8 +3819,8 @@ class BaseAPI {
3783
3819
  * // Throw a "not initialized" error
3784
3820
  * this.throwSCORMError(301, "The API must be initialized before calling GetValue");
3785
3821
  */
3786
- throwSCORMError(CMIElement, errorNumber, message) {
3787
- this._errorHandlingService.throwSCORMError(CMIElement, errorNumber ?? 0, message);
3822
+ throwSCORMError(CMIElement, errorNumber, message, messageLevel) {
3823
+ this._errorHandlingService.throwSCORMError(CMIElement, errorNumber ?? 0, message, messageLevel);
3788
3824
  }
3789
3825
  /**
3790
3826
  * Clears the last SCORM error code when an operation succeeds.