fcad-core-dragon 2.1.1 → 2.1.2

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.
Files changed (160) hide show
  1. package/.editorconfig +7 -7
  2. package/.gitlab-ci.yml +124 -0
  3. package/.prettierrc +11 -11
  4. package/.vscode/extensions.json +8 -8
  5. package/.vscode/settings.json +46 -16
  6. package/CHANGELOG +520 -520
  7. package/README.md +57 -57
  8. package/documentation/.vitepress/config.js +114 -114
  9. package/documentation/api-examples.md +49 -49
  10. package/documentation/composants/app-base-button.md +58 -58
  11. package/documentation/composants/app-base-error-display.md +59 -59
  12. package/documentation/composants/app-base-popover.md +68 -68
  13. package/documentation/composants/app-comp-audio.md +75 -75
  14. package/documentation/composants/app-comp-branch-buttons.md +111 -111
  15. package/documentation/composants/app-comp-button-progress.md +53 -53
  16. package/documentation/composants/app-comp-carousel.md +53 -53
  17. package/documentation/composants/app-comp-container.md +53 -53
  18. package/documentation/composants/app-comp-input-checkbox-next.md +42 -42
  19. package/documentation/composants/app-comp-input-dropdown-next.md +34 -34
  20. package/documentation/composants/app-comp-input-radio-next.md +39 -39
  21. package/documentation/composants/app-comp-input-text-next.md +35 -35
  22. package/documentation/composants/app-comp-input-text-table-next.md +34 -34
  23. package/documentation/composants/app-comp-input-text-to-fill-dropdown-next.md +53 -53
  24. package/documentation/composants/app-comp-input-text-to-fill-next.md +31 -31
  25. package/documentation/composants/app-comp-jauge.md +31 -31
  26. package/documentation/composants/app-comp-menu-item.md +55 -55
  27. package/documentation/composants/app-comp-menu.md +29 -29
  28. package/documentation/composants/app-comp-navigation.md +41 -41
  29. package/documentation/composants/app-comp-note-call.md +53 -53
  30. package/documentation/composants/app-comp-note-credit.md +53 -53
  31. package/documentation/composants/app-comp-play-bar-next.md +53 -53
  32. package/documentation/composants/app-comp-pop-up-next.md +93 -93
  33. package/documentation/composants/app-comp-quiz-next.md +235 -235
  34. package/documentation/composants/app-comp-quiz-recall.md +53 -53
  35. package/documentation/composants/app-comp-svg-next.md +53 -53
  36. package/documentation/composants/app-comp-table-of-content.md +50 -50
  37. package/documentation/composants/app-comp-video-player.md +82 -82
  38. package/documentation/composants.md +46 -46
  39. package/documentation/composants_critiques/ModelPageComposant.md +53 -53
  40. package/documentation/composants_critiques/app-base-module.md +43 -43
  41. package/documentation/composants_critiques/app-base-page.md +48 -48
  42. package/documentation/composants_critiques/app-base.md +311 -311
  43. package/documentation/composants_critiques/main.md +15 -15
  44. package/documentation/demarrage.md +50 -50
  45. package/documentation/deploiement.md +57 -57
  46. package/documentation/index.md +33 -33
  47. package/documentation/markdown-examples.md +85 -85
  48. package/documentation/public/vite.svg +14 -14
  49. package/documentation/public/vuejs.svg +1 -1
  50. package/documentation/public/vuetify.svg +5 -5
  51. package/eslint.config.js +60 -60
  52. package/junit-report.xml +182 -0
  53. package/package.json +66 -59
  54. package/playwright/index.html +12 -0
  55. package/playwright/index.js +21 -0
  56. package/playwright-ct.config.js +95 -0
  57. package/src/$locales/en.json +157 -157
  58. package/src/$locales/fr.json +120 -120
  59. package/src/assets/data/onboardingMessages.json +47 -47
  60. package/src/components/AppBase.vue +1171 -1169
  61. package/src/components/AppBaseButton.vue +90 -95
  62. package/src/components/AppBaseErrorDisplay.vue +438 -438
  63. package/src/components/AppBaseFlipCard.vue +84 -84
  64. package/src/components/AppBaseModule.vue +1639 -1634
  65. package/src/components/AppBasePage.vue +867 -866
  66. package/src/components/AppBasePopover.vue +41 -41
  67. package/src/components/AppBaseSkeleton.vue +66 -66
  68. package/src/components/AppCompAudio.vue +261 -256
  69. package/src/components/AppCompBranchButtons.vue +508 -508
  70. package/src/components/AppCompButtonProgress.vue +137 -132
  71. package/src/components/AppCompCarousel.vue +342 -336
  72. package/src/components/AppCompContainer.vue +29 -29
  73. package/src/components/AppCompInputCheckBoxNx.vue +325 -323
  74. package/src/components/AppCompInputDropdownNx.vue +302 -299
  75. package/src/components/AppCompInputRadioNx.vue +287 -284
  76. package/src/components/AppCompInputTextNx.vue +156 -153
  77. package/src/components/AppCompInputTextTableNx.vue +205 -202
  78. package/src/components/AppCompInputTextToFillDropdownNx.vue +343 -340
  79. package/src/components/AppCompInputTextToFillNx.vue +316 -313
  80. package/src/components/AppCompJauge.vue +81 -81
  81. package/src/components/AppCompMenu.vue +6 -1
  82. package/src/components/AppCompMenuItem.vue +246 -240
  83. package/src/components/AppCompNavigation.vue +977 -972
  84. package/src/components/AppCompNoteCall.vue +167 -161
  85. package/src/components/AppCompNoteCredit.vue +496 -491
  86. package/src/components/AppCompPlayBarNext.vue +2290 -2288
  87. package/src/components/AppCompPopUpNext.vue +508 -504
  88. package/src/components/AppCompQuizNext.vue +515 -510
  89. package/src/components/AppCompQuizRecall.vue +355 -350
  90. package/src/components/AppCompSVGNext.vue +346 -346
  91. package/src/components/AppCompSettingsMenu.vue +177 -172
  92. package/src/components/AppCompTableOfContent.vue +433 -427
  93. package/src/components/AppCompVideoPlayer.vue +377 -377
  94. package/src/components/AppCompViewDisplay.vue +6 -6
  95. package/src/components/BaseModule.vue +55 -55
  96. package/src/composables/useIdleDetector.js +56 -56
  97. package/src/composables/useQuiz.js +89 -89
  98. package/src/composables/useTimer.js +172 -172
  99. package/src/directives/nvdaFix.js +53 -53
  100. package/src/externalComps/ModuleView.vue +22 -22
  101. package/src/externalComps/SummaryView.vue +91 -91
  102. package/src/main.js +493 -476
  103. package/src/module/stores/appStore.js +960 -947
  104. package/src/module/xapi/ADL.js +520 -520
  105. package/src/module/xapi/Crypto/Hasher.js +241 -241
  106. package/src/module/xapi/Crypto/WordArray.js +278 -278
  107. package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +103 -103
  108. package/src/module/xapi/Crypto/algorithms/C_algo.js +315 -315
  109. package/src/module/xapi/Crypto/algorithms/HMAC.js +9 -9
  110. package/src/module/xapi/Crypto/algorithms/SHA1.js +9 -9
  111. package/src/module/xapi/Crypto/encoders/Base.js +105 -105
  112. package/src/module/xapi/Crypto/encoders/Base64.js +99 -99
  113. package/src/module/xapi/Crypto/encoders/Hex.js +61 -61
  114. package/src/module/xapi/Crypto/encoders/Latin1.js +61 -61
  115. package/src/module/xapi/Crypto/encoders/Utf8.js +45 -45
  116. package/src/module/xapi/Crypto/index.js +53 -53
  117. package/src/module/xapi/Statement/activity.js +47 -47
  118. package/src/module/xapi/Statement/agent.js +55 -55
  119. package/src/module/xapi/Statement/group.js +26 -26
  120. package/src/module/xapi/Statement/index.js +259 -259
  121. package/src/module/xapi/Statement/statement.js +253 -253
  122. package/src/module/xapi/Statement/statementRef.js +23 -23
  123. package/src/module/xapi/Statement/substatement.js +22 -22
  124. package/src/module/xapi/Statement/verb.js +36 -36
  125. package/src/module/xapi/activitytypes.js +17 -17
  126. package/src/module/xapi/launch.js +157 -157
  127. package/src/module/xapi/utils.js +167 -167
  128. package/src/module/xapi/verbs.js +294 -294
  129. package/src/module/xapi/wrapper.js +1895 -1895
  130. package/src/module/xapi/xapiStatement.js +444 -444
  131. package/src/plugins/analytics.js +34 -34
  132. package/src/plugins/bus.js +12 -8
  133. package/src/plugins/gsap.js +17 -17
  134. package/src/plugins/helper.js +355 -358
  135. package/src/plugins/i18n.js +27 -26
  136. package/src/plugins/idb.js +227 -227
  137. package/src/plugins/save.js +37 -37
  138. package/src/plugins/scorm.js +287 -287
  139. package/src/plugins/xapi.js +11 -11
  140. package/src/public/index.html +33 -33
  141. package/src/router/index.js +57 -57
  142. package/src/router/routes.js +312 -312
  143. package/src/shared/generalfuncs.js +344 -344
  144. package/src/shared/validators.js +1018 -1018
  145. package/tests/component/AppBaseButton.spec.js +53 -0
  146. package/tests/component/pinia.spec.js +24 -0
  147. package/{src/components/tests__ → tests/unit}/AppBaseButton.spec.js +53 -53
  148. package/tests/unit/AppCompInputCheckBoxNx.spec.js +59 -0
  149. package/tests/unit/AppCompInputDropdownNx.spec.js +51 -0
  150. package/tests/unit/AppCompInputRadioNx.spec.js +59 -0
  151. package/tests/unit/AppCompInputTextNx.spec.js +44 -0
  152. package/tests/unit/AppCompInputTextTableNx.spec.js +77 -0
  153. package/tests/unit/AppCompInputTextToFillDropdownNx.spec.js +60 -0
  154. package/tests/unit/AppCompInputTextToFillNx.spec.js +45 -0
  155. package/tests/unit/AppCompQuizNext.spec.js +114 -0
  156. package/tests/unit/AppCompVideoPlayer.spec.js +177 -0
  157. package/{src/components/tests__ → tests/unit}/useTimer.spec.js +91 -91
  158. package/vitest.config.js +28 -19
  159. package/vitest.setup.js +28 -0
  160. package/src/components/AppBaseButton.test.js +0 -21
