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.
@@ -2,6 +2,23 @@ const SECONDS_PER_SECOND = 1;
2
2
  const SECONDS_PER_MINUTE = 60;
3
3
  const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
4
4
  const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
5
+ const CORS_SAFELISTED_CONTENT_TYPES = [
6
+ "text/plain",
7
+ "application/x-www-form-urlencoded",
8
+ "multipart/form-data"
9
+ ];
10
+ function isCorsSafelistedContentType(contentType) {
11
+ const essence = ((contentType || "").split(";")[0] ?? "").trim().toLowerCase();
12
+ return CORS_SAFELISTED_CONTENT_TYPES.includes(essence);
13
+ }
14
+ function isCrossOriginUrl(url) {
15
+ if (typeof location === "undefined" || !location || !location.origin) return false;
16
+ try {
17
+ return new URL(url, location.href).origin !== location.origin;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
5
22
  const designations = {
6
23
  D: SECONDS_PER_DAY,
7
24
  H: SECONDS_PER_HOUR,
@@ -736,8 +753,10 @@ const DefaultSettings = {
736
753
  lmsCommitUrl: false,
737
754
  dataCommitFormat: "json",
738
755
  commitRequestDataType: "application/json;charset=UTF-8",
756
+ terminationCommitContentType: "text/plain;charset=UTF-8",
739
757
  autoProgress: false,
740
758
  logLevel: LogLevelEnum.ERROR,
759
+ uninitializedGetLogLevel: LogLevelEnum.WARN,
741
760
  selfReportSessionTime: false,
742
761
  alwaysSendTotalTime: false,
743
762
  renderCommonCommitFields: false,
@@ -5098,7 +5117,9 @@ class AsynchronousHttpService {
5098
5117
  */
5099
5118
  async performBeacon(url, params) {
5100
5119
  const { body, contentType } = this._prepareRequestBody(params);
5101
- const beaconSuccess = navigator.sendBeacon(url, new Blob([body], { type: contentType }));
5120
+ const beaconContentType = Array.isArray(params) ? contentType : this.settings.terminationCommitContentType;
5121
+ this._warnIfBeaconContentTypeUnsafe(url, beaconContentType);
5122
+ const beaconSuccess = navigator.sendBeacon(url, new Blob([body], { type: beaconContentType }));
5102
5123
  return Promise.resolve({
5103
5124
  status: beaconSuccess ? 200 : 0,
5104
5125
  ok: beaconSuccess,
@@ -5112,6 +5133,14 @@ class AsynchronousHttpService {
5112
5133
  })
5113
5134
  });
5114
5135
  }
5136
+ _warnIfBeaconContentTypeUnsafe(url, contentType) {
5137
+ if (isCrossOriginUrl(url) && !isCorsSafelistedContentType(contentType)) {
5138
+ this.settings.onLogMessage?.(
5139
+ LogLevelEnum.WARN,
5140
+ `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.`
5141
+ );
5142
+ }
5143
+ }
5115
5144
  /**
5116
5145
  * Transforms the response from the LMS to a ResultObject
5117
5146
  * @param {Response} response - The response from the LMS
@@ -5779,14 +5808,14 @@ class ErrorHandlingService {
5779
5808
  * @param {string} message - The error message
5780
5809
  * @throws {ValidationError} - If throwException is true, throws a ValidationError
5781
5810
  */
5782
- throwSCORMError(CMIElement, errorNumber, message) {
5811
+ throwSCORMError(CMIElement, errorNumber, message, messageLevel = LogLevelEnum.ERROR) {
5783
5812
  this._lastDiagnostic = message || "";
5784
5813
  if (!message) {
5785
5814
  message = this._getLmsErrorMessageDetails(errorNumber, true);
5786
5815
  }
5787
5816
  const formattedMessage = `SCORM Error ${errorNumber}: ${message}${CMIElement ? ` [Element: ${CMIElement}]` : ""}`;
5788
- this._apiLog("throwSCORMError", errorNumber + ": " + message, LogLevelEnum.ERROR, CMIElement);
5789
- this._loggingService.error(formattedMessage);
5817
+ this._apiLog("throwSCORMError", errorNumber + ": " + message, messageLevel, CMIElement);
5818
+ this._loggingService.log(messageLevel, formattedMessage);
5790
5819
  this._lastErrorCode = String(errorNumber);
5791
5820
  }
5792
5821
  /**
@@ -15931,15 +15960,22 @@ class SynchronousHttpService {
15931
15960
  const handledPayload = metadata === void 0 ? this.settings.requestHandler(params) : this.settings.requestHandler(params, metadata);
15932
15961
  const requestPayload = handledPayload ?? params;
15933
15962
  const { body } = this._prepareRequestBody(requestPayload);
15934
- const beaconSuccess = navigator.sendBeacon(
15935
- url,
15936
- new Blob([body], { type: "text/plain;charset=UTF-8" })
15937
- );
15963
+ const beaconContentType = this.settings.terminationCommitContentType;
15964
+ this._warnIfBeaconContentTypeUnsafe(url, beaconContentType);
15965
+ const beaconSuccess = navigator.sendBeacon(url, new Blob([body], { type: beaconContentType }));
15938
15966
  return {
15939
15967
  result: beaconSuccess ? "true" : "false",
15940
15968
  errorCode: beaconSuccess ? 0 : this.error_codes.GENERAL_COMMIT_FAILURE || 391
15941
15969
  };
15942
15970
  }
15971
+ _warnIfBeaconContentTypeUnsafe(url, contentType) {
15972
+ if (isCrossOriginUrl(url) && !isCorsSafelistedContentType(contentType)) {
15973
+ this.settings.onLogMessage?.(
15974
+ LogLevelEnum.WARN,
15975
+ `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.`
15976
+ );
15977
+ }
15978
+ }
15943
15979
  /**
15944
15980
  * Performs a synchronous XMLHttpRequest
15945
15981
  * @param {string} url - The URL to send the request to
@@ -16966,8 +17002,8 @@ class BaseAPI {
16966
17002
  * // Throw a "not initialized" error
16967
17003
  * this.throwSCORMError(301, "The API must be initialized before calling GetValue");
16968
17004
  */
16969
- throwSCORMError(CMIElement, errorNumber, message) {
16970
- this._errorHandlingService.throwSCORMError(CMIElement, errorNumber ?? 0, message);
17005
+ throwSCORMError(CMIElement, errorNumber, message, messageLevel) {
17006
+ this._errorHandlingService.throwSCORMError(CMIElement, errorNumber ?? 0, message, messageLevel);
16971
17007
  }
16972
17008
  /**
16973
17009
  * Clears the last SCORM error code when an operation succeeds.
@@ -23998,7 +24034,8 @@ class Scorm2004API extends BaseAPI {
23998
24034
  this.throwSCORMError(
23999
24035
  CMIElement,
24000
24036
  this._error_codes.VALUE_NOT_INITIALIZED ?? 403,
24001
- `The data model element passed to GetValue (${CMIElement}) has not been initialized.`
24037
+ `The data model element passed to GetValue (${CMIElement}) has not been initialized.`,
24038
+ this.settings.uninitializedGetLogLevel
24002
24039
  );
24003
24040
  }
24004
24041
  /**