@@ -1,1895 +1,1895 @@
1
- import { CryptoJS } from './Crypto'
2
-
3
- /*
4
- * Wrapper Ref: https://github.com/adlnet/xAPIWrapper
5
- */
6
- export function xapiwrapper(ADL) {
7
- //==============================================================================
8
- // adds toISOString to date objects if not there
9
- // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
10
-
11
- if (!Date.prototype.toISOString) {
12
- ;(function () {
13
- function pad(number) {
14
- let r = String(number)
15
- if (r.length === 1) {
16
- r = '0' + r
17
- }
18
- return r
19
- }
20
-
21
- Date.prototype.toISOString = function () {
22
- return (
23
- this.getUTCFullYear() +
24
- '-' +
25
- pad(this.getUTCMonth() + 1) +
26
- '-' +
27
- pad(this.getUTCDate()) +
28
- 'T' +
29
- pad(this.getUTCHours()) +
30
- ':' +
31
- pad(this.getUTCMinutes()) +
32
- ':' +
33
- pad(this.getUTCSeconds()) +
34
- '.' +
35
- String((this.getUTCMilliseconds() / 1000).toFixed(3)).slice(2, 5) +
36
- 'Z'
37
- )
38
- }
39
- })()
40
- }
41
- // shim for old-style Base64 lib
42
- function toBase64(text) {
43
- if (CryptoJS && CryptoJS.enc.Base64)
44
- return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(text))
45
- // else return Base64.encode(text)
46
- }
47
-
48
- // shim for old-style crypto lib
49
- function toSHA1(text) {
50
- if (CryptoJS && CryptoJS.SHA1) return CryptoJS.SHA1(text).toString()
51
- else return Crypto.util.bytesToHex(Crypto.SHA1(text, { asBytes: true }))
52
- }
53
-
54
- function toSHA256(content) {
55
- if (Object.prototype.toString.call(content) !== '[object ArrayBuffer]') {
56
- return CryptoJS.SHA256(content).toString(CryptoJS.enc.Hex)
57
- }
58
-
59
- // Create a WordArray from the ArrayBuffer.
60
- let i8a = new Uint8Array(content)
61
- let a = []
62
- for (let i = 0; i < i8a.length; i += 4) {
63
- a.push(
64
- (i8a[i] << 24) | (i8a[i + 1] << 16) | (i8a[i + 2] << 8) | i8a[i + 3]
65
- )
66
- }
67
-
68
- return CryptoJS.SHA256(
69
- CryptoJS.lib.WordArray.create(a, i8a.length)
70
- ).toString(CryptoJS.enc.Hex)
71
- }
72
-
73
- // check if string or object is date, if it is, return date object
74
- // feburary 31st == march 3rd in this solution
75
- function isDate(date) {
76
- // check if object is being passed
77
- let d
78
- if (Object.prototype.toString.call(date) === '[object Date]') d = date
79
- else d = new Date(date)
80
- // deep check on date object
81
- if (Object.prototype.toString.call(d) === '[object Date]') {
82
- // it is a date
83
- if (isNaN(d.valueOf())) {
84
- ADL.XAPIWrapper.log('Invalid date String passed')
85
- return null
86
- } else {
87
- return d
88
- }
89
- } else {
90
- // not a date
91
- ADL.XAPIWrapper.log('Invalid date object')
92
- return null
93
- }
94
- }
95
- //////////////////////////////////////////////////////////////////////
96
- log.debug = false
97
-
98
- function getByteLen(normal_val) {
99
- // Force string type
100
- normal_val = String(normal_val)
101
-
102
- let byteLen = 0
103
- for (let i = 0; i < normal_val.length; i++) {
104
- let c = normal_val.charCodeAt(i)
105
- byteLen +=
106
- c < 1 << 7
107
- ? 1
108
- : c < 1 << 11
109
- ? 2
110
- : c < 1 << 16
111
- ? 3
112
- : c < 1 << 21
113
- ? 4
114
- : c < 1 << 26
115
- ? 5
116
- : c < 1 << 31
117
- ? 6
118
- : Number.NaN
119
- }
120
- return byteLen
121
- }
122
- getByteLen()
123
- /*
124
- * Config object used w/ url params to configure the lrs object
125
- * change these to match your lrs
126
- * @return {object} config object
127
- * @example
128
- * let conf = {
129
- * "endpoint" : "https://lrs.adlnet.gov/xapi/",
130
- * "auth" : "Basic " + toBase64('tom:1234'),
131
- * };
132
- * ADL.XAPIWrapper.changeConfig(conf);
133
- */
134
- let Config = (function () {
135
- let conf = {}
136
- conf['endpoint'] = 'http://localhost:8000/xapi/'
137
- //try
138
- //{
139
- conf['auth'] = 'Basic ' + toBase64('tom:1234')
140
- //}
141
- //catch (e)
142
- //{
143
- // log("Exception in Config trying to encode auth: " + e);
144
- //}
145
-
146
- // Statement defaults
147
- // conf["actor"] = {"mbox":"default@example.com"};
148
- // conf["registration"] = ruuid();
149
- // conf["grouping"] = {"id":"ctxact:default/grouping"};
150
- // conf["activity_platform"] = "default platform";
151
-
152
- // Behavior defaults
153
- // conf["strictCallbacks"] = false; // Strict error-first callbacks
154
- return conf
155
- })()
156
- /*
157
- * XAPIWrapper Constructor
158
- * @param {object} config with a minimum of an endoint property
159
- * @param {boolean} verifyxapiversion indicating whether to verify the version of the LRS is compatible with this wrapper
160
- */
161
- let XAPIWrapper = function (config, verifyxapiversion) {
162
- this.lrs = getLRSObject(config || {})
163
- if (this.lrs.user && this.lrs.password)
164
- updateAuth(this.lrs, this.lrs.user, this.lrs.password)
165
- this.base = getbase(this.lrs.endpoint)
166
-
167
- this.withCredentials = false
168
- if (config && typeof config.withCredentials != 'undefined') {
169
- this.withCredentials = config.withCredentials
170
- }
171
-
172
- // Ensure that callbacks are always executed, first param is error (null if no error) followed
173
- // by the result(s)
174
- this.strictCallbacks = false
175
- this.strictCallbacks = config && config.strictCallbacks
176
-
177
- function getbase(url) {
178
- let l = document.createElement('a')
179
- l.href = url
180
- if (l.protocol && l.host) {
181
- return l.protocol + '//' + l.host
182
- } else if (l.href) {
183
- // IE 11 fix.
184
- let parts = l.href.split('//')
185
- return parts[0] + '//' + parts[1].substring(0, parts[1].indexOf('/'))
186
- } else
187
- ADL.XAPIWrapper.log("Couldn't create base url from endpoint: " + url)
188
- }
189
-
190
- function updateAuth(obj, username, password) {
191
- obj.auth = 'Basic ' + toBase64(username + ':' + password)
192
- }
193
-
194
- if (verifyxapiversion && testConfig.call(this)) {
195
- window.ADL.XHR_request(
196
- this.lrs,
197
- this.lrs.endpoint + 'about',
198
- 'GET',
199
- null,
200
- null,
201
- function (r) {
202
- if (r.status == 200) {
203
- try {
204
- let lrsabout = r.json()
205
- let versionOK = false
206
- for (let idx in lrsabout.version) {
207
- if (lrsabout.version.hasOwnProperty(idx))
208
- if (lrsabout.version[idx] == ADL.XAPIWrapper.xapiVersion) {
209
- versionOK = true
210
- break
211
- }
212
- }
213
- if (!versionOK) {
214
- ADL.XAPIWrapper.log(
215
- 'The lrs version [' +
216
- lrsabout.version +
217
- ']' +
218
- " does not match this wrapper's XAPI version [" +
219
- ADL.XAPIWrapper.xapiVersion +
220
- ']'
221
- )
222
- }
223
- } catch (e) {
224
- ADL.XAPIWrapper.log('The response was not an about object')
225
- }
226
- } else {
227
- ADL.XAPIWrapper.log(
228
- 'The request to get information about the LRS failed: ' + r
229
- )
230
- }
231
- },
232
- null,
233
- false,
234
- null,
235
- this.withCredentials,
236
- false
237
- )
238
- }
239
-
240
- this.searchParams = function () {
241
- let sp = { format: 'exact' }
242
- return sp
243
- }
244
-
245
- this.hash = function (tohash) {
246
- if (!tohash) return null
247
- try {
248
- return toSHA1(tohash)
249
- } catch (e) {
250
- ADL.XAPIWrapper.log('Error trying to hash -- ' + e)
251
- return null
252
- }
253
- }
254
-
255
- this.changeConfig = function (config) {
256
- try {
257
- ADL.XAPIWrapper.log('updating lrs object with new configuration')
258
- this.lrs = mergeRecursive(this.lrs, config)
259
- if (config.user && config.password)
260
- this.updateAuth(this.lrs, config.user, config.password)
261
- this.base = getbase(this.lrs.endpoint)
262
- this.withCredentials = config.withCredentials
263
- this.strictCallbacks = config.strictCallbacks
264
- } catch (e) {
265
- ADL.XAPIWrapper.log('error while changing configuration -- ' + e)
266
- }
267
- }
268
-
269
- this.updateAuth = updateAuth
270
- }
271
-
272
- // This wrapper is based on the Experience API Spec version:
273
- XAPIWrapper.prototype.xapiVersion = '1.0.1'
274
-
275
- /*
276
- * Adds info from the lrs object to the statement, if available.
277
- * These values could be initialized from the Config object or from the url query string.
278
- * @param {object} stmt the statement object
279
- */
280
- XAPIWrapper.prototype.prepareStatement = function (stmt) {
281
- if (stmt.actor === undefined) {
282
- stmt.actor = JSON.parse(this.lrs.actor)
283
- } else if (typeof stmt.actor === 'string') {
284
- stmt.actor = JSON.parse(stmt.actor)
285
- }
286
-
287
- if (
288
- this.lrs.grouping ||
289
- this.lrs.registration ||
290
- this.lrs.activity_platform
291
- ) {
292
- if (!stmt.context) {
293
- stmt.context = {}
294
- }
295
- }
296
-
297
- if (this.lrs.grouping) {
298
- if (!stmt.context.contextActivities) {
299
- stmt.context.contextActivities = {}
300
- }
301
-
302
- // PR from brian-learningpool to resolve context overwriting
303
- if (!Array.isArray(stmt.context.contextActivities.grouping)) {
304
- stmt.context.contextActivities.grouping = [{ id: this.lrs.grouping }]
305
- } else {
306
- stmt.context.contextActivities.grouping.splice(0, 0, {
307
- id: this.lrs.grouping
308
- })
309
- }
310
- }
311
- if (this.lrs.registration) {
312
- stmt.context.registration = this.lrs.registration
313
- }
314
- if (this.lrs.activity_platform) {
315
- stmt.context.platform = this.lrs.activity_platform
316
- }
317
- }
318
-
319
- // tests the configuration of the lrs object
320
- XAPIWrapper.prototype.testConfig = testConfig
321
-
322
- // writes to the console if available
323
- XAPIWrapper.prototype.log = log
324
-
325
- // Default encoding
326
- XAPIWrapper.prototype.defaultEncoding = 'utf-8'
327
-
328
- /*
329
- * Send a single statement to the LRS. Makes a Javascript object
330
- * with the statement id as 'id' available to the callback function.
331
- * @param {object} stmt statement object to send
332
- * @param {function} [callback] function to be called after the LRS responds
333
- * to this request (makes the call asynchronous)
334
- * the function will be passed the XMLHttpRequest object
335
- * and an object with an id property assigned the id
336
- * of the statement
337
- * @return {object} object containing xhr object and id of statement
338
- * @example
339
- * // Send Statement
340
- * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
341
- * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
342
- * "display" : {"en-US" : "answered"}},
343
- * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
344
- * let resp_obj = ADL.XAPIWrapper.sendStatement(stmt);
345
- * ADL.XAPIWrapper.log("[" + resp_obj.id + "]: " + resp_obj.xhr.status + " - " + resp_obj.xhr.statusText);
346
- * >> [3e616d1c-5394-42dc-a3aa-29414f8f0dfe]: 204 - NO CONTENT
347
- *
348
- * // Send Statement with Callback
349
- * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
350
- * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
351
- * "display" : {"en-US" : "answered"}},
352
- * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
353
- * ADL.XAPIWrapper.sendStatement(stmt, function(resp, obj){
354
- * ADL.XAPIWrapper.log("[" + obj.id + "]: " + resp.status + " - " + resp.statusText);});
355
- * >> [4edfe763-8b84-41f1-a355-78b7601a6fe8]: 204 - NO CONTENT
356
- */
357
- XAPIWrapper.prototype.sendStatement = function (stmt, callback, attachments) {
358
- if (this.testConfig()) {
359
- this.prepareStatement(stmt)
360
- let id
361
- if (stmt['id']) {
362
- id = stmt['id']
363
- } else {
364
- id = ADL.ruuid()
365
- stmt['id'] = id
366
- }
367
-
368
- let payload = JSON.stringify(stmt)
369
- let extraHeaders = null
370
- if (attachments && attachments.length > 0) {
371
- extraHeaders = {}
372
- payload = this.buildMultipartPost(stmt, attachments, extraHeaders)
373
- }
374
- let resp = ADL.XHR_request(
375
- this.lrs,
376
- this.lrs.endpoint + 'statements',
377
- 'POST',
378
- payload,
379
- this.lrs.auth,
380
- callback,
381
- { id: id },
382
- null,
383
- extraHeaders,
384
- this.withCredentials,
385
- this.strictCallbacks
386
- )
387
- if (!callback) return { xhr: resp[0], id: id }
388
- }
389
- }
390
-
391
- /*
392
- * Custome method to send statement to the LRS using the fech API instead of XMLHttpRequest. Makes a Javascript object
393
- * with the statement id as 'id' available to the callback function.
394
- * @param {object} stmt statement object to send
395
- * @param {function} [callback] function to be called after the LRS responds
396
- * to this request (makes the call asynchronous)
397
- * @return {object} object containing xhr object and id of statement
398
- * @example
399
- * // Send Statement
400
- * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
401
- * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
402
- * "display" : {"en-US" : "answered"}},
403
- * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
404
- * let resp_obj = ADL.XAPIWrapper.sendStatementWithFetch(stmt);
405
- * ADL.XAPIWrapper.log("[" + resp_obj.id + "]: " + resp_obj.xhr.status + " - " + resp_obj.xhr.statusText);
406
- * >> [3e616d1c-5394-42dc-a3aa-29414f8f0dfe]: 204 - NO CONTENT
407
- *
408
- * // Send Statement with Callback
409
- * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
410
- * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
411
- * "display" : {"en-US" : "answered"}},
412
- * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
413
- * ADL.XAPIWrapper.sendStatementWithFetch(stmt, function(resp, obj){
414
- * ADL.XAPIWrapper.log("[" + obj.id + "]: " + resp.status + " - " + resp.statusText);});
415
- * >> [4edfe763-8b84-41f1-a355-78b7601a6fe8]: 204 - NO CONTENT
416
- */
417
- XAPIWrapper.prototype.sendStatementWithFetch = async function (
418
- stmt,
419
- callback,
420
- attachments
421
- ) {
422
- if (this.testConfig()) {
423
- this.prepareStatement(stmt)
424
- let id
425
- if (stmt['id']) {
426
- id = stmt['id']
427
- } else {
428
- id = ADL.ruuid()
429
- stmt['id'] = id
430
- }
431
- let headers = {}
432
- headers['Content-type'] = 'application/json; charset=UTF-8'
433
- headers['Authorization'] = this.lrs.auth
434
- headers['X-Experience-API-Version'] = ADL.XAPIWrapper.xapiVersion
435
-
436
- let payload = JSON.stringify(stmt)
437
- let extraHeaders = null
438
- if (attachments && attachments.length > 0) {
439
- extraHeaders = {}
440
- payload = this.buildMultipartPost(stmt, attachments, extraHeaders)
441
- }
442
-
443
- // fecth API used to allow the delivery of the request after the browser is closed
444
- let resp = await fetch(this.lrs.endpoint + 'statements', {
445
- method: 'POST',
446
- headers,
447
- body: payload,
448
- keepalive: true // allow the request to outlive the closing of browser tab
449
- })
450
-
451
- if (!callback) return { xhr: resp, id: id }
452
- }
453
- }
454
-
455
- XAPIWrapper.prototype.stringToArrayBuffer = function (content, encoding) {
456
- encoding = encoding || ADL.XAPIWrapper.defaultEncoding
457
-
458
- return new TextEncoder(encoding).encode(content).buffer
459
- }
460
-
461
- XAPIWrapper.prototype.stringFromArrayBuffer = function (content, encoding) {
462
- encoding = encoding || ADL.XAPIWrapper.defaultEncoding
463
-
464
- return new TextDecoder(encoding).decode(content)
465
- }
466
-
467
- /*
468
- * Build the post body to include the multipart boundries, edit the statement to include the attachment types
469
- * extraHeaders should be an object. It will have the multipart boundary value set
470
- * attachments should be an array of objects of the type
471
- * {
472
- type:"signature" || {
473
- usageType : URI,
474
- display: Language-map
475
- description: Language-map
476
- },
477
- value : a UTF8 string containing the binary data of the attachment. For string values, this can just be the JS string.
478
- }
479
- */
480
- XAPIWrapper.prototype.buildMultipartPost = function (
481
- statement,
482
- attachments,
483
- extraHeaders
484
- ) {
485
- statement.attachments = []
486
- for (let i = 0; i < attachments.length; i++) {
487
- // Replace the term 'signature' with the hard coded definition for a signature attachment
488
- if (attachments[i].type == 'signature') {
489
- attachments[i].type = {
490
- usageType: 'http://adlnet.gov/expapi/attachments/signature',
491
- display: {
492
- 'en-US': 'A JWT signature'
493
- },
494
- description: {
495
- 'en-US': 'A signature proving the statement was not modified'
496
- },
497
- contentType: 'application/octet-stream'
498
- }
499
- }
500
-
501
- if (typeof attachments[i].value === 'string') {
502
- // Convert the string value to an array buffer.
503
- attachments[i].value = this.stringToArrayBuffer(attachments[i].value)
504
- }
505
-
506
- // Compute the length and the sha2 of the attachment
507
- attachments[i].type.length = attachments[i].value.byteLength
508
- attachments[i].type.sha2 = toSHA256(attachments[i].value)
509
-
510
- // Attach the attachment metadata to the statement.
511
- statement.attachments.push(attachments[i].type)
512
- }
513
-
514
- let blobParts = []
515
- let boundary =
516
- (Math.random() + ' ').substring(2, 10) +
517
- (Math.random() + ' ').substring(2, 10)
518
-
519
- extraHeaders['Content-Type'] = 'multipart/mixed; boundary=' + boundary
520
-
521
- let CRLF = '\r\n'
522
- let header =
523
- [
524
- '--' + boundary,
525
- 'Content-Type: application/json',
526
- 'Content-Disposition: form-data; name="statement"',
527
- '',
528
- JSON.stringify(statement)
529
- ].join(CRLF) + CRLF
530
-
531
- blobParts.push(header)
532
-
533
- for (let i in attachments) {
534
- if (attachments.hasOwnProperty(i)) {
535
- let attachmentHeader =
536
- [
537
- '--' + boundary,
538
- 'Content-Type: ' + attachments[i].type.contentType,
539
- 'Content-Transfer-Encoding: binary',
540
- 'X-Experience-API-Hash: ' + attachments[i].type.sha2
541
- ].join(CRLF) +
542
- CRLF +
543
- CRLF
544
-
545
- blobParts.push(attachmentHeader)
546
- blobParts.push(attachments[i].value)
547
- }
548
- }
549
-
550
- blobParts.push(CRLF + '--' + boundary + '--' + CRLF)
551
-
552
- return new Blob(blobParts)
553
- }
554
- /*
555
- * Send a list of statements to the LRS.
556
- * @param {array} stmtArray the list of statement objects to send
557
- * @param {function} [callback] function to be called after the LRS responds
558
- * to this request (makes the call asynchronous)
559
- * the function will be passed the XMLHttpRequest object
560
- * @return {object} xhr response object
561
- * @example
562
- * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
563
- * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
564
- * "display" : {"en-US" : "answered"}},
565
- * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
566
- * let resp_obj = ADL.XAPIWrapper.sendStatement(stmt);
567
- * ADL.XAPIWrapper.getStatements({"statementId":resp_obj.id});
568
- * >> {"version": "1.0.0",
569
- * "timestamp": "2013-09-09 21:36:40.185841+00:00",
570
- * "object": {"id": "http://adlnet.gov/expapi/activities/question", "objectType": "Activity"},
571
- * "actor": {"mbox": "mailto:tom@example.com", "name": "tom creighton", "objectType": "Agent"},
572
- * "stored": "2013-09-09 21:36:40.186124+00:00",
573
- * "verb": {"id": "http://adlnet.gov/expapi/verbs/answered", "display": {"en-US": "answered"}},
574
- * "authority": {"mbox": "mailto:tom@adlnet.gov", "name": "tom", "objectType": "Agent"},
575
- * "context": {"registration": "51a6f860-1997-11e3-8ffd-0800200c9a66"},
576
- * "id": "ea9c1d01-0606-4ec7-8e5d-20f87b1211ed"}
577
- */
578
- XAPIWrapper.prototype.sendStatements = function (stmtArray, callback) {
579
- if (this.testConfig()) {
580
- for (let i in stmtArray) {
581
- if (stmtArray.hasOwnProperty(i)) this.prepareStatement(stmtArray[i])
582
- }
583
- let resp = ADL.XHR_request(
584
- this.lrs,
585
- this.lrs.endpoint + 'statements',
586
- 'POST',
587
- JSON.stringify(stmtArray),
588
- this.lrs.auth,
589
- callback,
590
- null,
591
- false,
592
- null,
593
- this.withCredentials,
594
- this.strictCallbacks
595
- )
596
-
597
- if (!callback) {
598
- return resp
599
- }
600
- }
601
- }
602
-
603
- /*
604
- * Send a list of statements to the LRS.
605
- * @param {array} stmtArray the list of statement objects to send
606
- * @param {function} [callback] function to be called after the LRS responds
607
- * to this request (makes the call asynchronous)
608
- * the function will be passed the XMLHttpRequest object
609
- * @return {object} xhr response object
610
- * @example
611
- * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
612
- * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
613
- * "display" : {"en-US" : "answered"}},
614
- * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
615
- * let resp_obj = ADL.XAPIWrapper.sendStatement(stmt);
616
- * ADL.XAPIWrapper.getStatements({"statementId":resp_obj.id});
617
- * >> {"version": "1.0.0",
618
- * "timestamp": "2013-09-09 21:36:40.185841+00:00",
619
- * "object": {"id": "http://adlnet.gov/expapi/activities/question", "objectType": "Activity"},
620
- * "actor": {"mbox": "mailto:tom@example.com", "name": "tom creighton", "objectType": "Agent"},
621
- * "stored": "2013-09-09 21:36:40.186124+00:00",
622
- * "verb": {"id": "http://adlnet.gov/expapi/verbs/answered", "display": {"en-US": "answered"}},
623
- * "authority": {"mbox": "mailto:tom@adlnet.gov", "name": "tom", "objectType": "Agent"},
624
- * "context": {"registration": "51a6f860-1997-11e3-8ffd-0800200c9a66"},
625
- * "id": "ea9c1d01-0606-4ec7-8e5d-20f87b1211ed"}
626
- */
627
- XAPIWrapper.prototype.sendStatementsWithFetch = async function (
628
- stmtArray,
629
- callback
630
- ) {
631
- if (this.testConfig()) {
632
- for (let i in stmtArray) {
633
- if (stmtArray.hasOwnProperty(i)) this.prepareStatement(stmtArray[i])
634
- }
635
-
636
- let headers = {}
637
- headers['Content-type'] = 'application/json; charset=UTF-8'
638
- headers['Authorization'] = this.lrs.auth
639
- headers['X-Experience-API-Version'] = ADL.XAPIWrapper.xapiVersion
640
-
641
- let payload = JSON.stringify(stmtArray)
642
-
643
- // fecth API used to allow the delivery of the request after the browser is closed
644
- let resp = await fetch(this.lrs.endpoint + 'statements', {
645
- method: 'POST',
646
- headers,
647
- body: payload,
648
- keepalive: true // allow the request to outlive the closing of browser tab
649
- })
650
-
651
- //=============================================
652
- if (!callback) {
653
- return resp
654
- }
655
- }
656
- }
657
-
658
- /*
659
- * Get statement(s) based on the searchparams or more url.
660
- * @param {object || array } searchparams an ADL.XAPIWrapper.searchParams object of
661
- * key(search parameter)-value(parameter value) pairs.
662
- * Example:
663
- * let myparams = ADL.XAPIWrapper.searchParams();
664
- * myparams['verb'] = ADL.verbs.completed.id;
665
- * let completedStmts = ADL.XAPIWrapper.getStatements(myparams);
666
- * @param {string} more the more url found in the StatementResults object, if there are more
667
- * statements available based on your get statements request. Pass the
668
- * more url as this parameter to retrieve those statements.
669
- * @param {function} [callback] - function to be called after the LRS responds
670
- * to this request (makes the call asynchronous)
671
- * the function will be passed the XMLHttpRequest object
672
- * @return {object} xhr response object or null if 404
673
- * @example
674
- * let ret = ADL.XAPIWrapper.getStatements();
675
- * if (ret)
676
- * ADL.XAPIWrapper.log(ret.statements);
677
- *
678
- * >> <Array of statements>
679
- */
680
- XAPIWrapper.prototype.getStatements = async function (
681
- searchparams,
682
- more,
683
- callback
684
- ) {
685
- const listSearchParams = []
686
-
687
- searchparams.constructor == Array
688
- ? listSearchParams.push(...searchparams)
689
- : listSearchParams.push(searchparams)
690
-
691
- if (this.testConfig()) {
692
- let url = this.lrs.endpoint + 'statements'
693
- const URLS = []
694
- if (more) {
695
- url = this.base + more
696
- } else {
697
- for (let searchparams of listSearchParams) {
698
- let urlparams = new Array()
699
- let _urls = url
700
- for (let s in searchparams) {
701
- if (searchparams.hasOwnProperty(s)) {
702
- if (s == 'until' || s == 'since') {
703
- let d = new Date(searchparams[s])
704
- urlparams.push(s + '=' + encodeURIComponent(d.toISOString()))
705
- } else {
706
- urlparams.push(s + '=' + encodeURIComponent(searchparams[s]))
707
- }
708
- }
709
- }
710
- if (urlparams.length > 0) {
711
- _urls = _urls + '?' + urlparams.join('&')
712
- }
713
- URLS.push(_urls)
714
- }
715
- }
716
-
717
- let res = await ADL.XHR_request(
718
- this.lrs,
719
- URLS,
720
- 'GET',
721
- null,
722
- this.lrs.auth,
723
- callback,
724
- null,
725
- false,
726
- null,
727
- this.withCredentials,
728
- this.strictCallbacks
729
- )
730
-
731
- if (res === undefined || res.status == 404) {
732
- return null
733
- }
734
-
735
- try {
736
- return await res
737
- } catch (e) {
738
- return e
739
- }
740
- }
741
- }
742
-
743
- /*
744
- * Gets the Activity object from the LRS.
745
- * @param {string} activityid the id of the Activity to get
746
- * @param {function} [callback] function to be called after the LRS responds
747
- * to this request (makes the call asynchronous)
748
- * the function will be passed the XMLHttpRequest object
749
- * @return {object} xhr response object or null if 404
750
- * @example
751
- * let res = ADL.XAPIWrapper.getActivities("http://adlnet.gov/expapi/activities/question");
752
- * ADL.XAPIWrapper.log(res);
753
- * >> <Activity object>
754
- */
755
- XAPIWrapper.prototype.getActivities = async function (activityid, callback) {
756
- if (this.testConfig()) {
757
- let url = this.lrs.endpoint + 'activities?activityId=<activityid>'
758
- url = url.replace('<activityid>', encodeURIComponent(activityid))
759
-
760
- let result = ADL.XHR_request(
761
- this.lrs,
762
- url,
763
- 'GET',
764
- null,
765
- this.lrs.auth,
766
- callback,
767
- null,
768
- true,
769
- null,
770
- this.withCredentials,
771
- this.strictCallbacks
772
- )
773
-
774
- if (result === undefined || result.status == 404) {
775
- return null
776
- }
777
-
778
- try {
779
- return await result[0].json()
780
- } catch (e) {
781
- return result
782
- }
783
- }
784
- }
785
-
786
- /*
787
- * Store activity state in the LRS
788
- * @param {string} activityid the id of the Activity this state is about
789
- * @param {object} agent the agent this Activity state is related to
790
- * @param {string} stateid the id you want associated with this state
791
- * @param {string} [registration] the registraton id associated with this state
792
- * @param {string} stateval the state
793
- * @param {string} [matchHash] the hash of the state to replace or * to replace any
794
- * @param {string} [noneMatchHash] the hash of the current state or * to indicate no previous state
795
- * @param {function} [callback] function to be called after the LRS responds
796
- * to this request (makes the call asynchronous)
797
- * the function will be passed the XMLHttpRequest object
798
- * @return {boolean} false if no activity state is included
799
- * @example
800
- * let stateval = {"info":"the state info"};
801
- * ADL.XAPIWrapper.sendState("http://adlnet.gov/expapi/activities/question",
802
- * {"mbox":"mailto:tom@example.com"},
803
- * "questionstate", null, stateval);
804
- */
805
- XAPIWrapper.prototype.sendState = function (
806
- activityid,
807
- agent,
808
- stateid,
809
- registration,
810
- stateval,
811
- matchHash,
812
- noneMatchHash,
813
- callback
814
- ) {
815
- if (this.testConfig()) {
816
- let url =
817
- this.lrs.endpoint +
818
- 'activities/state?activityId=<activity ID>&agent=<agent>&stateId=<stateid>'
819
-
820
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
821
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
822
- url = url.replace('<stateid>', encodeURIComponent(stateid))
823
-
824
- if (registration) {
825
- url += '&registration=' + encodeURIComponent(registration)
826
- }
827
-
828
- let headers = null
829
- if (matchHash && noneMatchHash) {
830
- log("Can't have both If-Match and If-None-Match")
831
- } else if (matchHash) {
832
- headers = { 'If-Match': ADL.formatHash(matchHash) }
833
- } else if (noneMatchHash) {
834
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
835
- }
836
-
837
- let method = 'PUT'
838
- if (stateval) {
839
- if (stateval instanceof Array) {
840
- stateval = JSON.stringify(stateval)
841
- headers = headers || {}
842
- headers['Content-Type'] = 'application/json'
843
- } else if (stateval instanceof Object) {
844
- stateval = JSON.stringify(stateval)
845
- headers = headers || {}
846
- headers['Content-Type'] = 'application/json'
847
- method = 'POST'
848
- } else {
849
- headers = headers || {}
850
- headers['Content-Type'] = 'application/octet-stream'
851
- }
852
- } else {
853
- this.log('No activity state was included.')
854
- return false
855
- }
856
- //(lrs, url, method, data, auth, callback, callbackargs, ignore404, extraHeaders)
857
-
858
- ADL.XHR_request(
859
- this.lrs,
860
- url,
861
- method,
862
- stateval,
863
- this.lrs.auth,
864
- callback,
865
- null,
866
- null,
867
- headers,
868
- this.withCredentials,
869
- this.strictCallbacks
870
- )
871
- }
872
- }
873
-
874
- /*
875
- * Get activity state from the LRS
876
- * @param {string} activityid the id of the Activity this state is about
877
- * @param {object} agent the agent this Activity state is related to
878
- * @param {string} [stateid] the id of the state, if not included, the response will be a list of stateids
879
- * associated with the activity and agent)
880
- * @param {string} [registration] the registraton id associated with this state
881
- * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
882
- * @param {function} [callback] function to be called after the LRS responds
883
- * to this request (makes the call asynchronous)
884
- * the function will be passed the XMLHttpRequest object
885
- * @return {object} xhr response object or null if 404
886
- * @example
887
- * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
888
- * {"mbox":"mailto:tom@example.com"}, "questionstate");
889
- * >> {info: "the state info"}
890
- */
891
- XAPIWrapper.prototype.getState = async function (
892
- activityid,
893
- agent,
894
- stateid,
895
- registration,
896
- since,
897
- callback
898
- ) {
899
- if (this.testConfig()) {
900
- let url =
901
- this.lrs.endpoint +
902
- 'activities/state?activityId=<activity ID>&agent=<agent>'
903
-
904
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
905
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
906
-
907
- if (stateid) {
908
- url += '&stateId=' + encodeURIComponent(stateid)
909
- }
910
-
911
- if (registration) {
912
- url += '&registration=' + encodeURIComponent(registration)
913
- }
914
-
915
- if (since) {
916
- since = isDate(since)
917
- if (since != null) {
918
- url += '&since=' + encodeURIComponent(since.toISOString())
919
- }
920
- }
921
-
922
- let result = ADL.XHR_request(
923
- this.lrs,
924
- url,
925
- 'GET',
926
- null,
927
- this.lrs.auth,
928
- callback,
929
- null,
930
- true,
931
- null,
932
- this.withCredentials,
933
- this.strictCallbacks
934
- )
935
-
936
- if (result === undefined || result.status == 404) {
937
- return null
938
- }
939
-
940
- try {
941
- return await result[0].json()
942
- } catch (e) {
943
- return e
944
- }
945
- }
946
- }
947
-
948
- /*
949
- * Delete activity state in the LRS
950
- * @param {string} activityid the id of the Activity this state is about
951
- * @param {object} agent the agent this Activity state is related to
952
- * @param {string} stateid the id you want associated with this state
953
- * @param {string} [registration] the registraton id associated with this state
954
- * @param {string} [matchHash] the hash of the state to replace or * to replace any
955
- * @param {string} [noneMatchHash] the hash of the current state or * to indicate no previous state
956
- * @param {string} [callback] function to be called after the LRS responds
957
- * to this request (makes the call asynchronous)
958
- * the function will be passed the XMLHttpRequest object
959
- * @return {object} xhr response object or null if 404
960
- * @example
961
- * let stateval = {"info":"the state info"};
962
- * ADL.XAPIWrapper.sendState("http://adlnet.gov/expapi/activities/question",
963
- * {"mbox":"mailto:tom@example.com"},
964
- * "questionstate", null, stateval);
965
- * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
966
- * {"mbox":"mailto:tom@example.com"}, "questionstate");
967
- * >> {info: "the state info"}
968
- *
969
- * ADL.XAPIWrapper.deleteState("http://adlnet.gov/expapi/activities/question",
970
- * {"mbox":"mailto:tom@example.com"}, "questionstate");
971
- * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
972
- *
973
- * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
974
- * {"mbox":"mailto:tom@example.com"}, "questionstate");
975
- * >> 404
976
- */
977
- XAPIWrapper.prototype.deleteState = async function (
978
- activityid,
979
- agent,
980
- stateid,
981
- registration,
982
- matchHash,
983
- noneMatchHash,
984
- callback
985
- ) {
986
- if (this.testConfig()) {
987
- let url =
988
- this.lrs.endpoint +
989
- 'activities/state?activityId=<activity ID>&agent=<agent>&stateId=<stateid>'
990
-
991
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
992
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
993
- url = url.replace('<stateid>', encodeURIComponent(stateid))
994
-
995
- if (registration) {
996
- url += '&registration=' + encodeURIComponent(registration)
997
- }
998
-
999
- let headers = null
1000
- if (matchHash && noneMatchHash) {
1001
- log("Can't have both If-Match and If-None-Match")
1002
- } else if (matchHash) {
1003
- headers = { 'If-Match': ADL.formatHash(matchHash) }
1004
- } else if (noneMatchHash) {
1005
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1006
- }
1007
-
1008
- let result = ADL.XHR_request(
1009
- this.lrs,
1010
- url,
1011
- 'DELETE',
1012
- null,
1013
- this.lrs.auth,
1014
- callback,
1015
- null,
1016
- false,
1017
- headers,
1018
- this.withCredentials,
1019
- this.strictCallbacks
1020
- )
1021
-
1022
- if (result === undefined || result.status == 404) {
1023
- return null
1024
- }
1025
-
1026
- try {
1027
- return await result[0].json()
1028
- } catch (e) {
1029
- return result
1030
- }
1031
- }
1032
- }
1033
-
1034
- /*
1035
- * Store activity profile in the LRS
1036
- * @param {string} activityid the id of the Activity this profile is about
1037
- * @param {string} profileid the id you want associated with this profile
1038
- * @param {string} profileval the profile
1039
- * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1040
- * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1041
- * @param {string} [callback] function to be called after the LRS responds
1042
- * to this request (makes the call asynchronous)
1043
- * the function will be passed the XMLHttpRequest object
1044
- * @return {bolean} false if no activity profile is included
1045
- * @example
1046
- * let profile = {"info":"the profile"};
1047
- * ADL.XAPIWrapper.sendActivityProfile("http://adlnet.gov/expapi/activities/question",
1048
- * "actprofile", profile, null, "*");
1049
- */
1050
- XAPIWrapper.prototype.sendActivityProfile = function (
1051
- activityid,
1052
- profileid,
1053
- profileval,
1054
- matchHash,
1055
- noneMatchHash,
1056
- callback
1057
- ) {
1058
- if (this.testConfig()) {
1059
- let url =
1060
- this.lrs.endpoint +
1061
- 'activities/profile?activityId=<activity ID>&profileId=<profileid>'
1062
-
1063
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
1064
- url = url.replace('<profileid>', encodeURIComponent(profileid))
1065
-
1066
- let headers = null
1067
- if (matchHash && noneMatchHash) {
1068
- log("Can't have both If-Match and If-None-Match")
1069
- } else if (matchHash) {
1070
- headers = { 'If-Match': ADL.formatHash(matchHash) }
1071
- } else if (noneMatchHash) {
1072
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1073
- }
1074
-
1075
- let method = 'PUT'
1076
- if (profileval) {
1077
- if (profileval instanceof Array) {
1078
- profileval = JSON.stringify(profileval)
1079
- headers = headers || {}
1080
- headers['Content-Type'] = 'application/json'
1081
- } else if (profileval instanceof Object) {
1082
- profileval = JSON.stringify(profileval)
1083
- headers = headers || {}
1084
- headers['Content-Type'] = 'application/json'
1085
- method = 'POST'
1086
- } else {
1087
- headers = headers || {}
1088
- headers['Content-Type'] = 'application/octet-stream'
1089
- }
1090
- } else {
1091
- this.log('No activity profile was included.')
1092
- return false
1093
- }
1094
-
1095
- ADL.XHR_request(
1096
- this.lrs,
1097
- url,
1098
- method,
1099
- profileval,
1100
- this.lrs.auth,
1101
- callback,
1102
- null,
1103
- false,
1104
- headers,
1105
- this.withCredentials,
1106
- this.strictCallbacks
1107
- )
1108
- }
1109
- }
1110
-
1111
- /*
1112
- * Get activity profile from the LRS
1113
- * @param {string} activityid the id of the Activity this profile is about
1114
- * @param {string} [profileid] the id of the profile, if not included, the response will be a list of profileids
1115
- * associated with the activity
1116
- * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
1117
- * @param {function [callback] function to be called after the LRS responds
1118
- * to this request (makes the call asynchronous)
1119
- * the function will be passed the XMLHttpRequest object
1120
- * @return {object} xhr response object or null if 404
1121
- * @example
1122
- * ADL.XAPIWrapper.getActivityProfile("http://adlnet.gov/expapi/activities/question",
1123
- * "actprofile", null,
1124
- * function(r){ADL.XAPIWrapper.log(JSON.parse(r.response));});
1125
- * >> {info: "the profile"}
1126
- */
1127
- XAPIWrapper.prototype.getActivityProfile = async function (
1128
- activityid,
1129
- profileid,
1130
- since,
1131
- callback
1132
- ) {
1133
- if (this.testConfig()) {
1134
- let url =
1135
- this.lrs.endpoint + 'activities/profile?activityId=<activity ID>'
1136
-
1137
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
1138
-
1139
- if (profileid) {
1140
- url += '&profileId=' + encodeURIComponent(profileid)
1141
- }
1142
-
1143
- if (since) {
1144
- since = isDate(since)
1145
- if (since != null) {
1146
- url += '&since=' + encodeURIComponent(since.toISOString())
1147
- }
1148
- }
1149
-
1150
- let result = ADL.XHR_request(
1151
- this.lrs,
1152
- url,
1153
- 'GET',
1154
- null,
1155
- this.lrs.auth,
1156
- callback,
1157
- null,
1158
- true,
1159
- null,
1160
- this.withCredentials,
1161
- this.strictCallbacks
1162
- )
1163
-
1164
- if (result === undefined || result.status == 404) {
1165
- return null
1166
- }
1167
-
1168
- try {
1169
- return result[0].json()
1170
- } catch (e) {
1171
- return result
1172
- }
1173
- }
1174
- }
1175
-
1176
- /*
1177
- * Delete activity profile in the LRS
1178
- * @param {string} activityid the id of the Activity this profile is about
1179
- * @param {string} profileid the id you want associated with this profile
1180
- * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1181
- * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1182
- * @param {string} [callback] function to be called after the LRS responds
1183
- * to this request (makes the call asynchronous)
1184
- * the function will be passed the XMLHttpRequest object
1185
- * @return {object} xhr response object or null if 404
1186
- * @example
1187
- * ADL.XAPIWrapper.deleteActivityProfile("http://adlnet.gov/expapi/activities/question",
1188
- * "actprofile");
1189
- * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
1190
- */
1191
- XAPIWrapper.prototype.deleteActivityProfile = async function (
1192
- activityid,
1193
- profileid,
1194
- matchHash,
1195
- noneMatchHash,
1196
- callback
1197
- ) {
1198
- if (this.testConfig()) {
1199
- let url =
1200
- this.lrs.endpoint +
1201
- 'activities/profile?activityId=<activity ID>&profileId=<profileid>'
1202
-
1203
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
1204
- url = url.replace('<profileid>', encodeURIComponent(profileid))
1205
-
1206
- let headers = null
1207
- if (matchHash && noneMatchHash) {
1208
- log("Can't have both If-Match and If-None-Match")
1209
- } else if (matchHash) {
1210
- headers = { 'If-Match': ADL.formatHash(matchHash) }
1211
- } else if (noneMatchHash) {
1212
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1213
- }
1214
-
1215
- let result = ADL.XHR_request(
1216
- this.lrs,
1217
- url,
1218
- 'DELETE',
1219
- null,
1220
- this.lrs.auth,
1221
- callback,
1222
- null,
1223
- false,
1224
- headers,
1225
- this.withCredentials,
1226
- this.strictCallbacks
1227
- )
1228
-
1229
- if (result === undefined || result.status == 404) {
1230
- return null
1231
- }
1232
-
1233
- try {
1234
- return result[0].json()
1235
- } catch (e) {
1236
- return result
1237
- }
1238
- }
1239
- }
1240
-
1241
- /*
1242
- * Gets the Person object from the LRS based on an agent object.
1243
- * The Person object may contain more information about an agent.
1244
- * See the xAPI Spec for details.
1245
- * @param {object} agent the agent object to get a Person
1246
- * @param {function [callback] function to be called after the LRS responds
1247
- * to this request (makes the call asynchronous)
1248
- * the function will be passed the XMLHttpRequest object
1249
- * @return {object} xhr response object or null if 404
1250
- * @example
1251
- * let res = ADL.XAPIWrapper.getAgents({"mbox":"mailto:tom@example.com"});
1252
- * ADL.XAPIWrapper.log(res);
1253
- * >> <Person object>
1254
- */
1255
- XAPIWrapper.prototype.getAgents = async function (agent, callback) {
1256
- if (this.testConfig()) {
1257
- let url = this.lrs.endpoint + 'agents?agent=<agent>'
1258
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1259
-
1260
- let result = ADL.XHR_request(
1261
- this.lrs,
1262
- url,
1263
- 'GET',
1264
- null,
1265
- this.lrs.auth,
1266
- callback,
1267
- null,
1268
- true,
1269
- null,
1270
- this.withCredentials,
1271
- this.strictCallbacks
1272
- )
1273
-
1274
- if (result === undefined || result.status == 404) {
1275
- return null
1276
- }
1277
-
1278
- try {
1279
- return result[0].json()
1280
- } catch (e) {
1281
- return result
1282
- }
1283
- }
1284
- }
1285
-
1286
- /*
1287
- * Store agent profile in the LRS
1288
- * @param {object} agent the agent this profile is related to
1289
- * @param {string} profileid the id you want associated with this profile
1290
- * @param {string} profileval the profile
1291
- * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1292
- * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1293
- * @param {string} [callback] function to be called after the LRS responds
1294
- * to this request (makes the call asynchronous)
1295
- * the function will be passed the XMLHttpRequest object
1296
- * @return {object} false if no agent profile is included
1297
- * @example
1298
- * let profile = {"info":"the agent profile"};
1299
- * ADL.XAPIWrapper.sendAgentProfile({"mbox":"mailto:tom@example.com"},
1300
- * "agentprofile", profile, null, "*");
1301
- */
1302
- XAPIWrapper.prototype.sendAgentProfile = function (
1303
- agent,
1304
- profileid,
1305
- profileval,
1306
- matchHash,
1307
- noneMatchHash,
1308
- callback
1309
- ) {
1310
- if (this.testConfig()) {
1311
- let url =
1312
- this.lrs.endpoint + 'agents/profile?agent=<agent>&profileId=<profileid>'
1313
-
1314
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1315
- url = url.replace('<profileid>', encodeURIComponent(profileid))
1316
-
1317
- let headers = null
1318
- if (matchHash && noneMatchHash) {
1319
- log("Can't have both If-Match and If-None-Match")
1320
- } else if (matchHash) {
1321
- headers = { 'If-Match': ADL.formatHash(matchHash) }
1322
- } else if (noneMatchHash) {
1323
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1324
- }
1325
-
1326
- let method = 'PUT'
1327
- if (profileval) {
1328
- if (profileval instanceof Array) {
1329
- profileval = JSON.stringify(profileval)
1330
- headers = headers || {}
1331
- headers['Content-Type'] = 'application/json'
1332
- } else if (profileval instanceof Object) {
1333
- profileval = JSON.stringify(profileval)
1334
- headers = headers || {}
1335
- headers['Content-Type'] = 'application/json'
1336
- method = 'POST'
1337
- } else {
1338
- headers = headers || {}
1339
- headers['Content-Type'] = 'application/octet-stream'
1340
- }
1341
- } else {
1342
- this.log('No agent profile was included.')
1343
- return false
1344
- }
1345
-
1346
- ADL.XHR_request(
1347
- this.lrs,
1348
- url,
1349
- method,
1350
- profileval,
1351
- this.lrs.auth,
1352
- callback,
1353
- null,
1354
- false,
1355
- headers,
1356
- this.withCredentials,
1357
- this.strictCallbacks
1358
- )
1359
- }
1360
- }
1361
-
1362
- /*
1363
- * Get agnet profile from the LRS
1364
- * @param {object} agent the agent associated with this profile
1365
- * @param {string} [profileid] the id of the profile, if not included, the response will be a list of profileids
1366
- * associated with the agent
1367
- * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
1368
- * @param {function} [callback] function to be called after the LRS responds
1369
- * to this request (makes the call asynchronous)
1370
- * the function will be passed the XMLHttpRequest object
1371
- * @return {object} xhr response object or null if 404
1372
- * @example
1373
- * ADL.XAPIWrapper.getAgentProfile({"mbox":"mailto:tom@example.com"},
1374
- * "agentprofile", null,
1375
- * function(r){ADL.XAPIWrapper.log(JSON.parse(r.response));});
1376
- * >> {info: "the agent profile"}
1377
- */
1378
- XAPIWrapper.prototype.getAgentProfile = async function (
1379
- agent,
1380
- profileid,
1381
- since,
1382
- callback
1383
- ) {
1384
- if (this.testConfig()) {
1385
- let url = this.lrs.endpoint + 'agents/profile?agent=<agent>'
1386
-
1387
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1388
- url = url.replace('<profileid>', encodeURIComponent(profileid))
1389
-
1390
- if (profileid) {
1391
- url += '&profileId=' + encodeURIComponent(profileid)
1392
- }
1393
-
1394
- if (since) {
1395
- since = isDate(since)
1396
- if (since != null) {
1397
- url += '&since=' + encodeURIComponent(since.toISOString())
1398
- }
1399
- }
1400
-
1401
- let result = ADL.XHR_request(
1402
- this.lrs,
1403
- url,
1404
- 'GET',
1405
- null,
1406
- this.lrs.auth,
1407
- callback,
1408
- null,
1409
- true,
1410
- null,
1411
- this.withCredentials,
1412
- this.strictCallbacks
1413
- )
1414
-
1415
- if (result === undefined || result.status == 404) {
1416
- return null
1417
- }
1418
-
1419
- try {
1420
- return result[0].json()
1421
- } catch (e) {
1422
- return result.response
1423
- }
1424
- }
1425
- }
1426
-
1427
- /*
1428
- * Delete agent profile in the LRS
1429
- * @param {oject} agent the id of the Agent this profile is about
1430
- * @param {string} profileid the id you want associated with this profile
1431
- * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1432
- * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1433
- * @param {string} [callback] function to be called after the LRS responds
1434
- * to this request (makes the call asynchronous)
1435
- * the function will be passed the XMLHttpRequest object
1436
- * @return {object} xhr response object or null if 404
1437
- * @example
1438
- * ADL.XAPIWrapper.deleteAgentProfile({"mbox":"mailto:tom@example.com"},
1439
- * "agentprofile");
1440
- * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
1441
- */
1442
- XAPIWrapper.prototype.deleteAgentProfile = async function (
1443
- agent,
1444
- profileid,
1445
- matchHash,
1446
- noneMatchHash,
1447
- callback
1448
- ) {
1449
- if (this.testConfig()) {
1450
- let url =
1451
- this.lrs.endpoint + 'agents/profile?agent=<agent>&profileId=<profileid>'
1452
-
1453
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1454
- url = url.replace('<profileid>', encodeURIComponent(profileid))
1455
-
1456
- let headers = null
1457
- if (matchHash && noneMatchHash) {
1458
- log("Can't have both If-Match and If-None-Match")
1459
- } else if (matchHash) {
1460
- headers = { 'If-Match': ADL.formatHash(matchHash) }
1461
- } else if (noneMatchHash) {
1462
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1463
- }
1464
-
1465
- let result = ADL.XHR_request(
1466
- this.lrs,
1467
- url,
1468
- 'DELETE',
1469
- null,
1470
- this.lrs.auth,
1471
- callback,
1472
- null,
1473
- false,
1474
- headers,
1475
- this.withCredentials,
1476
- this.strictCallbacks
1477
- )
1478
-
1479
- if (result === undefined || result.status == 404) {
1480
- return null
1481
- }
1482
-
1483
- try {
1484
- return result[0].json()
1485
- } catch (e) {
1486
- return result
1487
- }
1488
- }
1489
- }
1490
-
1491
- /*
1492
- * Tests the configuration of the lrs object
1493
- */
1494
- function testConfig() {
1495
- try {
1496
- return this.lrs.endpoint != undefined && this.lrs.endpoint != ''
1497
- } catch (e) {
1498
- return false
1499
- }
1500
- }
1501
-
1502
- // outputs the message to the console if available
1503
- function log(message) {
1504
- if (!log.debug) return false
1505
- try {
1506
- message
1507
- return true
1508
- } catch (e) {
1509
- return false
1510
- }
1511
- }
1512
-
1513
- // merges two object
1514
- function mergeRecursive(obj1, obj2) {
1515
- for (let p in obj2) {
1516
- if (obj2.hasOwnProperty(p) == false) continue
1517
-
1518
- let prop = obj2[p]
1519
- log(p + ' : ' + prop)
1520
- try {
1521
- // Property in destination object set; update its value.
1522
- if (obj2[p].constructor == Object) {
1523
- obj1[p] = mergeRecursive(obj1[p], obj2[p])
1524
- } else {
1525
- if (obj1 == undefined) {
1526
- obj1 = new Object()
1527
- }
1528
- obj1[p] = obj2[p]
1529
- }
1530
- } catch (e) {
1531
- if (obj1 == undefined) {
1532
- obj1 = new Object()
1533
- }
1534
- // Property in destination object not set; create it and set its value.
1535
- obj1[p] = obj2[p]
1536
- }
1537
- }
1538
-
1539
- return obj1
1540
- }
1541
-
1542
- // iniitializes an lrs object with settings from
1543
- // a config file and from the url query string
1544
- function getLRSObject(config) {
1545
- let lrsProps = [
1546
- 'endpoint',
1547
- 'auth',
1548
- 'actor',
1549
- 'registration',
1550
- 'activity_id',
1551
- 'grouping',
1552
- 'activity_platform'
1553
- ]
1554
- let lrs = new Object()
1555
- let qsVars, prop
1556
-
1557
- qsVars = parseQueryString()
1558
- if (qsVars !== undefined && Object.keys(qsVars).length !== 0) {
1559
- for (let i = 0; i < lrsProps.length; i++) {
1560
- prop = lrsProps[i]
1561
- if (qsVars[prop]) {
1562
- lrs[prop] = qsVars[prop]
1563
- delete qsVars[prop]
1564
- }
1565
- }
1566
- // if (Object.keys(qsVars).length !== 0) {
1567
- // lrs.extended = qsVars;
1568
- // }
1569
-
1570
- lrs = mergeRecursive(config, lrs)
1571
- } else {
1572
- lrs = config
1573
- }
1574
-
1575
- return lrs
1576
- }
1577
-
1578
- // parses the params in the url query string
1579
- function parseQueryString() {
1580
- let qs, pairs, pair, ii, parsed
1581
-
1582
- qs = window.location.search.substring(1)
1583
-
1584
- pairs = qs.split('&')
1585
- parsed = {}
1586
- for (ii = 0; ii < pairs.length; ii++) {
1587
- pair = pairs[ii].split('=')
1588
- if (pair.length === 2 && pair[0]) {
1589
- parsed[pair[0]] = decodeURIComponent(pair[1])
1590
- }
1591
- }
1592
-
1593
- return parsed
1594
- }
1595
-
1596
- function delay() {
1597
- let xhr = new XMLHttpRequest()
1598
- let url = window.location + '?forcenocache=' + ADL.ruuid()
1599
- xhr.open('GET', url, false)
1600
- xhr.send(null)
1601
- }
1602
-
1603
- /*
1604
- * formats a request in a way that IE will allow
1605
- * @param {string} method the http request method (ex: "PUT", "GET")
1606
- * @param {string} url the url to the request (ex: ADL.XAPIWrapper.lrs.endpoint + "statements")
1607
- * @param {array} [headers] headers to include in the request
1608
- * @param {string} [data] the body of the request, if there is one
1609
- * @return {object} xhr response object
1610
- */
1611
- // function ie_request(method, url, headers, data) {
1612
- // let newUrl = url
1613
-
1614
- // //Everything that was on query string goes into form vars
1615
- // let formData = new Array()
1616
- // let qsIndex = newUrl.indexOf('?')
1617
- // if (qsIndex > 0) {
1618
- // formData.push(newUrl.substring(qsIndex + 1))
1619
- // newUrl = newUrl.substring(0, qsIndex)
1620
- // }
1621
-
1622
- // //Method has to go on querystring, and nothing else
1623
- // newUrl = newUrl + '?method=' + method
1624
-
1625
- // //Headers
1626
- // if (headers !== null) {
1627
- // for (let headerName in headers) {
1628
- // if (headers.hasOwnProperty(headerName))
1629
- // formData.push(
1630
- // headerName + '=' + encodeURIComponent(headers[headerName])
1631
- // )
1632
- // }
1633
- // }
1634
-
1635
- // //The original data is repackaged as "content" form var
1636
- // if (data !== null) {
1637
- // formData.push('content=' + encodeURIComponent(data))
1638
- // }
1639
-
1640
- // return {
1641
- // method: 'POST',
1642
- // url: newUrl,
1643
- // headers: {},
1644
- // data: formData.join('&')
1645
- // }
1646
- // }
1647
-
1648
- /*!
1649
- Excerpt from: Math.uuid.js (v1.4)
1650
- http://www.broofa.com
1651
- mailto:robert@broofa.com
1652
- Copyright (c) 2010 Robert Kieffer
1653
- Dual licensed under the MIT and GPL licenses.
1654
- */
1655
- ADL.ruuid = function () {
1656
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
1657
- /[xy]/g,
1658
- function (c) {
1659
- let r = (Math.random() * 16) | 0,
1660
- v = c == 'x' ? r : (r & 0x3) | 0x8
1661
- return v.toString(16)
1662
- }
1663
- )
1664
- }
1665
-
1666
- /*
1667
- * dateFromISOString
1668
- * parses an ISO string into a date object
1669
- * isostr - the ISO string
1670
- */
1671
- ADL.dateFromISOString = function (isostr) {
1672
- let regexp =
1673
- '([0-9]{4})(-([0-9]{2})(-([0-9]{2})' +
1674
- '([T| ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(.([0-9]+))?)?' +
1675
- '(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?'
1676
- let d = isostr.match(new RegExp(regexp))
1677
-
1678
- let offset = 0
1679
- let date = new Date(d[1], 0, 1)
1680
-
1681
- if (d[3]) {
1682
- date.setMonth(d[3] - 1)
1683
- }
1684
- if (d[5]) {
1685
- date.setDate(d[5])
1686
- }
1687
- if (d[7]) {
1688
- date.setHours(d[7])
1689
- }
1690
- if (d[8]) {
1691
- date.setMinutes(d[8])
1692
- }
1693
- if (d[10]) {
1694
- date.setSeconds(d[10])
1695
- }
1696
- if (d[12]) {
1697
- date.setMilliseconds(Number('0.' + d[12]) * 1000)
1698
- }
1699
- if (d[14]) {
1700
- offset = Number(d[16]) * 60 + Number(d[17])
1701
- offset *= d[15] == '-' ? 1 : -1
1702
- }
1703
-
1704
- offset -= date.getTimezoneOffset()
1705
- let time = Number(date) + offset * 60 * 1000
1706
-
1707
- let dateToReturn = new Date()
1708
- dateToReturn.setTime(Number(time))
1709
- return dateToReturn
1710
- }
1711
-
1712
- // Synchronous if callback is not provided (not recommended)
1713
- /*
1714
- * makes a request to a server (if possible, use functions provided in XAPIWrapper)
1715
- * @param {string} lrs the lrs connection info, such as endpoint, auth, etc
1716
- * @param {string | Array } urls the url of this request
1717
- * @param {string} method the http request method
1718
- * @param {string} data the payload
1719
- * @param {string} auth the value for the Authorization header
1720
- * @param {function} callback function to be called after the LRS responds
1721
- * to this request (makes the call asynchronous)
1722
- * @param {object} [callbackargs] additional javascript object to be passed to the callback function
1723
- * @param {boolean} ignore404 allow page not found errors to pass
1724
- * @param {object} extraHeaders other header key-values to be added to this request
1725
- * @param {boolean} withCredentials
1726
- * @param {boolean} strictCallbacks Callback must be executed and first param is error or null if no error
1727
- * @return {Objec} containing the status of last promise request and Array of the responses;
1728
- */
1729
- ADL.XHR_request = function (
1730
- lrs,
1731
- urls,
1732
- method,
1733
- data,
1734
- auth,
1735
- callback,
1736
- callbackargs,
1737
- ignore404,
1738
- extraHeaders,
1739
- withCredentials,
1740
- strictCallbacks
1741
- ) {
1742
- 'use strict'
1743
- let URLList = urls.constructor == Array ? [...urls] : [urls]
1744
- // Consolidate headers
1745
- let headers = {
1746
- 'Content-Type': 'application/json',
1747
- Authorization: auth,
1748
- 'X-Experience-API-Version': ADL.XAPIWrapper.xapiVersion
1749
- }
1750
-
1751
- if (extraHeaders !== null) {
1752
- for (let headerName in extraHeaders) {
1753
- if (extraHeaders.hasOwnProperty(headerName)) {
1754
- headers[headerName] = extraHeaders[headerName]
1755
- }
1756
- }
1757
- }
1758
-
1759
- // Add extended LMS-specified values to the URL
1760
- if (lrs !== null && lrs.extended !== undefined) {
1761
- let extended = []
1762
- for (let prop in lrs.extended) {
1763
- extended.push(prop + '=' + encodeURIComponent(lrs.extended[prop]))
1764
- }
1765
- if (extended.length > 0) {
1766
- URLList.map(
1767
- (url) =>
1768
- (url += (url.indexOf('?') > -1 ? '&' : '?') + extended.join('&'))
1769
- )
1770
- }
1771
- }
1772
- let requestArray = []
1773
-
1774
- for (const url of URLList) {
1775
- // Prepare request options for fetch
1776
- const fetchOptions = {
1777
- method: method,
1778
- headers: headers,
1779
- body: data ? JSON.stringify(data) : null,
1780
- credentials: withCredentials ? 'include' : 'same-origin'
1781
- }
1782
- requestArray.push(fetch(url, fetchOptions))
1783
- }
1784
-
1785
- // If no callback, we assume synchronous mode (using Promise)
1786
- const makeRequest = async (requests) => {
1787
- try {
1788
- const response = await Promise.all(requests)
1789
- const status = response[response.length - 1].status
1790
- let body
1791
-
1792
- if (status === 404 && ignore404) {
1793
- // If we are ignoring 404s
1794
- body = null
1795
- } else if (status >= 200 && status < 400) {
1796
- // Successful response
1797
- body = await response
1798
- } else {
1799
- throw new Error(
1800
- `There was a problem communicating with the Learning Record Store. (${status} | ${response.statusText}) ${urls}`
1801
- )
1802
- }
1803
-
1804
- // Callback handling
1805
- if (callback) {
1806
- if (strictCallbacks) {
1807
- callback(null, response, body, status)
1808
- } else {
1809
- callback(response, body, status)
1810
- }
1811
- }
1812
- return { response, status }
1813
- } catch (error) {
1814
- // Log error to the console or call error handler
1815
- ADL.XAPIWrapper.log(error.toString())
1816
- if (callback) {
1817
- if (strictCallbacks) {
1818
- callback(error, null, callbackargs)
1819
- } else {
1820
- callback(null, callbackargs)
1821
- }
1822
- }
1823
- return null
1824
- }
1825
- }
1826
-
1827
- if (!callback) {
1828
- // If no callback, we return the Promise (synchronous behavior)
1829
- return makeRequest(requestArray)
1830
- }
1831
-
1832
- // If there's a callback, proceed with asynchronous request
1833
- makeRequest(requestArray)
1834
- }
1835
-
1836
- //====================================================================
1837
-
1838
- /*
1839
- * Holder for custom global error callback
1840
- * @param {object} xhr xhr object or null
1841
- * @param {string} method XMLHttpRequest request method
1842
- * @param {string} url full endpoint url
1843
- * @param {function} callback function to be called after the LRS responds
1844
- * to this request (makes the call asynchronous)
1845
- * @param {object} [callbackargs] additional javascript object to be passed to the callback function
1846
- * @param {boolean} strictCallbacks Callback must be executed and first param is error or null if no error
1847
- * @example
1848
- * ADL.xhrRequestOnError = function(xhr, method, url, callback, callbackargs) {
1849
- * console.log(xhr);
1850
- * alert(xhr.status + " " + xhr.statusText + ": " + xhr.response);
1851
- * };
1852
- */
1853
- ADL.xhrRequestOnError = function (
1854
- xhr,
1855
- method,
1856
- url,
1857
- callback,
1858
- callbackargs,
1859
- strictCallbacks
1860
- ) {
1861
- if (callback && strictCallbacks) {
1862
- let status = xhr ? xhr.status : undefined
1863
- let error
1864
- if (status) {
1865
- error = new Error('Request error: ' + xhr.status)
1866
- } else if (status === 0 || status === null) {
1867
- // 0 and null = aborted
1868
- error = new Error('Request error: aborted')
1869
- } else {
1870
- error = new Error('Reqeust error: unknown')
1871
- }
1872
-
1873
- if (callbackargs) {
1874
- callback(error, xhr, callbackargs)
1875
- } else {
1876
- let body
1877
-
1878
- try {
1879
- body = JSON.parse(xhr.responseText)
1880
- } catch (e) {
1881
- body = xhr.responseText
1882
- }
1883
-
1884
- callback(error, xhr, body)
1885
- }
1886
- }
1887
- }
1888
-
1889
- ADL.formatHash = function (hash) {
1890
- return hash === '*' ? hash : '"' + hash + '"'
1891
- }
1892
-
1893
- ADL.XAPIWrapper = new XAPIWrapper(Config, false)
1894
- //============================= END ==========================================
1895
- }
1
+ import { CryptoJS } from './Crypto'
2
+
3
+ /*
4
+ * Wrapper Ref: https://github.com/adlnet/xAPIWrapper
5
+ */
6
+ export function xapiwrapper(ADL) {
7
+ //==============================================================================
8
+ // adds toISOString to date objects if not there
9
+ // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
10
+
11
+ if (!Date.prototype.toISOString) {
12
+ ;(function () {
13
+ function pad(number) {
14
+ let r = String(number)
15
+ if (r.length === 1) {
16
+ r = '0' + r
17
+ }
18
+ return r
19
+ }
20
+
21
+ Date.prototype.toISOString = function () {
22
+ return (
23
+ this.getUTCFullYear() +
24
+ '-' +
25
+ pad(this.getUTCMonth() + 1) +
26
+ '-' +
27
+ pad(this.getUTCDate()) +
28
+ 'T' +
29
+ pad(this.getUTCHours()) +
30
+ ':' +
31
+ pad(this.getUTCMinutes()) +
32
+ ':' +
33
+ pad(this.getUTCSeconds()) +
34
+ '.' +
35
+ String((this.getUTCMilliseconds() / 1000).toFixed(3)).slice(2, 5) +
36
+ 'Z'
37
+ )
38
+ }
39
+ })()
40
+ }
41
+ // shim for old-style Base64 lib
42
+ function toBase64(text) {
43
+ if (CryptoJS && CryptoJS.enc.Base64)
44
+ return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(text))
45
+ // else return Base64.encode(text)
46
+ }
47
+
48
+ // shim for old-style crypto lib
49
+ function toSHA1(text) {
50
+ if (CryptoJS && CryptoJS.SHA1) return CryptoJS.SHA1(text).toString()
51
+ else return Crypto.util.bytesToHex(Crypto.SHA1(text, { asBytes: true }))
52
+ }
53
+
54
+ function toSHA256(content) {
55
+ if (Object.prototype.toString.call(content) !== '[object ArrayBuffer]') {
56
+ return CryptoJS.SHA256(content).toString(CryptoJS.enc.Hex)
57
+ }
58
+
59
+ // Create a WordArray from the ArrayBuffer.
60
+ let i8a = new Uint8Array(content)
61
+ let a = []
62
+ for (let i = 0; i < i8a.length; i += 4) {
63
+ a.push(
64
+ (i8a[i] << 24) | (i8a[i + 1] << 16) | (i8a[i + 2] << 8) | i8a[i + 3]
65
+ )
66
+ }
67
+
68
+ return CryptoJS.SHA256(
69
+ CryptoJS.lib.WordArray.create(a, i8a.length)
70
+ ).toString(CryptoJS.enc.Hex)
71
+ }
72
+
73
+ // check if string or object is date, if it is, return date object
74
+ // feburary 31st == march 3rd in this solution
75
+ function isDate(date) {
76
+ // check if object is being passed
77
+ let d
78
+ if (Object.prototype.toString.call(date) === '[object Date]') d = date
79
+ else d = new Date(date)
80
+ // deep check on date object
81
+ if (Object.prototype.toString.call(d) === '[object Date]') {
82
+ // it is a date
83
+ if (isNaN(d.valueOf())) {
84
+ ADL.XAPIWrapper.log('Invalid date String passed')
85
+ return null
86
+ } else {
87
+ return d
88
+ }
89
+ } else {
90
+ // not a date
91
+ ADL.XAPIWrapper.log('Invalid date object')
92
+ return null
93
+ }
94
+ }
95
+ //////////////////////////////////////////////////////////////////////
96
+ log.debug = false
97
+
98
+ function getByteLen(normal_val) {
99
+ // Force string type
100
+ normal_val = String(normal_val)
101
+
102
+ let byteLen = 0
103
+ for (let i = 0; i < normal_val.length; i++) {
104
+ let c = normal_val.charCodeAt(i)
105
+ byteLen +=
106
+ c < 1 << 7
107
+ ? 1
108
+ : c < 1 << 11
109
+ ? 2
110
+ : c < 1 << 16
111
+ ? 3
112
+ : c < 1 << 21
113
+ ? 4
114
+ : c < 1 << 26
115
+ ? 5
116
+ : c < 1 << 31
117
+ ? 6
118
+ : Number.NaN
119
+ }
120
+ return byteLen
121
+ }
122
+ getByteLen()
123
+ /*
124
+ * Config object used w/ url params to configure the lrs object
125
+ * change these to match your lrs
126
+ * @return {object} config object
127
+ * @example
128
+ * let conf = {
129
+ * "endpoint" : "https://lrs.adlnet.gov/xapi/",
130
+ * "auth" : "Basic " + toBase64('tom:1234'),
131
+ * };
132
+ * ADL.XAPIWrapper.changeConfig(conf);
133
+ */
134
+ let Config = (function () {
135
+ let conf = {}
136
+ conf['endpoint'] = 'http://localhost:8000/xapi/'
137
+ //try
138
+ //{
139
+ conf['auth'] = 'Basic ' + toBase64('tom:1234')
140
+ //}
141
+ //catch (e)
142
+ //{
143
+ // log("Exception in Config trying to encode auth: " + e);
144
+ //}
145
+
146
+ // Statement defaults
147
+ // conf["actor"] = {"mbox":"default@example.com"};
148
+ // conf["registration"] = ruuid();
149
+ // conf["grouping"] = {"id":"ctxact:default/grouping"};
150
+ // conf["activity_platform"] = "default platform";
151
+
152
+ // Behavior defaults
153
+ // conf["strictCallbacks"] = false; // Strict error-first callbacks
154
+ return conf
155
+ })()
156
+ /*
157
+ * XAPIWrapper Constructor
158
+ * @param {object} config with a minimum of an endoint property
159
+ * @param {boolean} verifyxapiversion indicating whether to verify the version of the LRS is compatible with this wrapper
160
+ */
161
+ let XAPIWrapper = function (config, verifyxapiversion) {
162
+ this.lrs = getLRSObject(config || {})
163
+ if (this.lrs.user && this.lrs.password)
164
+ updateAuth(this.lrs, this.lrs.user, this.lrs.password)
165
+ this.base = getbase(this.lrs.endpoint)
166
+
167
+ this.withCredentials = false
168
+ if (config && typeof config.withCredentials != 'undefined') {
169
+ this.withCredentials = config.withCredentials
170
+ }
171
+
172
+ // Ensure that callbacks are always executed, first param is error (null if no error) followed
173
+ // by the result(s)
174
+ this.strictCallbacks = false
175
+ this.strictCallbacks = config && config.strictCallbacks
176
+
177
+ function getbase(url) {
178
+ let l = document.createElement('a')
179
+ l.href = url
180
+ if (l.protocol && l.host) {
181
+ return l.protocol + '//' + l.host
182
+ } else if (l.href) {
183
+ // IE 11 fix.
184
+ let parts = l.href.split('//')
185
+ return parts[0] + '//' + parts[1].substring(0, parts[1].indexOf('/'))
186
+ } else
187
+ ADL.XAPIWrapper.log("Couldn't create base url from endpoint: " + url)
188
+ }
189
+
190
+ function updateAuth(obj, username, password) {
191
+ obj.auth = 'Basic ' + toBase64(username + ':' + password)
192
+ }
193
+
194
+ if (verifyxapiversion && testConfig.call(this)) {
195
+ window.ADL.XHR_request(
196
+ this.lrs,
197
+ this.lrs.endpoint + 'about',
198
+ 'GET',
199
+ null,
200
+ null,
201
+ function (r) {
202
+ if (r.status == 200) {
203
+ try {
204
+ let lrsabout = r.json()
205
+ let versionOK = false
206
+ for (let idx in lrsabout.version) {
207
+ if (lrsabout.version.hasOwnProperty(idx))
208
+ if (lrsabout.version[idx] == ADL.XAPIWrapper.xapiVersion) {
209
+ versionOK = true
210
+ break
211
+ }
212
+ }
213
+ if (!versionOK) {
214
+ ADL.XAPIWrapper.log(
215
+ 'The lrs version [' +
216
+ lrsabout.version +
217
+ ']' +
218
+ " does not match this wrapper's XAPI version [" +
219
+ ADL.XAPIWrapper.xapiVersion +
220
+ ']'
221
+ )
222
+ }
223
+ } catch (e) {
224
+ ADL.XAPIWrapper.log('The response was not an about object')
225
+ }
226
+ } else {
227
+ ADL.XAPIWrapper.log(
228
+ 'The request to get information about the LRS failed: ' + r
229
+ )
230
+ }
231
+ },
232
+ null,
233
+ false,
234
+ null,
235
+ this.withCredentials,
236
+ false
237
+ )
238
+ }
239
+
240
+ this.searchParams = function () {
241
+ let sp = { format: 'exact' }
242
+ return sp
243
+ }
244
+
245
+ this.hash = function (tohash) {
246
+ if (!tohash) return null
247
+ try {
248
+ return toSHA1(tohash)
249
+ } catch (e) {
250
+ ADL.XAPIWrapper.log('Error trying to hash -- ' + e)
251
+ return null
252
+ }
253
+ }
254
+
255
+ this.changeConfig = function (config) {
256
+ try {
257
+ ADL.XAPIWrapper.log('updating lrs object with new configuration')
258
+ this.lrs = mergeRecursive(this.lrs, config)
259
+ if (config.user && config.password)
260
+ this.updateAuth(this.lrs, config.user, config.password)
261
+ this.base = getbase(this.lrs.endpoint)
262
+ this.withCredentials = config.withCredentials
263
+ this.strictCallbacks = config.strictCallbacks
264
+ } catch (e) {
265
+ ADL.XAPIWrapper.log('error while changing configuration -- ' + e)
266
+ }
267
+ }
268
+
269
+ this.updateAuth = updateAuth
270
+ }
271
+
272
+ // This wrapper is based on the Experience API Spec version:
273
+ XAPIWrapper.prototype.xapiVersion = '1.0.1'
274
+
275
+ /*
276
+ * Adds info from the lrs object to the statement, if available.
277
+ * These values could be initialized from the Config object or from the url query string.
278
+ * @param {object} stmt the statement object
279
+ */
280
+ XAPIWrapper.prototype.prepareStatement = function (stmt) {
281
+ if (stmt.actor === undefined) {
282
+ stmt.actor = JSON.parse(this.lrs.actor)
283
+ } else if (typeof stmt.actor === 'string') {
284
+ stmt.actor = JSON.parse(stmt.actor)
285
+ }
286
+
287
+ if (
288
+ this.lrs.grouping ||
289
+ this.lrs.registration ||
290
+ this.lrs.activity_platform
291
+ ) {
292
+ if (!stmt.context) {
293
+ stmt.context = {}
294
+ }
295
+ }
296
+
297
+ if (this.lrs.grouping) {
298
+ if (!stmt.context.contextActivities) {
299
+ stmt.context.contextActivities = {}
300
+ }
301
+
302
+ // PR from brian-learningpool to resolve context overwriting
303
+ if (!Array.isArray(stmt.context.contextActivities.grouping)) {
304
+ stmt.context.contextActivities.grouping = [{ id: this.lrs.grouping }]
305
+ } else {
306
+ stmt.context.contextActivities.grouping.splice(0, 0, {
307
+ id: this.lrs.grouping
308
+ })
309
+ }
310
+ }
311
+ if (this.lrs.registration) {
312
+ stmt.context.registration = this.lrs.registration
313
+ }
314
+ if (this.lrs.activity_platform) {
315
+ stmt.context.platform = this.lrs.activity_platform
316
+ }
317
+ }
318
+
319
+ // tests the configuration of the lrs object
320
+ XAPIWrapper.prototype.testConfig = testConfig
321
+
322
+ // writes to the console if available
323
+ XAPIWrapper.prototype.log = log
324
+
325
+ // Default encoding
326
+ XAPIWrapper.prototype.defaultEncoding = 'utf-8'
327
+
328
+ /*
329
+ * Send a single statement to the LRS. Makes a Javascript object
330
+ * with the statement id as 'id' available to the callback function.
331
+ * @param {object} stmt statement object to send
332
+ * @param {function} [callback] function to be called after the LRS responds
333
+ * to this request (makes the call asynchronous)
334
+ * the function will be passed the XMLHttpRequest object
335
+ * and an object with an id property assigned the id
336
+ * of the statement
337
+ * @return {object} object containing xhr object and id of statement
338
+ * @example
339
+ * // Send Statement
340
+ * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
341
+ * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
342
+ * "display" : {"en-US" : "answered"}},
343
+ * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
344
+ * let resp_obj = ADL.XAPIWrapper.sendStatement(stmt);
345
+ * ADL.XAPIWrapper.log("[" + resp_obj.id + "]: " + resp_obj.xhr.status + " - " + resp_obj.xhr.statusText);
346
+ * >> [3e616d1c-5394-42dc-a3aa-29414f8f0dfe]: 204 - NO CONTENT
347
+ *
348
+ * // Send Statement with Callback
349
+ * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
350
+ * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
351
+ * "display" : {"en-US" : "answered"}},
352
+ * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
353
+ * ADL.XAPIWrapper.sendStatement(stmt, function(resp, obj){
354
+ * ADL.XAPIWrapper.log("[" + obj.id + "]: " + resp.status + " - " + resp.statusText);});
355
+ * >> [4edfe763-8b84-41f1-a355-78b7601a6fe8]: 204 - NO CONTENT
356
+ */
357
+ XAPIWrapper.prototype.sendStatement = function (stmt, callback, attachments) {
358
+ if (this.testConfig()) {
359
+ this.prepareStatement(stmt)
360
+ let id
361
+ if (stmt['id']) {
362
+ id = stmt['id']
363
+ } else {
364
+ id = ADL.ruuid()
365
+ stmt['id'] = id
366
+ }
367
+
368
+ let payload = JSON.stringify(stmt)
369
+ let extraHeaders = null
370
+ if (attachments && attachments.length > 0) {
371
+ extraHeaders = {}
372
+ payload = this.buildMultipartPost(stmt, attachments, extraHeaders)
373
+ }
374
+ let resp = ADL.XHR_request(
375
+ this.lrs,
376
+ this.lrs.endpoint + 'statements',
377
+ 'POST',
378
+ payload,
379
+ this.lrs.auth,
380
+ callback,
381
+ { id: id },
382
+ null,
383
+ extraHeaders,
384
+ this.withCredentials,
385
+ this.strictCallbacks
386
+ )
387
+ if (!callback) return { xhr: resp[0], id: id }
388
+ }
389
+ }
390
+
391
+ /*
392
+ * Custome method to send statement to the LRS using the fech API instead of XMLHttpRequest. Makes a Javascript object
393
+ * with the statement id as 'id' available to the callback function.
394
+ * @param {object} stmt statement object to send
395
+ * @param {function} [callback] function to be called after the LRS responds
396
+ * to this request (makes the call asynchronous)
397
+ * @return {object} object containing xhr object and id of statement
398
+ * @example
399
+ * // Send Statement
400
+ * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
401
+ * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
402
+ * "display" : {"en-US" : "answered"}},
403
+ * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
404
+ * let resp_obj = ADL.XAPIWrapper.sendStatementWithFetch(stmt);
405
+ * ADL.XAPIWrapper.log("[" + resp_obj.id + "]: " + resp_obj.xhr.status + " - " + resp_obj.xhr.statusText);
406
+ * >> [3e616d1c-5394-42dc-a3aa-29414f8f0dfe]: 204 - NO CONTENT
407
+ *
408
+ * // Send Statement with Callback
409
+ * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
410
+ * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
411
+ * "display" : {"en-US" : "answered"}},
412
+ * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
413
+ * ADL.XAPIWrapper.sendStatementWithFetch(stmt, function(resp, obj){
414
+ * ADL.XAPIWrapper.log("[" + obj.id + "]: " + resp.status + " - " + resp.statusText);});
415
+ * >> [4edfe763-8b84-41f1-a355-78b7601a6fe8]: 204 - NO CONTENT
416
+ */
417
+ XAPIWrapper.prototype.sendStatementWithFetch = async function (
418
+ stmt,
419
+ callback,
420
+ attachments
421
+ ) {
422
+ if (this.testConfig()) {
423
+ this.prepareStatement(stmt)
424
+ let id
425
+ if (stmt['id']) {
426
+ id = stmt['id']
427
+ } else {
428
+ id = ADL.ruuid()
429
+ stmt['id'] = id
430
+ }
431
+ let headers = {}
432
+ headers['Content-type'] = 'application/json; charset=UTF-8'
433
+ headers['Authorization'] = this.lrs.auth
434
+ headers['X-Experience-API-Version'] = ADL.XAPIWrapper.xapiVersion
435
+
436
+ let payload = JSON.stringify(stmt)
437
+ let extraHeaders = null
438
+ if (attachments && attachments.length > 0) {
439
+ extraHeaders = {}
440
+ payload = this.buildMultipartPost(stmt, attachments, extraHeaders)
441
+ }
442
+
443
+ // fecth API used to allow the delivery of the request after the browser is closed
444
+ let resp = await fetch(this.lrs.endpoint + 'statements', {
445
+ method: 'POST',
446
+ headers,
447
+ body: payload,
448
+ keepalive: true // allow the request to outlive the closing of browser tab
449
+ })
450
+
451
+ if (!callback) return { xhr: resp, id: id }
452
+ }
453
+ }
454
+
455
+ XAPIWrapper.prototype.stringToArrayBuffer = function (content, encoding) {
456
+ encoding = encoding || ADL.XAPIWrapper.defaultEncoding
457
+
458
+ return new TextEncoder(encoding).encode(content).buffer
459
+ }
460
+
461
+ XAPIWrapper.prototype.stringFromArrayBuffer = function (content, encoding) {
462
+ encoding = encoding || ADL.XAPIWrapper.defaultEncoding
463
+
464
+ return new TextDecoder(encoding).decode(content)
465
+ }
466
+
467
+ /*
468
+ * Build the post body to include the multipart boundries, edit the statement to include the attachment types
469
+ * extraHeaders should be an object. It will have the multipart boundary value set
470
+ * attachments should be an array of objects of the type
471
+ * {
472
+ type:"signature" || {
473
+ usageType : URI,
474
+ display: Language-map
475
+ description: Language-map
476
+ },
477
+ value : a UTF8 string containing the binary data of the attachment. For string values, this can just be the JS string.
478
+ }
479
+ */
480
+ XAPIWrapper.prototype.buildMultipartPost = function (
481
+ statement,
482
+ attachments,
483
+ extraHeaders
484
+ ) {
485
+ statement.attachments = []
486
+ for (let i = 0; i < attachments.length; i++) {
487
+ // Replace the term 'signature' with the hard coded definition for a signature attachment
488
+ if (attachments[i].type == 'signature') {
489
+ attachments[i].type = {
490
+ usageType: 'http://adlnet.gov/expapi/attachments/signature',
491
+ display: {
492
+ 'en-US': 'A JWT signature'
493
+ },
494
+ description: {
495
+ 'en-US': 'A signature proving the statement was not modified'
496
+ },
497
+ contentType: 'application/octet-stream'
498
+ }
499
+ }
500
+
501
+ if (typeof attachments[i].value === 'string') {
502
+ // Convert the string value to an array buffer.
503
+ attachments[i].value = this.stringToArrayBuffer(attachments[i].value)
504
+ }
505
+
506
+ // Compute the length and the sha2 of the attachment
507
+ attachments[i].type.length = attachments[i].value.byteLength
508
+ attachments[i].type.sha2 = toSHA256(attachments[i].value)
509
+
510
+ // Attach the attachment metadata to the statement.
511
+ statement.attachments.push(attachments[i].type)
512
+ }
513
+
514
+ let blobParts = []
515
+ let boundary =
516
+ (Math.random() + ' ').substring(2, 10) +
517
+ (Math.random() + ' ').substring(2, 10)
518
+
519
+ extraHeaders['Content-Type'] = 'multipart/mixed; boundary=' + boundary
520
+
521
+ let CRLF = '\r\n'
522
+ let header =
523
+ [
524
+ '--' + boundary,
525
+ 'Content-Type: application/json',
526
+ 'Content-Disposition: form-data; name="statement"',
527
+ '',
528
+ JSON.stringify(statement)
529
+ ].join(CRLF) + CRLF
530
+
531
+ blobParts.push(header)
532
+
533
+ for (let i in attachments) {
534
+ if (attachments.hasOwnProperty(i)) {
535
+ let attachmentHeader =
536
+ [
537
+ '--' + boundary,
538
+ 'Content-Type: ' + attachments[i].type.contentType,
539
+ 'Content-Transfer-Encoding: binary',
540
+ 'X-Experience-API-Hash: ' + attachments[i].type.sha2
541
+ ].join(CRLF) +
542
+ CRLF +
543
+ CRLF
544
+
545
+ blobParts.push(attachmentHeader)
546
+ blobParts.push(attachments[i].value)
547
+ }
548
+ }
549
+
550
+ blobParts.push(CRLF + '--' + boundary + '--' + CRLF)
551
+
552
+ return new Blob(blobParts)
553
+ }
554
+ /*
555
+ * Send a list of statements to the LRS.
556
+ * @param {array} stmtArray the list of statement objects to send
557
+ * @param {function} [callback] function to be called after the LRS responds
558
+ * to this request (makes the call asynchronous)
559
+ * the function will be passed the XMLHttpRequest object
560
+ * @return {object} xhr response object
561
+ * @example
562
+ * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
563
+ * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
564
+ * "display" : {"en-US" : "answered"}},
565
+ * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
566
+ * let resp_obj = ADL.XAPIWrapper.sendStatement(stmt);
567
+ * ADL.XAPIWrapper.getStatements({"statementId":resp_obj.id});
568
+ * >> {"version": "1.0.0",
569
+ * "timestamp": "2013-09-09 21:36:40.185841+00:00",
570
+ * "object": {"id": "http://adlnet.gov/expapi/activities/question", "objectType": "Activity"},
571
+ * "actor": {"mbox": "mailto:tom@example.com", "name": "tom creighton", "objectType": "Agent"},
572
+ * "stored": "2013-09-09 21:36:40.186124+00:00",
573
+ * "verb": {"id": "http://adlnet.gov/expapi/verbs/answered", "display": {"en-US": "answered"}},
574
+ * "authority": {"mbox": "mailto:tom@adlnet.gov", "name": "tom", "objectType": "Agent"},
575
+ * "context": {"registration": "51a6f860-1997-11e3-8ffd-0800200c9a66"},
576
+ * "id": "ea9c1d01-0606-4ec7-8e5d-20f87b1211ed"}
577
+ */
578
+ XAPIWrapper.prototype.sendStatements = function (stmtArray, callback) {
579
+ if (this.testConfig()) {
580
+ for (let i in stmtArray) {
581
+ if (stmtArray.hasOwnProperty(i)) this.prepareStatement(stmtArray[i])
582
+ }
583
+ let resp = ADL.XHR_request(
584
+ this.lrs,
585
+ this.lrs.endpoint + 'statements',
586
+ 'POST',
587
+ JSON.stringify(stmtArray),
588
+ this.lrs.auth,
589
+ callback,
590
+ null,
591
+ false,
592
+ null,
593
+ this.withCredentials,
594
+ this.strictCallbacks
595
+ )
596
+
597
+ if (!callback) {
598
+ return resp
599
+ }
600
+ }
601
+ }
602
+
603
+ /*
604
+ * Send a list of statements to the LRS.
605
+ * @param {array} stmtArray the list of statement objects to send
606
+ * @param {function} [callback] function to be called after the LRS responds
607
+ * to this request (makes the call asynchronous)
608
+ * the function will be passed the XMLHttpRequest object
609
+ * @return {object} xhr response object
610
+ * @example
611
+ * let stmt = {"actor" : {"mbox" : "mailto:tom@example.com"},
612
+ * "verb" : {"id" : "http://adlnet.gov/expapi/verbs/answered",
613
+ * "display" : {"en-US" : "answered"}},
614
+ * "object" : {"id" : "http://adlnet.gov/expapi/activities/question"}};
615
+ * let resp_obj = ADL.XAPIWrapper.sendStatement(stmt);
616
+ * ADL.XAPIWrapper.getStatements({"statementId":resp_obj.id});
617
+ * >> {"version": "1.0.0",
618
+ * "timestamp": "2013-09-09 21:36:40.185841+00:00",
619
+ * "object": {"id": "http://adlnet.gov/expapi/activities/question", "objectType": "Activity"},
620
+ * "actor": {"mbox": "mailto:tom@example.com", "name": "tom creighton", "objectType": "Agent"},
621
+ * "stored": "2013-09-09 21:36:40.186124+00:00",
622
+ * "verb": {"id": "http://adlnet.gov/expapi/verbs/answered", "display": {"en-US": "answered"}},
623
+ * "authority": {"mbox": "mailto:tom@adlnet.gov", "name": "tom", "objectType": "Agent"},
624
+ * "context": {"registration": "51a6f860-1997-11e3-8ffd-0800200c9a66"},
625
+ * "id": "ea9c1d01-0606-4ec7-8e5d-20f87b1211ed"}
626
+ */
627
+ XAPIWrapper.prototype.sendStatementsWithFetch = async function (
628
+ stmtArray,
629
+ callback
630
+ ) {
631
+ if (this.testConfig()) {
632
+ for (let i in stmtArray) {
633
+ if (stmtArray.hasOwnProperty(i)) this.prepareStatement(stmtArray[i])
634
+ }
635
+
636
+ let headers = {}
637
+ headers['Content-type'] = 'application/json; charset=UTF-8'
638
+ headers['Authorization'] = this.lrs.auth
639
+ headers['X-Experience-API-Version'] = ADL.XAPIWrapper.xapiVersion
640
+
641
+ let payload = JSON.stringify(stmtArray)
642
+
643
+ // fecth API used to allow the delivery of the request after the browser is closed
644
+ let resp = await fetch(this.lrs.endpoint + 'statements', {
645
+ method: 'POST',
646
+ headers,
647
+ body: payload,
648
+ keepalive: true // allow the request to outlive the closing of browser tab
649
+ })
650
+
651
+ //=============================================
652
+ if (!callback) {
653
+ return resp
654
+ }
655
+ }
656
+ }
657
+
658
+ /*
659
+ * Get statement(s) based on the searchparams or more url.
660
+ * @param {object || array } searchparams an ADL.XAPIWrapper.searchParams object of
661
+ * key(search parameter)-value(parameter value) pairs.
662
+ * Example:
663
+ * let myparams = ADL.XAPIWrapper.searchParams();
664
+ * myparams['verb'] = ADL.verbs.completed.id;
665
+ * let completedStmts = ADL.XAPIWrapper.getStatements(myparams);
666
+ * @param {string} more the more url found in the StatementResults object, if there are more
667
+ * statements available based on your get statements request. Pass the
668
+ * more url as this parameter to retrieve those statements.
669
+ * @param {function} [callback] - function to be called after the LRS responds
670
+ * to this request (makes the call asynchronous)
671
+ * the function will be passed the XMLHttpRequest object
672
+ * @return {object} xhr response object or null if 404
673
+ * @example
674
+ * let ret = ADL.XAPIWrapper.getStatements();
675
+ * if (ret)
676
+ * ADL.XAPIWrapper.log(ret.statements);
677
+ *
678
+ * >> <Array of statements>
679
+ */
680
+ XAPIWrapper.prototype.getStatements = async function (
681
+ searchparams,
682
+ more,
683
+ callback
684
+ ) {
685
+ const listSearchParams = []
686
+
687
+ searchparams.constructor == Array
688
+ ? listSearchParams.push(...searchparams)
689
+ : listSearchParams.push(searchparams)
690
+
691
+ if (this.testConfig()) {
692
+ let url = this.lrs.endpoint + 'statements'
693
+ const URLS = []
694
+ if (more) {
695
+ url = this.base + more
696
+ } else {
697
+ for (let searchparams of listSearchParams) {
698
+ let urlparams = new Array()
699
+ let _urls = url
700
+ for (let s in searchparams) {
701
+ if (searchparams.hasOwnProperty(s)) {
702
+ if (s == 'until' || s == 'since') {
703
+ let d = new Date(searchparams[s])
704
+ urlparams.push(s + '=' + encodeURIComponent(d.toISOString()))
705
+ } else {
706
+ urlparams.push(s + '=' + encodeURIComponent(searchparams[s]))
707
+ }
708
+ }
709
+ }
710
+ if (urlparams.length > 0) {
711
+ _urls = _urls + '?' + urlparams.join('&')
712
+ }
713
+ URLS.push(_urls)
714
+ }
715
+ }
716
+
717
+ let res = await ADL.XHR_request(
718
+ this.lrs,
719
+ URLS,
720
+ 'GET',
721
+ null,
722
+ this.lrs.auth,
723
+ callback,
724
+ null,
725
+ false,
726
+ null,
727
+ this.withCredentials,
728
+ this.strictCallbacks
729
+ )
730
+
731
+ if (res === undefined || res.status == 404) {
732
+ return null
733
+ }
734
+
735
+ try {
736
+ return await res
737
+ } catch (e) {
738
+ return e
739
+ }
740
+ }
741
+ }
742
+
743
+ /*
744
+ * Gets the Activity object from the LRS.
745
+ * @param {string} activityid the id of the Activity to get
746
+ * @param {function} [callback] function to be called after the LRS responds
747
+ * to this request (makes the call asynchronous)
748
+ * the function will be passed the XMLHttpRequest object
749
+ * @return {object} xhr response object or null if 404
750
+ * @example
751
+ * let res = ADL.XAPIWrapper.getActivities("http://adlnet.gov/expapi/activities/question");
752
+ * ADL.XAPIWrapper.log(res);
753
+ * >> <Activity object>
754
+ */
755
+ XAPIWrapper.prototype.getActivities = async function (activityid, callback) {
756
+ if (this.testConfig()) {
757
+ let url = this.lrs.endpoint + 'activities?activityId=<activityid>'
758
+ url = url.replace('<activityid>', encodeURIComponent(activityid))
759
+
760
+ let result = ADL.XHR_request(
761
+ this.lrs,
762
+ url,
763
+ 'GET',
764
+ null,
765
+ this.lrs.auth,
766
+ callback,
767
+ null,
768
+ true,
769
+ null,
770
+ this.withCredentials,
771
+ this.strictCallbacks
772
+ )
773
+
774
+ if (result === undefined || result.status == 404) {
775
+ return null
776
+ }
777
+
778
+ try {
779
+ return await result[0].json()
780
+ } catch (e) {
781
+ return result
782
+ }
783
+ }
784
+ }
785
+
786
+ /*
787
+ * Store activity state in the LRS
788
+ * @param {string} activityid the id of the Activity this state is about
789
+ * @param {object} agent the agent this Activity state is related to
790
+ * @param {string} stateid the id you want associated with this state
791
+ * @param {string} [registration] the registraton id associated with this state
792
+ * @param {string} stateval the state
793
+ * @param {string} [matchHash] the hash of the state to replace or * to replace any
794
+ * @param {string} [noneMatchHash] the hash of the current state or * to indicate no previous state
795
+ * @param {function} [callback] function to be called after the LRS responds
796
+ * to this request (makes the call asynchronous)
797
+ * the function will be passed the XMLHttpRequest object
798
+ * @return {boolean} false if no activity state is included
799
+ * @example
800
+ * let stateval = {"info":"the state info"};
801
+ * ADL.XAPIWrapper.sendState("http://adlnet.gov/expapi/activities/question",
802
+ * {"mbox":"mailto:tom@example.com"},
803
+ * "questionstate", null, stateval);
804
+ */
805
+ XAPIWrapper.prototype.sendState = function (
806
+ activityid,
807
+ agent,
808
+ stateid,
809
+ registration,
810
+ stateval,
811
+ matchHash,
812
+ noneMatchHash,
813
+ callback
814
+ ) {
815
+ if (this.testConfig()) {
816
+ let url =
817
+ this.lrs.endpoint +
818
+ 'activities/state?activityId=<activity ID>&agent=<agent>&stateId=<stateid>'
819
+
820
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
821
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
822
+ url = url.replace('<stateid>', encodeURIComponent(stateid))
823
+
824
+ if (registration) {
825
+ url += '&registration=' + encodeURIComponent(registration)
826
+ }
827
+
828
+ let headers = null
829
+ if (matchHash && noneMatchHash) {
830
+ log("Can't have both If-Match and If-None-Match")
831
+ } else if (matchHash) {
832
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
833
+ } else if (noneMatchHash) {
834
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
835
+ }
836
+
837
+ let method = 'PUT'
838
+ if (stateval) {
839
+ if (stateval instanceof Array) {
840
+ stateval = JSON.stringify(stateval)
841
+ headers = headers || {}
842
+ headers['Content-Type'] = 'application/json'
843
+ } else if (stateval instanceof Object) {
844
+ stateval = JSON.stringify(stateval)
845
+ headers = headers || {}
846
+ headers['Content-Type'] = 'application/json'
847
+ method = 'POST'
848
+ } else {
849
+ headers = headers || {}
850
+ headers['Content-Type'] = 'application/octet-stream'
851
+ }
852
+ } else {
853
+ this.log('No activity state was included.')
854
+ return false
855
+ }
856
+ //(lrs, url, method, data, auth, callback, callbackargs, ignore404, extraHeaders)
857
+
858
+ ADL.XHR_request(
859
+ this.lrs,
860
+ url,
861
+ method,
862
+ stateval,
863
+ this.lrs.auth,
864
+ callback,
865
+ null,
866
+ null,
867
+ headers,
868
+ this.withCredentials,
869
+ this.strictCallbacks
870
+ )
871
+ }
872
+ }
873
+
874
+ /*
875
+ * Get activity state from the LRS
876
+ * @param {string} activityid the id of the Activity this state is about
877
+ * @param {object} agent the agent this Activity state is related to
878
+ * @param {string} [stateid] the id of the state, if not included, the response will be a list of stateids
879
+ * associated with the activity and agent)
880
+ * @param {string} [registration] the registraton id associated with this state
881
+ * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
882
+ * @param {function} [callback] function to be called after the LRS responds
883
+ * to this request (makes the call asynchronous)
884
+ * the function will be passed the XMLHttpRequest object
885
+ * @return {object} xhr response object or null if 404
886
+ * @example
887
+ * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
888
+ * {"mbox":"mailto:tom@example.com"}, "questionstate");
889
+ * >> {info: "the state info"}
890
+ */
891
+ XAPIWrapper.prototype.getState = async function (
892
+ activityid,
893
+ agent,
894
+ stateid,
895
+ registration,
896
+ since,
897
+ callback
898
+ ) {
899
+ if (this.testConfig()) {
900
+ let url =
901
+ this.lrs.endpoint +
902
+ 'activities/state?activityId=<activity ID>&agent=<agent>'
903
+
904
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
905
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
906
+
907
+ if (stateid) {
908
+ url += '&stateId=' + encodeURIComponent(stateid)
909
+ }
910
+
911
+ if (registration) {
912
+ url += '&registration=' + encodeURIComponent(registration)
913
+ }
914
+
915
+ if (since) {
916
+ since = isDate(since)
917
+ if (since != null) {
918
+ url += '&since=' + encodeURIComponent(since.toISOString())
919
+ }
920
+ }
921
+
922
+ let result = ADL.XHR_request(
923
+ this.lrs,
924
+ url,
925
+ 'GET',
926
+ null,
927
+ this.lrs.auth,
928
+ callback,
929
+ null,
930
+ true,
931
+ null,
932
+ this.withCredentials,
933
+ this.strictCallbacks
934
+ )
935
+
936
+ if (result === undefined || result.status == 404) {
937
+ return null
938
+ }
939
+
940
+ try {
941
+ return await result[0].json()
942
+ } catch (e) {
943
+ return e
944
+ }
945
+ }
946
+ }
947
+
948
+ /*
949
+ * Delete activity state in the LRS
950
+ * @param {string} activityid the id of the Activity this state is about
951
+ * @param {object} agent the agent this Activity state is related to
952
+ * @param {string} stateid the id you want associated with this state
953
+ * @param {string} [registration] the registraton id associated with this state
954
+ * @param {string} [matchHash] the hash of the state to replace or * to replace any
955
+ * @param {string} [noneMatchHash] the hash of the current state or * to indicate no previous state
956
+ * @param {string} [callback] function to be called after the LRS responds
957
+ * to this request (makes the call asynchronous)
958
+ * the function will be passed the XMLHttpRequest object
959
+ * @return {object} xhr response object or null if 404
960
+ * @example
961
+ * let stateval = {"info":"the state info"};
962
+ * ADL.XAPIWrapper.sendState("http://adlnet.gov/expapi/activities/question",
963
+ * {"mbox":"mailto:tom@example.com"},
964
+ * "questionstate", null, stateval);
965
+ * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
966
+ * {"mbox":"mailto:tom@example.com"}, "questionstate");
967
+ * >> {info: "the state info"}
968
+ *
969
+ * ADL.XAPIWrapper.deleteState("http://adlnet.gov/expapi/activities/question",
970
+ * {"mbox":"mailto:tom@example.com"}, "questionstate");
971
+ * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
972
+ *
973
+ * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
974
+ * {"mbox":"mailto:tom@example.com"}, "questionstate");
975
+ * >> 404
976
+ */
977
+ XAPIWrapper.prototype.deleteState = async function (
978
+ activityid,
979
+ agent,
980
+ stateid,
981
+ registration,
982
+ matchHash,
983
+ noneMatchHash,
984
+ callback
985
+ ) {
986
+ if (this.testConfig()) {
987
+ let url =
988
+ this.lrs.endpoint +
989
+ 'activities/state?activityId=<activity ID>&agent=<agent>&stateId=<stateid>'
990
+
991
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
992
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
993
+ url = url.replace('<stateid>', encodeURIComponent(stateid))
994
+
995
+ if (registration) {
996
+ url += '&registration=' + encodeURIComponent(registration)
997
+ }
998
+
999
+ let headers = null
1000
+ if (matchHash && noneMatchHash) {
1001
+ log("Can't have both If-Match and If-None-Match")
1002
+ } else if (matchHash) {
1003
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
1004
+ } else if (noneMatchHash) {
1005
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1006
+ }
1007
+
1008
+ let result = ADL.XHR_request(
1009
+ this.lrs,
1010
+ url,
1011
+ 'DELETE',
1012
+ null,
1013
+ this.lrs.auth,
1014
+ callback,
1015
+ null,
1016
+ false,
1017
+ headers,
1018
+ this.withCredentials,
1019
+ this.strictCallbacks
1020
+ )
1021
+
1022
+ if (result === undefined || result.status == 404) {
1023
+ return null
1024
+ }
1025
+
1026
+ try {
1027
+ return await result[0].json()
1028
+ } catch (e) {
1029
+ return result
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ /*
1035
+ * Store activity profile in the LRS
1036
+ * @param {string} activityid the id of the Activity this profile is about
1037
+ * @param {string} profileid the id you want associated with this profile
1038
+ * @param {string} profileval the profile
1039
+ * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1040
+ * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1041
+ * @param {string} [callback] function to be called after the LRS responds
1042
+ * to this request (makes the call asynchronous)
1043
+ * the function will be passed the XMLHttpRequest object
1044
+ * @return {bolean} false if no activity profile is included
1045
+ * @example
1046
+ * let profile = {"info":"the profile"};
1047
+ * ADL.XAPIWrapper.sendActivityProfile("http://adlnet.gov/expapi/activities/question",
1048
+ * "actprofile", profile, null, "*");
1049
+ */
1050
+ XAPIWrapper.prototype.sendActivityProfile = function (
1051
+ activityid,
1052
+ profileid,
1053
+ profileval,
1054
+ matchHash,
1055
+ noneMatchHash,
1056
+ callback
1057
+ ) {
1058
+ if (this.testConfig()) {
1059
+ let url =
1060
+ this.lrs.endpoint +
1061
+ 'activities/profile?activityId=<activity ID>&profileId=<profileid>'
1062
+
1063
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
1064
+ url = url.replace('<profileid>', encodeURIComponent(profileid))
1065
+
1066
+ let headers = null
1067
+ if (matchHash && noneMatchHash) {
1068
+ log("Can't have both If-Match and If-None-Match")
1069
+ } else if (matchHash) {
1070
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
1071
+ } else if (noneMatchHash) {
1072
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1073
+ }
1074
+
1075
+ let method = 'PUT'
1076
+ if (profileval) {
1077
+ if (profileval instanceof Array) {
1078
+ profileval = JSON.stringify(profileval)
1079
+ headers = headers || {}
1080
+ headers['Content-Type'] = 'application/json'
1081
+ } else if (profileval instanceof Object) {
1082
+ profileval = JSON.stringify(profileval)
1083
+ headers = headers || {}
1084
+ headers['Content-Type'] = 'application/json'
1085
+ method = 'POST'
1086
+ } else {
1087
+ headers = headers || {}
1088
+ headers['Content-Type'] = 'application/octet-stream'
1089
+ }
1090
+ } else {
1091
+ this.log('No activity profile was included.')
1092
+ return false
1093
+ }
1094
+
1095
+ ADL.XHR_request(
1096
+ this.lrs,
1097
+ url,
1098
+ method,
1099
+ profileval,
1100
+ this.lrs.auth,
1101
+ callback,
1102
+ null,
1103
+ false,
1104
+ headers,
1105
+ this.withCredentials,
1106
+ this.strictCallbacks
1107
+ )
1108
+ }
1109
+ }
1110
+
1111
+ /*
1112
+ * Get activity profile from the LRS
1113
+ * @param {string} activityid the id of the Activity this profile is about
1114
+ * @param {string} [profileid] the id of the profile, if not included, the response will be a list of profileids
1115
+ * associated with the activity
1116
+ * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
1117
+ * @param {function [callback] function to be called after the LRS responds
1118
+ * to this request (makes the call asynchronous)
1119
+ * the function will be passed the XMLHttpRequest object
1120
+ * @return {object} xhr response object or null if 404
1121
+ * @example
1122
+ * ADL.XAPIWrapper.getActivityProfile("http://adlnet.gov/expapi/activities/question",
1123
+ * "actprofile", null,
1124
+ * function(r){ADL.XAPIWrapper.log(JSON.parse(r.response));});
1125
+ * >> {info: "the profile"}
1126
+ */
1127
+ XAPIWrapper.prototype.getActivityProfile = async function (
1128
+ activityid,
1129
+ profileid,
1130
+ since,
1131
+ callback
1132
+ ) {
1133
+ if (this.testConfig()) {
1134
+ let url =
1135
+ this.lrs.endpoint + 'activities/profile?activityId=<activity ID>'
1136
+
1137
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
1138
+
1139
+ if (profileid) {
1140
+ url += '&profileId=' + encodeURIComponent(profileid)
1141
+ }
1142
+
1143
+ if (since) {
1144
+ since = isDate(since)
1145
+ if (since != null) {
1146
+ url += '&since=' + encodeURIComponent(since.toISOString())
1147
+ }
1148
+ }
1149
+
1150
+ let result = ADL.XHR_request(
1151
+ this.lrs,
1152
+ url,
1153
+ 'GET',
1154
+ null,
1155
+ this.lrs.auth,
1156
+ callback,
1157
+ null,
1158
+ true,
1159
+ null,
1160
+ this.withCredentials,
1161
+ this.strictCallbacks
1162
+ )
1163
+
1164
+ if (result === undefined || result.status == 404) {
1165
+ return null
1166
+ }
1167
+
1168
+ try {
1169
+ return result[0].json()
1170
+ } catch (e) {
1171
+ return result
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ /*
1177
+ * Delete activity profile in the LRS
1178
+ * @param {string} activityid the id of the Activity this profile is about
1179
+ * @param {string} profileid the id you want associated with this profile
1180
+ * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1181
+ * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1182
+ * @param {string} [callback] function to be called after the LRS responds
1183
+ * to this request (makes the call asynchronous)
1184
+ * the function will be passed the XMLHttpRequest object
1185
+ * @return {object} xhr response object or null if 404
1186
+ * @example
1187
+ * ADL.XAPIWrapper.deleteActivityProfile("http://adlnet.gov/expapi/activities/question",
1188
+ * "actprofile");
1189
+ * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
1190
+ */
1191
+ XAPIWrapper.prototype.deleteActivityProfile = async function (
1192
+ activityid,
1193
+ profileid,
1194
+ matchHash,
1195
+ noneMatchHash,
1196
+ callback
1197
+ ) {
1198
+ if (this.testConfig()) {
1199
+ let url =
1200
+ this.lrs.endpoint +
1201
+ 'activities/profile?activityId=<activity ID>&profileId=<profileid>'
1202
+
1203
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
1204
+ url = url.replace('<profileid>', encodeURIComponent(profileid))
1205
+
1206
+ let headers = null
1207
+ if (matchHash && noneMatchHash) {
1208
+ log("Can't have both If-Match and If-None-Match")
1209
+ } else if (matchHash) {
1210
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
1211
+ } else if (noneMatchHash) {
1212
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1213
+ }
1214
+
1215
+ let result = ADL.XHR_request(
1216
+ this.lrs,
1217
+ url,
1218
+ 'DELETE',
1219
+ null,
1220
+ this.lrs.auth,
1221
+ callback,
1222
+ null,
1223
+ false,
1224
+ headers,
1225
+ this.withCredentials,
1226
+ this.strictCallbacks
1227
+ )
1228
+
1229
+ if (result === undefined || result.status == 404) {
1230
+ return null
1231
+ }
1232
+
1233
+ try {
1234
+ return result[0].json()
1235
+ } catch (e) {
1236
+ return result
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ /*
1242
+ * Gets the Person object from the LRS based on an agent object.
1243
+ * The Person object may contain more information about an agent.
1244
+ * See the xAPI Spec for details.
1245
+ * @param {object} agent the agent object to get a Person
1246
+ * @param {function [callback] function to be called after the LRS responds
1247
+ * to this request (makes the call asynchronous)
1248
+ * the function will be passed the XMLHttpRequest object
1249
+ * @return {object} xhr response object or null if 404
1250
+ * @example
1251
+ * let res = ADL.XAPIWrapper.getAgents({"mbox":"mailto:tom@example.com"});
1252
+ * ADL.XAPIWrapper.log(res);
1253
+ * >> <Person object>
1254
+ */
1255
+ XAPIWrapper.prototype.getAgents = async function (agent, callback) {
1256
+ if (this.testConfig()) {
1257
+ let url = this.lrs.endpoint + 'agents?agent=<agent>'
1258
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1259
+
1260
+ let result = ADL.XHR_request(
1261
+ this.lrs,
1262
+ url,
1263
+ 'GET',
1264
+ null,
1265
+ this.lrs.auth,
1266
+ callback,
1267
+ null,
1268
+ true,
1269
+ null,
1270
+ this.withCredentials,
1271
+ this.strictCallbacks
1272
+ )
1273
+
1274
+ if (result === undefined || result.status == 404) {
1275
+ return null
1276
+ }
1277
+
1278
+ try {
1279
+ return result[0].json()
1280
+ } catch (e) {
1281
+ return result
1282
+ }
1283
+ }
1284
+ }
1285
+
1286
+ /*
1287
+ * Store agent profile in the LRS
1288
+ * @param {object} agent the agent this profile is related to
1289
+ * @param {string} profileid the id you want associated with this profile
1290
+ * @param {string} profileval the profile
1291
+ * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1292
+ * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1293
+ * @param {string} [callback] function to be called after the LRS responds
1294
+ * to this request (makes the call asynchronous)
1295
+ * the function will be passed the XMLHttpRequest object
1296
+ * @return {object} false if no agent profile is included
1297
+ * @example
1298
+ * let profile = {"info":"the agent profile"};
1299
+ * ADL.XAPIWrapper.sendAgentProfile({"mbox":"mailto:tom@example.com"},
1300
+ * "agentprofile", profile, null, "*");
1301
+ */
1302
+ XAPIWrapper.prototype.sendAgentProfile = function (
1303
+ agent,
1304
+ profileid,
1305
+ profileval,
1306
+ matchHash,
1307
+ noneMatchHash,
1308
+ callback
1309
+ ) {
1310
+ if (this.testConfig()) {
1311
+ let url =
1312
+ this.lrs.endpoint + 'agents/profile?agent=<agent>&profileId=<profileid>'
1313
+
1314
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1315
+ url = url.replace('<profileid>', encodeURIComponent(profileid))
1316
+
1317
+ let headers = null
1318
+ if (matchHash && noneMatchHash) {
1319
+ log("Can't have both If-Match and If-None-Match")
1320
+ } else if (matchHash) {
1321
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
1322
+ } else if (noneMatchHash) {
1323
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1324
+ }
1325
+
1326
+ let method = 'PUT'
1327
+ if (profileval) {
1328
+ if (profileval instanceof Array) {
1329
+ profileval = JSON.stringify(profileval)
1330
+ headers = headers || {}
1331
+ headers['Content-Type'] = 'application/json'
1332
+ } else if (profileval instanceof Object) {
1333
+ profileval = JSON.stringify(profileval)
1334
+ headers = headers || {}
1335
+ headers['Content-Type'] = 'application/json'
1336
+ method = 'POST'
1337
+ } else {
1338
+ headers = headers || {}
1339
+ headers['Content-Type'] = 'application/octet-stream'
1340
+ }
1341
+ } else {
1342
+ this.log('No agent profile was included.')
1343
+ return false
1344
+ }
1345
+
1346
+ ADL.XHR_request(
1347
+ this.lrs,
1348
+ url,
1349
+ method,
1350
+ profileval,
1351
+ this.lrs.auth,
1352
+ callback,
1353
+ null,
1354
+ false,
1355
+ headers,
1356
+ this.withCredentials,
1357
+ this.strictCallbacks
1358
+ )
1359
+ }
1360
+ }
1361
+
1362
+ /*
1363
+ * Get agnet profile from the LRS
1364
+ * @param {object} agent the agent associated with this profile
1365
+ * @param {string} [profileid] the id of the profile, if not included, the response will be a list of profileids
1366
+ * associated with the agent
1367
+ * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
1368
+ * @param {function} [callback] function to be called after the LRS responds
1369
+ * to this request (makes the call asynchronous)
1370
+ * the function will be passed the XMLHttpRequest object
1371
+ * @return {object} xhr response object or null if 404
1372
+ * @example
1373
+ * ADL.XAPIWrapper.getAgentProfile({"mbox":"mailto:tom@example.com"},
1374
+ * "agentprofile", null,
1375
+ * function(r){ADL.XAPIWrapper.log(JSON.parse(r.response));});
1376
+ * >> {info: "the agent profile"}
1377
+ */
1378
+ XAPIWrapper.prototype.getAgentProfile = async function (
1379
+ agent,
1380
+ profileid,
1381
+ since,
1382
+ callback
1383
+ ) {
1384
+ if (this.testConfig()) {
1385
+ let url = this.lrs.endpoint + 'agents/profile?agent=<agent>'
1386
+
1387
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1388
+ url = url.replace('<profileid>', encodeURIComponent(profileid))
1389
+
1390
+ if (profileid) {
1391
+ url += '&profileId=' + encodeURIComponent(profileid)
1392
+ }
1393
+
1394
+ if (since) {
1395
+ since = isDate(since)
1396
+ if (since != null) {
1397
+ url += '&since=' + encodeURIComponent(since.toISOString())
1398
+ }
1399
+ }
1400
+
1401
+ let result = ADL.XHR_request(
1402
+ this.lrs,
1403
+ url,
1404
+ 'GET',
1405
+ null,
1406
+ this.lrs.auth,
1407
+ callback,
1408
+ null,
1409
+ true,
1410
+ null,
1411
+ this.withCredentials,
1412
+ this.strictCallbacks
1413
+ )
1414
+
1415
+ if (result === undefined || result.status == 404) {
1416
+ return null
1417
+ }
1418
+
1419
+ try {
1420
+ return result[0].json()
1421
+ } catch (e) {
1422
+ return result.response
1423
+ }
1424
+ }
1425
+ }
1426
+
1427
+ /*
1428
+ * Delete agent profile in the LRS
1429
+ * @param {oject} agent the id of the Agent this profile is about
1430
+ * @param {string} profileid the id you want associated with this profile
1431
+ * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1432
+ * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1433
+ * @param {string} [callback] function to be called after the LRS responds
1434
+ * to this request (makes the call asynchronous)
1435
+ * the function will be passed the XMLHttpRequest object
1436
+ * @return {object} xhr response object or null if 404
1437
+ * @example
1438
+ * ADL.XAPIWrapper.deleteAgentProfile({"mbox":"mailto:tom@example.com"},
1439
+ * "agentprofile");
1440
+ * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
1441
+ */
1442
+ XAPIWrapper.prototype.deleteAgentProfile = async function (
1443
+ agent,
1444
+ profileid,
1445
+ matchHash,
1446
+ noneMatchHash,
1447
+ callback
1448
+ ) {
1449
+ if (this.testConfig()) {
1450
+ let url =
1451
+ this.lrs.endpoint + 'agents/profile?agent=<agent>&profileId=<profileid>'
1452
+
1453
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1454
+ url = url.replace('<profileid>', encodeURIComponent(profileid))
1455
+
1456
+ let headers = null
1457
+ if (matchHash && noneMatchHash) {
1458
+ log("Can't have both If-Match and If-None-Match")
1459
+ } else if (matchHash) {
1460
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
1461
+ } else if (noneMatchHash) {
1462
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1463
+ }
1464
+
1465
+ let result = ADL.XHR_request(
1466
+ this.lrs,
1467
+ url,
1468
+ 'DELETE',
1469
+ null,
1470
+ this.lrs.auth,
1471
+ callback,
1472
+ null,
1473
+ false,
1474
+ headers,
1475
+ this.withCredentials,
1476
+ this.strictCallbacks
1477
+ )
1478
+
1479
+ if (result === undefined || result.status == 404) {
1480
+ return null
1481
+ }
1482
+
1483
+ try {
1484
+ return result[0].json()
1485
+ } catch (e) {
1486
+ return result
1487
+ }
1488
+ }
1489
+ }
1490
+
1491
+ /*
1492
+ * Tests the configuration of the lrs object
1493
+ */
1494
+ function testConfig() {
1495
+ try {
1496
+ return this.lrs.endpoint != undefined && this.lrs.endpoint != ''
1497
+ } catch (e) {
1498
+ return false
1499
+ }
1500
+ }
1501
+
1502
+ // outputs the message to the console if available
1503
+ function log(message) {
1504
+ if (!log.debug) return false
1505
+ try {
1506
+ message
1507
+ return true
1508
+ } catch (e) {
1509
+ return false
1510
+ }
1511
+ }
1512
+
1513
+ // merges two object
1514
+ function mergeRecursive(obj1, obj2) {
1515
+ for (let p in obj2) {
1516
+ if (obj2.hasOwnProperty(p) == false) continue
1517
+
1518
+ let prop = obj2[p]
1519
+ log(p + ' : ' + prop)
1520
+ try {
1521
+ // Property in destination object set; update its value.
1522
+ if (obj2[p].constructor == Object) {
1523
+ obj1[p] = mergeRecursive(obj1[p], obj2[p])
1524
+ } else {
1525
+ if (obj1 == undefined) {
1526
+ obj1 = new Object()
1527
+ }
1528
+ obj1[p] = obj2[p]
1529
+ }
1530
+ } catch (e) {
1531
+ if (obj1 == undefined) {
1532
+ obj1 = new Object()
1533
+ }
1534
+ // Property in destination object not set; create it and set its value.
1535
+ obj1[p] = obj2[p]
1536
+ }
1537
+ }
1538
+
1539
+ return obj1
1540
+ }
1541
+
1542
+ // iniitializes an lrs object with settings from
1543
+ // a config file and from the url query string
1544
+ function getLRSObject(config) {
1545
+ let lrsProps = [
1546
+ 'endpoint',
1547
+ 'auth',
1548
+ 'actor',
1549
+ 'registration',
1550
+ 'activity_id',
1551
+ 'grouping',
1552
+ 'activity_platform'
1553
+ ]
1554
+ let lrs = new Object()
1555
+ let qsVars, prop
1556
+
1557
+ qsVars = parseQueryString()
1558
+ if (qsVars !== undefined && Object.keys(qsVars).length !== 0) {
1559
+ for (let i = 0; i < lrsProps.length; i++) {
1560
+ prop = lrsProps[i]
1561
+ if (qsVars[prop]) {
1562
+ lrs[prop] = qsVars[prop]
1563
+ delete qsVars[prop]
1564
+ }
1565
+ }
1566
+ // if (Object.keys(qsVars).length !== 0) {
1567
+ // lrs.extended = qsVars;
1568
+ // }
1569
+
1570
+ lrs = mergeRecursive(config, lrs)
1571
+ } else {
1572
+ lrs = config
1573
+ }
1574
+
1575
+ return lrs
1576
+ }
1577
+
1578
+ // parses the params in the url query string
1579
+ function parseQueryString() {
1580
+ let qs, pairs, pair, ii, parsed
1581
+
1582
+ qs = window.location.search.substring(1)
1583
+
1584
+ pairs = qs.split('&')
1585
+ parsed = {}
1586
+ for (ii = 0; ii < pairs.length; ii++) {
1587
+ pair = pairs[ii].split('=')
1588
+ if (pair.length === 2 && pair[0]) {
1589
+ parsed[pair[0]] = decodeURIComponent(pair[1])
1590
+ }
1591
+ }
1592
+
1593
+ return parsed
1594
+ }
1595
+
1596
+ function delay() {
1597
+ let xhr = new XMLHttpRequest()
1598
+ let url = window.location + '?forcenocache=' + ADL.ruuid()
1599
+ xhr.open('GET', url, false)
1600
+ xhr.send(null)
1601
+ }
1602
+
1603
+ /*
1604
+ * formats a request in a way that IE will allow
1605
+ * @param {string} method the http request method (ex: "PUT", "GET")
1606
+ * @param {string} url the url to the request (ex: ADL.XAPIWrapper.lrs.endpoint + "statements")
1607
+ * @param {array} [headers] headers to include in the request
1608
+ * @param {string} [data] the body of the request, if there is one
1609
+ * @return {object} xhr response object
1610
+ */
1611
+ // function ie_request(method, url, headers, data) {
1612
+ // let newUrl = url
1613
+
1614
+ // //Everything that was on query string goes into form vars
1615
+ // let formData = new Array()
1616
+ // let qsIndex = newUrl.indexOf('?')
1617
+ // if (qsIndex > 0) {
1618
+ // formData.push(newUrl.substring(qsIndex + 1))
1619
+ // newUrl = newUrl.substring(0, qsIndex)
1620
+ // }
1621
+
1622
+ // //Method has to go on querystring, and nothing else
1623
+ // newUrl = newUrl + '?method=' + method
1624
+
1625
+ // //Headers
1626
+ // if (headers !== null) {
1627
+ // for (let headerName in headers) {
1628
+ // if (headers.hasOwnProperty(headerName))
1629
+ // formData.push(
1630
+ // headerName + '=' + encodeURIComponent(headers[headerName])
1631
+ // )
1632
+ // }
1633
+ // }
1634
+
1635
+ // //The original data is repackaged as "content" form var
1636
+ // if (data !== null) {
1637
+ // formData.push('content=' + encodeURIComponent(data))
1638
+ // }
1639
+
1640
+ // return {
1641
+ // method: 'POST',
1642
+ // url: newUrl,
1643
+ // headers: {},
1644
+ // data: formData.join('&')
1645
+ // }
1646
+ // }
1647
+
1648
+ /*!
1649
+ Excerpt from: Math.uuid.js (v1.4)
1650
+ http://www.broofa.com
1651
+ mailto:robert@broofa.com
1652
+ Copyright (c) 2010 Robert Kieffer
1653
+ Dual licensed under the MIT and GPL licenses.
1654
+ */
1655
+ ADL.ruuid = function () {
1656
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
1657
+ /[xy]/g,
1658
+ function (c) {
1659
+ let r = (Math.random() * 16) | 0,
1660
+ v = c == 'x' ? r : (r & 0x3) | 0x8
1661
+ return v.toString(16)
1662
+ }
1663
+ )
1664
+ }
1665
+
1666
+ /*
1667
+ * dateFromISOString
1668
+ * parses an ISO string into a date object
1669
+ * isostr - the ISO string
1670
+ */
1671
+ ADL.dateFromISOString = function (isostr) {
1672
+ let regexp =
1673
+ '([0-9]{4})(-([0-9]{2})(-([0-9]{2})' +
1674
+ '([T| ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(.([0-9]+))?)?' +
1675
+ '(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?'
1676
+ let d = isostr.match(new RegExp(regexp))
1677
+
1678
+ let offset = 0
1679
+ let date = new Date(d[1], 0, 1)
1680
+
1681
+ if (d[3]) {
1682
+ date.setMonth(d[3] - 1)
1683
+ }
1684
+ if (d[5]) {
1685
+ date.setDate(d[5])
1686
+ }
1687
+ if (d[7]) {
1688
+ date.setHours(d[7])
1689
+ }
1690
+ if (d[8]) {
1691
+ date.setMinutes(d[8])
1692
+ }
1693
+ if (d[10]) {
1694
+ date.setSeconds(d[10])
1695
+ }
1696
+ if (d[12]) {
1697
+ date.setMilliseconds(Number('0.' + d[12]) * 1000)
1698
+ }
1699
+ if (d[14]) {
1700
+ offset = Number(d[16]) * 60 + Number(d[17])
1701
+ offset *= d[15] == '-' ? 1 : -1
1702
+ }
1703
+
1704
+ offset -= date.getTimezoneOffset()
1705
+ let time = Number(date) + offset * 60 * 1000
1706
+
1707
+ let dateToReturn = new Date()
1708
+ dateToReturn.setTime(Number(time))
1709
+ return dateToReturn
1710
+ }
1711
+
1712
+ // Synchronous if callback is not provided (not recommended)
1713
+ /*
1714
+ * makes a request to a server (if possible, use functions provided in XAPIWrapper)
1715
+ * @param {string} lrs the lrs connection info, such as endpoint, auth, etc
1716
+ * @param {string | Array } urls the url of this request
1717
+ * @param {string} method the http request method
1718
+ * @param {string} data the payload
1719
+ * @param {string} auth the value for the Authorization header
1720
+ * @param {function} callback function to be called after the LRS responds
1721
+ * to this request (makes the call asynchronous)
1722
+ * @param {object} [callbackargs] additional javascript object to be passed to the callback function
1723
+ * @param {boolean} ignore404 allow page not found errors to pass
1724
+ * @param {object} extraHeaders other header key-values to be added to this request
1725
+ * @param {boolean} withCredentials
1726
+ * @param {boolean} strictCallbacks Callback must be executed and first param is error or null if no error
1727
+ * @return {Objec} containing the status of last promise request and Array of the responses;
1728
+ */
1729
+ ADL.XHR_request = function (
1730
+ lrs,
1731
+ urls,
1732
+ method,
1733
+ data,
1734
+ auth,
1735
+ callback,
1736
+ callbackargs,
1737
+ ignore404,
1738
+ extraHeaders,
1739
+ withCredentials,
1740
+ strictCallbacks
1741
+ ) {
1742
+ 'use strict'
1743
+ let URLList = urls.constructor == Array ? [...urls] : [urls]
1744
+ // Consolidate headers
1745
+ let headers = {
1746
+ 'Content-Type': 'application/json',
1747
+ Authorization: auth,
1748
+ 'X-Experience-API-Version': ADL.XAPIWrapper.xapiVersion
1749
+ }
1750
+
1751
+ if (extraHeaders !== null) {
1752
+ for (let headerName in extraHeaders) {
1753
+ if (extraHeaders.hasOwnProperty(headerName)) {
1754
+ headers[headerName] = extraHeaders[headerName]
1755
+ }
1756
+ }
1757
+ }
1758
+
1759
+ // Add extended LMS-specified values to the URL
1760
+ if (lrs !== null && lrs.extended !== undefined) {
1761
+ let extended = []
1762
+ for (let prop in lrs.extended) {
1763
+ extended.push(prop + '=' + encodeURIComponent(lrs.extended[prop]))
1764
+ }
1765
+ if (extended.length > 0) {
1766
+ URLList.map(
1767
+ (url) =>
1768
+ (url += (url.indexOf('?') > -1 ? '&' : '?') + extended.join('&'))
1769
+ )
1770
+ }
1771
+ }
1772
+ let requestArray = []
1773
+
1774
+ for (const url of URLList) {
1775
+ // Prepare request options for fetch
1776
+ const fetchOptions = {
1777
+ method: method,
1778
+ headers: headers,
1779
+ body: data ? JSON.stringify(data) : null,
1780
+ credentials: withCredentials ? 'include' : 'same-origin'
1781
+ }
1782
+ requestArray.push(fetch(url, fetchOptions))
1783
+ }
1784
+
1785
+ // If no callback, we assume synchronous mode (using Promise)
1786
+ const makeRequest = async (requests) => {
1787
+ try {
1788
+ const response = await Promise.all(requests)
1789
+ const status = response[response.length - 1].status
1790
+ let body
1791
+
1792
+ if (status === 404 && ignore404) {
1793
+ // If we are ignoring 404s
1794
+ body = null
1795
+ } else if (status >= 200 && status < 400) {
1796
+ // Successful response
1797
+ body = await response
1798
+ } else {
1799
+ throw new Error(
1800
+ `There was a problem communicating with the Learning Record Store. (${status} | ${response.statusText}) ${urls}`
1801
+ )
1802
+ }
1803
+
1804
+ // Callback handling
1805
+ if (callback) {
1806
+ if (strictCallbacks) {
1807
+ callback(null, response, body, status)
1808
+ } else {
1809
+ callback(response, body, status)
1810
+ }
1811
+ }
1812
+ return { response, status }
1813
+ } catch (error) {
1814
+ // Log error to the console or call error handler
1815
+ ADL.XAPIWrapper.log(error.toString())
1816
+ if (callback) {
1817
+ if (strictCallbacks) {
1818
+ callback(error, null, callbackargs)
1819
+ } else {
1820
+ callback(null, callbackargs)
1821
+ }
1822
+ }
1823
+ return null
1824
+ }
1825
+ }
1826
+
1827
+ if (!callback) {
1828
+ // If no callback, we return the Promise (synchronous behavior)
1829
+ return makeRequest(requestArray)
1830
+ }
1831
+
1832
+ // If there's a callback, proceed with asynchronous request
1833
+ makeRequest(requestArray)
1834
+ }
1835
+
1836
+ //====================================================================
1837
+
1838
+ /*
1839
+ * Holder for custom global error callback
1840
+ * @param {object} xhr xhr object or null
1841
+ * @param {string} method XMLHttpRequest request method
1842
+ * @param {string} url full endpoint url
1843
+ * @param {function} callback function to be called after the LRS responds
1844
+ * to this request (makes the call asynchronous)
1845
+ * @param {object} [callbackargs] additional javascript object to be passed to the callback function
1846
+ * @param {boolean} strictCallbacks Callback must be executed and first param is error or null if no error
1847
+ * @example
1848
+ * ADL.xhrRequestOnError = function(xhr, method, url, callback, callbackargs) {
1849
+ * console.log(xhr);
1850
+ * alert(xhr.status + " " + xhr.statusText + ": " + xhr.response);
1851
+ * };
1852
+ */
1853
+ ADL.xhrRequestOnError = function (
1854
+ xhr,
1855
+ method,
1856
+ url,
1857
+ callback,
1858
+ callbackargs,
1859
+ strictCallbacks
1860
+ ) {
1861
+ if (callback && strictCallbacks) {
1862
+ let status = xhr ? xhr.status : undefined
1863
+ let error
1864
+ if (status) {
1865
+ error = new Error('Request error: ' + xhr.status)
1866
+ } else if (status === 0 || status === null) {
1867
+ // 0 and null = aborted
1868
+ error = new Error('Request error: aborted')
1869
+ } else {
1870
+ error = new Error('Reqeust error: unknown')
1871
+ }
1872
+
1873
+ if (callbackargs) {
1874
+ callback(error, xhr, callbackargs)
1875
+ } else {
1876
+ let body
1877
+
1878
+ try {
1879
+ body = JSON.parse(xhr.responseText)
1880
+ } catch (e) {
1881
+ body = xhr.responseText
1882
+ }
1883
+
1884
+ callback(error, xhr, body)
1885
+ }
1886
+ }
1887
+ }
1888
+
1889
+ ADL.formatHash = function (hash) {
1890
+ return hash === '*' ? hash : '"' + hash + '"'
1891
+ }
1892
+
1893
+ ADL.XAPIWrapper = new XAPIWrapper(Config, false)
1894
+ //============================= END ==========================================
1895
+ }