fcad-core-dragon 2.0.0-beta.1 → 2.0.0-beta.3

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 (118) hide show
  1. package/.editorconfig +33 -33
  2. package/.eslintignore +29 -29
  3. package/{.eslintrc.js → .eslintrc.cjs} +81 -86
  4. package/CHANGELOG +364 -364
  5. package/README.md +71 -71
  6. package/bk.scss +117 -0
  7. package/package.json +61 -63
  8. package/src/$locales/en.json +143 -179
  9. package/src/$locales/fr.json +105 -181
  10. package/src/assets/data/onboardingMessages.json +47 -47
  11. package/src/components/AppBase.vue +1054 -614
  12. package/src/components/AppBaseButton.vue +87 -63
  13. package/src/components/AppBaseErrorDisplay.vue +438 -420
  14. package/src/components/AppBaseFlipCard.vue +84 -83
  15. package/src/components/AppBaseModule.vue +1673 -1842
  16. package/src/components/AppBasePage.vue +779 -312
  17. package/src/components/AppBasePopover.vue +41 -0
  18. package/src/components/AppCompAudio.vue +234 -0
  19. package/src/components/AppCompBranchButtons.vue +552 -582
  20. package/src/components/AppCompButtonProgress.vue +126 -147
  21. package/src/components/AppCompCarousel.vue +298 -192
  22. package/src/components/AppCompInputCheckBoxNext.vue +195 -0
  23. package/src/components/AppCompInputDropdownNext.vue +159 -0
  24. package/src/components/AppCompInputRadioNext.vue +152 -0
  25. package/src/components/{AppCompInputTextBox.vue → AppCompInputTextNext.vue} +106 -91
  26. package/src/components/AppCompInputTextTableNext.vue +141 -0
  27. package/src/components/AppCompInputTextToFillDropdownNext.vue +230 -0
  28. package/src/components/{AppCompInputTextToFillText.vue → AppCompInputTextToFillNext.vue} +171 -164
  29. package/src/components/AppCompJauge.vue +74 -55
  30. package/src/components/AppCompMenu.vue +413 -209
  31. package/src/components/AppCompMenuItem.vue +228 -174
  32. package/src/components/AppCompNavigation.vue +960 -949
  33. package/src/components/AppCompNoteCall.vue +133 -126
  34. package/src/components/AppCompNoteCredit.vue +292 -164
  35. package/src/components/AppCompPlayBar.vue +1218 -1319
  36. package/src/components/AppCompPlayBarNext.vue +2052 -0
  37. package/src/components/AppCompPlayBarProgress.vue +82 -0
  38. package/src/components/AppCompPopUpNext.vue +503 -0
  39. package/src/components/{AppCompQuiz.vue → AppCompQuizNext.vue} +2904 -2989
  40. package/src/components/AppCompQuizRecall.vue +276 -250
  41. package/src/components/AppCompSVGNext.vue +347 -0
  42. package/src/components/AppCompSettingsMenu.vue +172 -171
  43. package/src/components/AppCompTableOfContent.vue +387 -264
  44. package/src/components/AppCompTranscript.vue +24 -19
  45. package/src/components/AppCompVideoPlayer.vue +368 -336
  46. package/src/components/AppCompViewDisplay.vue +6 -6
  47. package/src/components/BaseModule.vue +72 -67
  48. package/src/composables/useQuiz.js +206 -0
  49. package/src/externalComps/ModuleView.vue +22 -0
  50. package/src/externalComps/SummaryView.vue +91 -0
  51. package/src/main.js +272 -227
  52. package/src/mixins/$mediaMixins.js +819 -0
  53. package/src/mixins/timerMixin.js +155 -156
  54. package/src/module/stores/appStore.js +893 -0
  55. package/src/module/xapi/ADL.js +376 -339
  56. package/src/module/xapi/Crypto/Hasher.js +241 -241
  57. package/src/module/xapi/Crypto/WordArray.js +278 -278
  58. package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +103 -103
  59. package/src/module/xapi/Crypto/algorithms/C_algo.js +315 -319
  60. package/src/module/xapi/Crypto/algorithms/HMAC.js +9 -9
  61. package/src/module/xapi/Crypto/algorithms/SHA1.js +9 -9
  62. package/src/module/xapi/Crypto/encoders/Base.js +105 -105
  63. package/src/module/xapi/Crypto/encoders/Base64.js +99 -99
  64. package/src/module/xapi/Crypto/encoders/Hex.js +61 -61
  65. package/src/module/xapi/Crypto/encoders/Latin1.js +61 -61
  66. package/src/module/xapi/Crypto/encoders/Utf8.js +45 -45
  67. package/src/module/xapi/Crypto/index.js +53 -53
  68. package/src/module/xapi/Statement/activity.js +47 -47
  69. package/src/module/xapi/Statement/agent.js +55 -55
  70. package/src/module/xapi/Statement/group.js +26 -26
  71. package/src/module/xapi/Statement/index.js +259 -259
  72. package/src/module/xapi/Statement/statement.js +253 -253
  73. package/src/module/xapi/Statement/statementRef.js +23 -23
  74. package/src/module/xapi/Statement/substatement.js +22 -22
  75. package/src/module/xapi/Statement/verb.js +36 -36
  76. package/src/module/xapi/activitytypes.js +17 -17
  77. package/src/module/xapi/launch.js +157 -157
  78. package/src/module/xapi/utils.js +167 -167
  79. package/src/module/xapi/verbs.js +294 -294
  80. package/src/module/xapi/wrapper.js +1963 -1890
  81. package/src/module/xapi/xapiStatement.js +444 -444
  82. package/src/plugins/bus.js +8 -3
  83. package/src/plugins/gsap.js +14 -17
  84. package/src/plugins/helper.js +308 -295
  85. package/src/plugins/i18n.js +44 -31
  86. package/src/plugins/idb.js +219 -212
  87. package/src/plugins/save.js +37 -37
  88. package/src/plugins/scorm.js +287 -287
  89. package/src/plugins/xapi.js +11 -11
  90. package/src/public/index.html +33 -21
  91. package/src/router/index.js +43 -41
  92. package/src/router/routes.js +312 -337
  93. package/src/shared/generalfuncs.js +210 -188
  94. package/src/shared/validators.js +1069 -249
  95. package/vite.config.js +27 -0
  96. package/.prettierrc.js +0 -5
  97. package/babel.config.js +0 -3
  98. package/src/components/AppBaseDragChoice.vue +0 -91
  99. package/src/components/AppBaseDropZone.vue +0 -112
  100. package/src/components/AppCompBif.vue +0 -120
  101. package/src/components/AppCompDragAndDrop.vue +0 -339
  102. package/src/components/AppCompInputAssociation.vue +0 -332
  103. package/src/components/AppCompInputCheckBox.vue +0 -227
  104. package/src/components/AppCompInputDropdown.vue +0 -184
  105. package/src/components/AppCompInputRadio.vue +0 -169
  106. package/src/components/AppCompInputTextTable.vue +0 -155
  107. package/src/components/AppCompInputTextToFillDropdown.vue +0 -255
  108. package/src/components/AppCompMediaPlayer.vue +0 -397
  109. package/src/components/AppCompPopUp.vue +0 -522
  110. package/src/components/AppCompPopover.vue +0 -27
  111. package/src/components/AppCompSVG.vue +0 -309
  112. package/src/mixins/$pageMixins.js +0 -459
  113. package/src/mixins/$quizMixins.js +0 -456
  114. package/src/module/store.js +0 -895
  115. package/src/plugins/timeManager.js +0 -77
  116. package/src/routes_bckp.js +0 -313
  117. package/src/routes_static.js +0 -344
  118. package/vue.config.js +0 -83
@@ -1,1890 +1,1963 @@
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 = JSON.parse(r.response)
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, 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
- * Get statement(s) based on the searchparams or more url.
605
- * @param {object} searchparams an ADL.XAPIWrapper.searchParams object of
606
- * key(search parameter)-value(parameter value) pairs.
607
- * Example:
608
- * let myparams = ADL.XAPIWrapper.searchParams();
609
- * myparams['verb'] = ADL.verbs.completed.id;
610
- * let completedStmts = ADL.XAPIWrapper.getStatements(myparams);
611
- * @param {string} more the more url found in the StatementResults object, if there are more
612
- * statements available based on your get statements request. Pass the
613
- * more url as this parameter to retrieve those statements.
614
- * @param {function} [callback] - function to be called after the LRS responds
615
- * to this request (makes the call asynchronous)
616
- * the function will be passed the XMLHttpRequest object
617
- * @return {object} xhr response object or null if 404
618
- * @example
619
- * let ret = ADL.XAPIWrapper.getStatements();
620
- * if (ret)
621
- * ADL.XAPIWrapper.log(ret.statements);
622
- *
623
- * >> <Array of statements>
624
- */
625
- XAPIWrapper.prototype.getStatements = function(searchparams, more, callback) {
626
- if (this.testConfig()) {
627
- let url = this.lrs.endpoint + 'statements'
628
- if (more) {
629
- url = this.base + more
630
- } else {
631
- let urlparams = new Array()
632
-
633
- for (let s in searchparams) {
634
- if (searchparams.hasOwnProperty(s)) {
635
- if (s == 'until' || s == 'since') {
636
- let d = new Date(searchparams[s])
637
- urlparams.push(s + '=' + encodeURIComponent(d.toISOString()))
638
- } else {
639
- urlparams.push(s + '=' + encodeURIComponent(searchparams[s]))
640
- }
641
- }
642
- }
643
- if (urlparams.length > 0) url = url + '?' + urlparams.join('&')
644
- }
645
-
646
- let res = ADL.XHR_request(
647
- this.lrs,
648
- url,
649
- 'GET',
650
- null,
651
- this.lrs.auth,
652
- callback,
653
- null,
654
- false,
655
- null,
656
- this.withCredentials,
657
- this.strictCallbacks
658
- )
659
-
660
- if (res === undefined || res.status == 404) {
661
- return null
662
- }
663
-
664
- try {
665
- return JSON.parse(res.response)
666
- } catch (e) {
667
- return res.response
668
- }
669
- }
670
- }
671
-
672
- /*
673
- * Gets the Activity object from the LRS.
674
- * @param {string} activityid the id of the Activity to get
675
- * @param {function} [callback] function to be called after the LRS responds
676
- * to this request (makes the call asynchronous)
677
- * the function will be passed the XMLHttpRequest object
678
- * @return {object} xhr response object or null if 404
679
- * @example
680
- * let res = ADL.XAPIWrapper.getActivities("http://adlnet.gov/expapi/activities/question");
681
- * ADL.XAPIWrapper.log(res);
682
- * >> <Activity object>
683
- */
684
- XAPIWrapper.prototype.getActivities = function(activityid, callback) {
685
- if (this.testConfig()) {
686
- let url = this.lrs.endpoint + 'activities?activityId=<activityid>'
687
- url = url.replace('<activityid>', encodeURIComponent(activityid))
688
-
689
- let result = ADL.XHR_request(
690
- this.lrs,
691
- url,
692
- 'GET',
693
- null,
694
- this.lrs.auth,
695
- callback,
696
- null,
697
- true,
698
- null,
699
- this.withCredentials,
700
- this.strictCallbacks
701
- )
702
-
703
- if (result === undefined || result.status == 404) {
704
- return null
705
- }
706
-
707
- try {
708
- return JSON.parse(result.response)
709
- } catch (e) {
710
- return result.response
711
- }
712
- }
713
- }
714
-
715
- /*
716
- * Store activity state in the LRS
717
- * @param {string} activityid the id of the Activity this state is about
718
- * @param {object} agent the agent this Activity state is related to
719
- * @param {string} stateid the id you want associated with this state
720
- * @param {string} [registration] the registraton id associated with this state
721
- * @param {string} stateval the state
722
- * @param {string} [matchHash] the hash of the state to replace or * to replace any
723
- * @param {string} [noneMatchHash] the hash of the current state or * to indicate no previous state
724
- * @param {function} [callback] function to be called after the LRS responds
725
- * to this request (makes the call asynchronous)
726
- * the function will be passed the XMLHttpRequest object
727
- * @return {boolean} false if no activity state is included
728
- * @example
729
- * let stateval = {"info":"the state info"};
730
- * ADL.XAPIWrapper.sendState("http://adlnet.gov/expapi/activities/question",
731
- * {"mbox":"mailto:tom@example.com"},
732
- * "questionstate", null, stateval);
733
- */
734
- XAPIWrapper.prototype.sendState = function(
735
- activityid,
736
- agent,
737
- stateid,
738
- registration,
739
- stateval,
740
- matchHash,
741
- noneMatchHash,
742
- callback
743
- ) {
744
- if (this.testConfig()) {
745
- let url =
746
- this.lrs.endpoint +
747
- 'activities/state?activityId=<activity ID>&agent=<agent>&stateId=<stateid>'
748
-
749
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
750
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
751
- url = url.replace('<stateid>', encodeURIComponent(stateid))
752
-
753
- if (registration) {
754
- url += '&registration=' + encodeURIComponent(registration)
755
- }
756
-
757
- let headers = null
758
- if (matchHash && noneMatchHash) {
759
- log("Can't have both If-Match and If-None-Match")
760
- } else if (matchHash) {
761
- headers = { 'If-Match': ADL.formatHash(matchHash) }
762
- } else if (noneMatchHash) {
763
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
764
- }
765
-
766
- let method = 'PUT'
767
- if (stateval) {
768
- if (stateval instanceof Array) {
769
- stateval = JSON.stringify(stateval)
770
- headers = headers || {}
771
- headers['Content-Type'] = 'application/json'
772
- } else if (stateval instanceof Object) {
773
- stateval = JSON.stringify(stateval)
774
- headers = headers || {}
775
- headers['Content-Type'] = 'application/json'
776
- method = 'POST'
777
- } else {
778
- headers = headers || {}
779
- headers['Content-Type'] = 'application/octet-stream'
780
- }
781
- } else {
782
- this.log('No activity state was included.')
783
- return false
784
- }
785
- //(lrs, url, method, data, auth, callback, callbackargs, ignore404, extraHeaders)
786
-
787
- ADL.XHR_request(
788
- this.lrs,
789
- url,
790
- method,
791
- stateval,
792
- this.lrs.auth,
793
- callback,
794
- null,
795
- null,
796
- headers,
797
- this.withCredentials,
798
- this.strictCallbacks
799
- )
800
- }
801
- }
802
-
803
- /*
804
- * Get activity state from the LRS
805
- * @param {string} activityid the id of the Activity this state is about
806
- * @param {object} agent the agent this Activity state is related to
807
- * @param {string} [stateid] the id of the state, if not included, the response will be a list of stateids
808
- * associated with the activity and agent)
809
- * @param {string} [registration] the registraton id associated with this state
810
- * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
811
- * @param {function} [callback] function to be called after the LRS responds
812
- * to this request (makes the call asynchronous)
813
- * the function will be passed the XMLHttpRequest object
814
- * @return {object} xhr response object or null if 404
815
- * @example
816
- * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
817
- * {"mbox":"mailto:tom@example.com"}, "questionstate");
818
- * >> {info: "the state info"}
819
- */
820
- XAPIWrapper.prototype.getState = function(
821
- activityid,
822
- agent,
823
- stateid,
824
- registration,
825
- since,
826
- callback
827
- ) {
828
- if (this.testConfig()) {
829
- let url =
830
- this.lrs.endpoint +
831
- 'activities/state?activityId=<activity ID>&agent=<agent>'
832
-
833
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
834
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
835
-
836
- if (stateid) {
837
- url += '&stateId=' + encodeURIComponent(stateid)
838
- }
839
-
840
- if (registration) {
841
- url += '&registration=' + encodeURIComponent(registration)
842
- }
843
-
844
- if (since) {
845
- since = isDate(since)
846
- if (since != null) {
847
- url += '&since=' + encodeURIComponent(since.toISOString())
848
- }
849
- }
850
-
851
- let result = ADL.XHR_request(
852
- this.lrs,
853
- url,
854
- 'GET',
855
- null,
856
- this.lrs.auth,
857
- callback,
858
- null,
859
- true,
860
- null,
861
- this.withCredentials,
862
- this.strictCallbacks
863
- )
864
-
865
- if (result === undefined || result.status == 404) {
866
- return null
867
- }
868
-
869
- try {
870
- return JSON.parse(result.response)
871
- } catch (e) {
872
- return result.response
873
- }
874
- }
875
- }
876
-
877
- /*
878
- * Delete activity state in the LRS
879
- * @param {string} activityid the id of the Activity this state is about
880
- * @param {object} agent the agent this Activity state is related to
881
- * @param {string} stateid the id you want associated with this state
882
- * @param {string} [registration] the registraton id associated with this state
883
- * @param {string} [matchHash] the hash of the state to replace or * to replace any
884
- * @param {string} [noneMatchHash] the hash of the current state or * to indicate no previous state
885
- * @param {string} [callback] function to be called after the LRS responds
886
- * to this request (makes the call asynchronous)
887
- * the function will be passed the XMLHttpRequest object
888
- * @return {object} xhr response object or null if 404
889
- * @example
890
- * let stateval = {"info":"the state info"};
891
- * ADL.XAPIWrapper.sendState("http://adlnet.gov/expapi/activities/question",
892
- * {"mbox":"mailto:tom@example.com"},
893
- * "questionstate", null, stateval);
894
- * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
895
- * {"mbox":"mailto:tom@example.com"}, "questionstate");
896
- * >> {info: "the state info"}
897
- *
898
- * ADL.XAPIWrapper.deleteState("http://adlnet.gov/expapi/activities/question",
899
- * {"mbox":"mailto:tom@example.com"}, "questionstate");
900
- * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
901
- *
902
- * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
903
- * {"mbox":"mailto:tom@example.com"}, "questionstate");
904
- * >> 404
905
- */
906
- XAPIWrapper.prototype.deleteState = function(
907
- activityid,
908
- agent,
909
- stateid,
910
- registration,
911
- matchHash,
912
- noneMatchHash,
913
- callback
914
- ) {
915
- if (this.testConfig()) {
916
- let url =
917
- this.lrs.endpoint +
918
- 'activities/state?activityId=<activity ID>&agent=<agent>&stateId=<stateid>'
919
-
920
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
921
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
922
- url = url.replace('<stateid>', encodeURIComponent(stateid))
923
-
924
- if (registration) {
925
- url += '&registration=' + encodeURIComponent(registration)
926
- }
927
-
928
- let headers = null
929
- if (matchHash && noneMatchHash) {
930
- log("Can't have both If-Match and If-None-Match")
931
- } else if (matchHash) {
932
- headers = { 'If-Match': ADL.formatHash(matchHash) }
933
- } else if (noneMatchHash) {
934
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
935
- }
936
-
937
- let result = ADL.XHR_request(
938
- this.lrs,
939
- url,
940
- 'DELETE',
941
- null,
942
- this.lrs.auth,
943
- callback,
944
- null,
945
- false,
946
- headers,
947
- this.withCredentials,
948
- this.strictCallbacks
949
- )
950
-
951
- if (result === undefined || result.status == 404) {
952
- return null
953
- }
954
-
955
- try {
956
- return JSON.parse(result.response)
957
- } catch (e) {
958
- return result
959
- }
960
- }
961
- }
962
-
963
- /*
964
- * Store activity profile in the LRS
965
- * @param {string} activityid the id of the Activity this profile is about
966
- * @param {string} profileid the id you want associated with this profile
967
- * @param {string} profileval the profile
968
- * @param {string} [matchHash] the hash of the profile to replace or * to replace any
969
- * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
970
- * @param {string} [callback] function to be called after the LRS responds
971
- * to this request (makes the call asynchronous)
972
- * the function will be passed the XMLHttpRequest object
973
- * @return {bolean} false if no activity profile is included
974
- * @example
975
- * let profile = {"info":"the profile"};
976
- * ADL.XAPIWrapper.sendActivityProfile("http://adlnet.gov/expapi/activities/question",
977
- * "actprofile", profile, null, "*");
978
- */
979
- XAPIWrapper.prototype.sendActivityProfile = function(
980
- activityid,
981
- profileid,
982
- profileval,
983
- matchHash,
984
- noneMatchHash,
985
- callback
986
- ) {
987
- if (this.testConfig()) {
988
- let url =
989
- this.lrs.endpoint +
990
- 'activities/profile?activityId=<activity ID>&profileId=<profileid>'
991
-
992
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
993
- url = url.replace('<profileid>', encodeURIComponent(profileid))
994
-
995
- let headers = null
996
- if (matchHash && noneMatchHash) {
997
- log("Can't have both If-Match and If-None-Match")
998
- } else if (matchHash) {
999
- headers = { 'If-Match': ADL.formatHash(matchHash) }
1000
- } else if (noneMatchHash) {
1001
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1002
- }
1003
-
1004
- let method = 'PUT'
1005
- if (profileval) {
1006
- if (profileval instanceof Array) {
1007
- profileval = JSON.stringify(profileval)
1008
- headers = headers || {}
1009
- headers['Content-Type'] = 'application/json'
1010
- } else if (profileval instanceof Object) {
1011
- profileval = JSON.stringify(profileval)
1012
- headers = headers || {}
1013
- headers['Content-Type'] = 'application/json'
1014
- method = 'POST'
1015
- } else {
1016
- headers = headers || {}
1017
- headers['Content-Type'] = 'application/octet-stream'
1018
- }
1019
- } else {
1020
- this.log('No activity profile was included.')
1021
- return false
1022
- }
1023
-
1024
- ADL.XHR_request(
1025
- this.lrs,
1026
- url,
1027
- method,
1028
- profileval,
1029
- this.lrs.auth,
1030
- callback,
1031
- null,
1032
- false,
1033
- headers,
1034
- this.withCredentials,
1035
- this.strictCallbacks
1036
- )
1037
- }
1038
- }
1039
-
1040
- /*
1041
- * Get activity profile from the LRS
1042
- * @param {string} activityid the id of the Activity this profile is about
1043
- * @param {string} [profileid] the id of the profile, if not included, the response will be a list of profileids
1044
- * associated with the activity
1045
- * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
1046
- * @param {function [callback] function to be called after the LRS responds
1047
- * to this request (makes the call asynchronous)
1048
- * the function will be passed the XMLHttpRequest object
1049
- * @return {object} xhr response object or null if 404
1050
- * @example
1051
- * ADL.XAPIWrapper.getActivityProfile("http://adlnet.gov/expapi/activities/question",
1052
- * "actprofile", null,
1053
- * function(r){ADL.XAPIWrapper.log(JSON.parse(r.response));});
1054
- * >> {info: "the profile"}
1055
- */
1056
- XAPIWrapper.prototype.getActivityProfile = function(
1057
- activityid,
1058
- profileid,
1059
- since,
1060
- callback
1061
- ) {
1062
- if (this.testConfig()) {
1063
- let url =
1064
- this.lrs.endpoint + 'activities/profile?activityId=<activity ID>'
1065
-
1066
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
1067
-
1068
- if (profileid) {
1069
- url += '&profileId=' + encodeURIComponent(profileid)
1070
- }
1071
-
1072
- if (since) {
1073
- since = isDate(since)
1074
- if (since != null) {
1075
- url += '&since=' + encodeURIComponent(since.toISOString())
1076
- }
1077
- }
1078
-
1079
- let result = ADL.XHR_request(
1080
- this.lrs,
1081
- url,
1082
- 'GET',
1083
- null,
1084
- this.lrs.auth,
1085
- callback,
1086
- null,
1087
- true,
1088
- null,
1089
- this.withCredentials,
1090
- this.strictCallbacks
1091
- )
1092
-
1093
- if (result === undefined || result.status == 404) {
1094
- return null
1095
- }
1096
-
1097
- try {
1098
- return JSON.parse(result.response)
1099
- } catch (e) {
1100
- return result.response
1101
- }
1102
- }
1103
- }
1104
-
1105
- /*
1106
- * Delete activity profile in the LRS
1107
- * @param {string} activityid the id of the Activity this profile is about
1108
- * @param {string} profileid the id you want associated with this profile
1109
- * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1110
- * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1111
- * @param {string} [callback] function to be called after the LRS responds
1112
- * to this request (makes the call asynchronous)
1113
- * the function will be passed the XMLHttpRequest object
1114
- * @return {object} xhr response object or null if 404
1115
- * @example
1116
- * ADL.XAPIWrapper.deleteActivityProfile("http://adlnet.gov/expapi/activities/question",
1117
- * "actprofile");
1118
- * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
1119
- */
1120
- XAPIWrapper.prototype.deleteActivityProfile = function(
1121
- activityid,
1122
- profileid,
1123
- matchHash,
1124
- noneMatchHash,
1125
- callback
1126
- ) {
1127
- if (this.testConfig()) {
1128
- let url =
1129
- this.lrs.endpoint +
1130
- 'activities/profile?activityId=<activity ID>&profileId=<profileid>'
1131
-
1132
- url = url.replace('<activity ID>', encodeURIComponent(activityid))
1133
- url = url.replace('<profileid>', encodeURIComponent(profileid))
1134
-
1135
- let headers = null
1136
- if (matchHash && noneMatchHash) {
1137
- log("Can't have both If-Match and If-None-Match")
1138
- } else if (matchHash) {
1139
- headers = { 'If-Match': ADL.formatHash(matchHash) }
1140
- } else if (noneMatchHash) {
1141
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1142
- }
1143
-
1144
- let result = ADL.XHR_request(
1145
- this.lrs,
1146
- url,
1147
- 'DELETE',
1148
- null,
1149
- this.lrs.auth,
1150
- callback,
1151
- null,
1152
- false,
1153
- headers,
1154
- this.withCredentials,
1155
- this.strictCallbacks
1156
- )
1157
-
1158
- if (result === undefined || result.status == 404) {
1159
- return null
1160
- }
1161
-
1162
- try {
1163
- return JSON.parse(result.response)
1164
- } catch (e) {
1165
- return result
1166
- }
1167
- }
1168
- }
1169
-
1170
- /*
1171
- * Gets the Person object from the LRS based on an agent object.
1172
- * The Person object may contain more information about an agent.
1173
- * See the xAPI Spec for details.
1174
- * @param {object} agent the agent object to get a Person
1175
- * @param {function [callback] function to be called after the LRS responds
1176
- * to this request (makes the call asynchronous)
1177
- * the function will be passed the XMLHttpRequest object
1178
- * @return {object} xhr response object or null if 404
1179
- * @example
1180
- * let res = ADL.XAPIWrapper.getAgents({"mbox":"mailto:tom@example.com"});
1181
- * ADL.XAPIWrapper.log(res);
1182
- * >> <Person object>
1183
- */
1184
- XAPIWrapper.prototype.getAgents = function(agent, callback) {
1185
- if (this.testConfig()) {
1186
- let url = this.lrs.endpoint + 'agents?agent=<agent>'
1187
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1188
-
1189
- let result = ADL.XHR_request(
1190
- this.lrs,
1191
- url,
1192
- 'GET',
1193
- null,
1194
- this.lrs.auth,
1195
- callback,
1196
- null,
1197
- true,
1198
- null,
1199
- this.withCredentials,
1200
- this.strictCallbacks
1201
- )
1202
-
1203
- if (result === undefined || result.status == 404) {
1204
- return null
1205
- }
1206
-
1207
- try {
1208
- return JSON.parse(result.response)
1209
- } catch (e) {
1210
- return result.response
1211
- }
1212
- }
1213
- }
1214
-
1215
- /*
1216
- * Store agent profile in the LRS
1217
- * @param {object} agent the agent this profile is related to
1218
- * @param {string} profileid the id you want associated with this profile
1219
- * @param {string} profileval the profile
1220
- * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1221
- * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1222
- * @param {string} [callback] function to be called after the LRS responds
1223
- * to this request (makes the call asynchronous)
1224
- * the function will be passed the XMLHttpRequest object
1225
- * @return {object} false if no agent profile is included
1226
- * @example
1227
- * let profile = {"info":"the agent profile"};
1228
- * ADL.XAPIWrapper.sendAgentProfile({"mbox":"mailto:tom@example.com"},
1229
- * "agentprofile", profile, null, "*");
1230
- */
1231
- XAPIWrapper.prototype.sendAgentProfile = function(
1232
- agent,
1233
- profileid,
1234
- profileval,
1235
- matchHash,
1236
- noneMatchHash,
1237
- callback
1238
- ) {
1239
- if (this.testConfig()) {
1240
- let url =
1241
- this.lrs.endpoint + 'agents/profile?agent=<agent>&profileId=<profileid>'
1242
-
1243
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1244
- url = url.replace('<profileid>', encodeURIComponent(profileid))
1245
-
1246
- let headers = null
1247
- if (matchHash && noneMatchHash) {
1248
- log("Can't have both If-Match and If-None-Match")
1249
- } else if (matchHash) {
1250
- headers = { 'If-Match': ADL.formatHash(matchHash) }
1251
- } else if (noneMatchHash) {
1252
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1253
- }
1254
-
1255
- let method = 'PUT'
1256
- if (profileval) {
1257
- if (profileval instanceof Array) {
1258
- profileval = JSON.stringify(profileval)
1259
- headers = headers || {}
1260
- headers['Content-Type'] = 'application/json'
1261
- } else if (profileval instanceof Object) {
1262
- profileval = JSON.stringify(profileval)
1263
- headers = headers || {}
1264
- headers['Content-Type'] = 'application/json'
1265
- method = 'POST'
1266
- } else {
1267
- headers = headers || {}
1268
- headers['Content-Type'] = 'application/octet-stream'
1269
- }
1270
- } else {
1271
- this.log('No agent profile was included.')
1272
- return false
1273
- }
1274
-
1275
- ADL.XHR_request(
1276
- this.lrs,
1277
- url,
1278
- method,
1279
- profileval,
1280
- this.lrs.auth,
1281
- callback,
1282
- null,
1283
- false,
1284
- headers,
1285
- this.withCredentials,
1286
- this.strictCallbacks
1287
- )
1288
- }
1289
- }
1290
-
1291
- /*
1292
- * Get agnet profile from the LRS
1293
- * @param {object} agent the agent associated with this profile
1294
- * @param {string} [profileid] the id of the profile, if not included, the response will be a list of profileids
1295
- * associated with the agent
1296
- * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
1297
- * @param {function} [callback] function to be called after the LRS responds
1298
- * to this request (makes the call asynchronous)
1299
- * the function will be passed the XMLHttpRequest object
1300
- * @return {object} xhr response object or null if 404
1301
- * @example
1302
- * ADL.XAPIWrapper.getAgentProfile({"mbox":"mailto:tom@example.com"},
1303
- * "agentprofile", null,
1304
- * function(r){ADL.XAPIWrapper.log(JSON.parse(r.response));});
1305
- * >> {info: "the agent profile"}
1306
- */
1307
- XAPIWrapper.prototype.getAgentProfile = function(
1308
- agent,
1309
- profileid,
1310
- since,
1311
- callback
1312
- ) {
1313
- if (this.testConfig()) {
1314
- let url = this.lrs.endpoint + 'agents/profile?agent=<agent>'
1315
-
1316
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1317
- url = url.replace('<profileid>', encodeURIComponent(profileid))
1318
-
1319
- if (profileid) {
1320
- url += '&profileId=' + encodeURIComponent(profileid)
1321
- }
1322
-
1323
- if (since) {
1324
- since = isDate(since)
1325
- if (since != null) {
1326
- url += '&since=' + encodeURIComponent(since.toISOString())
1327
- }
1328
- }
1329
-
1330
- let result = ADL.XHR_request(
1331
- this.lrs,
1332
- url,
1333
- 'GET',
1334
- null,
1335
- this.lrs.auth,
1336
- callback,
1337
- null,
1338
- true,
1339
- null,
1340
- this.withCredentials,
1341
- this.strictCallbacks
1342
- )
1343
-
1344
- if (result === undefined || result.status == 404) {
1345
- return null
1346
- }
1347
-
1348
- try {
1349
- return JSON.parse(result.response)
1350
- } catch (e) {
1351
- return result.response
1352
- }
1353
- }
1354
- }
1355
-
1356
- /*
1357
- * Delete agent profile in the LRS
1358
- * @param {oject} agent the id of the Agent this profile is about
1359
- * @param {string} profileid the id you want associated with this profile
1360
- * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1361
- * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1362
- * @param {string} [callback] function to be called after the LRS responds
1363
- * to this request (makes the call asynchronous)
1364
- * the function will be passed the XMLHttpRequest object
1365
- * @return {object} xhr response object or null if 404
1366
- * @example
1367
- * ADL.XAPIWrapper.deleteAgentProfile({"mbox":"mailto:tom@example.com"},
1368
- * "agentprofile");
1369
- * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
1370
- */
1371
- XAPIWrapper.prototype.deleteAgentProfile = function(
1372
- agent,
1373
- profileid,
1374
- matchHash,
1375
- noneMatchHash,
1376
- callback
1377
- ) {
1378
- if (this.testConfig()) {
1379
- let url =
1380
- this.lrs.endpoint + 'agents/profile?agent=<agent>&profileId=<profileid>'
1381
-
1382
- url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1383
- url = url.replace('<profileid>', encodeURIComponent(profileid))
1384
-
1385
- let headers = null
1386
- if (matchHash && noneMatchHash) {
1387
- log("Can't have both If-Match and If-None-Match")
1388
- } else if (matchHash) {
1389
- headers = { 'If-Match': ADL.formatHash(matchHash) }
1390
- } else if (noneMatchHash) {
1391
- headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1392
- }
1393
-
1394
- let result = ADL.XHR_request(
1395
- this.lrs,
1396
- url,
1397
- 'DELETE',
1398
- null,
1399
- this.lrs.auth,
1400
- callback,
1401
- null,
1402
- false,
1403
- headers,
1404
- this.withCredentials,
1405
- this.strictCallbacks
1406
- )
1407
-
1408
- if (result === undefined || result.status == 404) {
1409
- return null
1410
- }
1411
-
1412
- try {
1413
- return JSON.parse(result.response)
1414
- } catch (e) {
1415
- return result
1416
- }
1417
- }
1418
- }
1419
-
1420
- /*
1421
- * Tests the configuration of the lrs object
1422
- */
1423
- function testConfig() {
1424
- try {
1425
- return this.lrs.endpoint != undefined && this.lrs.endpoint != ''
1426
- } catch (e) {
1427
- return false
1428
- }
1429
- }
1430
-
1431
- // outputs the message to the console if available
1432
- function log(message) {
1433
- if (!log.debug) return false
1434
- try {
1435
- message
1436
- return true
1437
- } catch (e) {
1438
- return false
1439
- }
1440
- }
1441
-
1442
- // merges two object
1443
- function mergeRecursive(obj1, obj2) {
1444
- for (let p in obj2) {
1445
- if (obj2.hasOwnProperty(p) == false) continue
1446
-
1447
- let prop = obj2[p]
1448
- log(p + ' : ' + prop)
1449
- try {
1450
- // Property in destination object set; update its value.
1451
- if (obj2[p].constructor == Object) {
1452
- obj1[p] = mergeRecursive(obj1[p], obj2[p])
1453
- } else {
1454
- if (obj1 == undefined) {
1455
- obj1 = new Object()
1456
- }
1457
- obj1[p] = obj2[p]
1458
- }
1459
- } catch (e) {
1460
- if (obj1 == undefined) {
1461
- obj1 = new Object()
1462
- }
1463
- // Property in destination object not set; create it and set its value.
1464
- obj1[p] = obj2[p]
1465
- }
1466
- }
1467
-
1468
- return obj1
1469
- }
1470
-
1471
- // iniitializes an lrs object with settings from
1472
- // a config file and from the url query string
1473
- function getLRSObject(config) {
1474
- let lrsProps = [
1475
- 'endpoint',
1476
- 'auth',
1477
- 'actor',
1478
- 'registration',
1479
- 'activity_id',
1480
- 'grouping',
1481
- 'activity_platform'
1482
- ]
1483
- let lrs = new Object()
1484
- let qsVars, prop
1485
-
1486
- qsVars = parseQueryString()
1487
- if (qsVars !== undefined && Object.keys(qsVars).length !== 0) {
1488
- for (let i = 0; i < lrsProps.length; i++) {
1489
- prop = lrsProps[i]
1490
- if (qsVars[prop]) {
1491
- lrs[prop] = qsVars[prop]
1492
- delete qsVars[prop]
1493
- }
1494
- }
1495
- // if (Object.keys(qsVars).length !== 0) {
1496
- // lrs.extended = qsVars;
1497
- // }
1498
-
1499
- lrs = mergeRecursive(config, lrs)
1500
- } else {
1501
- lrs = config
1502
- }
1503
-
1504
- return lrs
1505
- }
1506
-
1507
- // parses the params in the url query string
1508
- function parseQueryString() {
1509
- let qs, pairs, pair, ii, parsed
1510
-
1511
- qs = window.location.search.substring(1)
1512
-
1513
- pairs = qs.split('&')
1514
- parsed = {}
1515
- for (ii = 0; ii < pairs.length; ii++) {
1516
- pair = pairs[ii].split('=')
1517
- if (pair.length === 2 && pair[0]) {
1518
- parsed[pair[0]] = decodeURIComponent(pair[1])
1519
- }
1520
- }
1521
-
1522
- return parsed
1523
- }
1524
-
1525
- function delay() {
1526
- let xhr = new XMLHttpRequest()
1527
- let url = window.location + '?forcenocache=' + ADL.ruuid()
1528
- xhr.open('GET', url, false)
1529
- xhr.send(null)
1530
- }
1531
-
1532
- /*
1533
- * formats a request in a way that IE will allow
1534
- * @param {string} method the http request method (ex: "PUT", "GET")
1535
- * @param {string} url the url to the request (ex: ADL.XAPIWrapper.lrs.endpoint + "statements")
1536
- * @param {array} [headers] headers to include in the request
1537
- * @param {string} [data] the body of the request, if there is one
1538
- * @return {object} xhr response object
1539
- */
1540
- // function ie_request(method, url, headers, data) {
1541
- // let newUrl = url
1542
-
1543
- // //Everything that was on query string goes into form vars
1544
- // let formData = new Array()
1545
- // let qsIndex = newUrl.indexOf('?')
1546
- // if (qsIndex > 0) {
1547
- // formData.push(newUrl.substring(qsIndex + 1))
1548
- // newUrl = newUrl.substring(0, qsIndex)
1549
- // }
1550
-
1551
- // //Method has to go on querystring, and nothing else
1552
- // newUrl = newUrl + '?method=' + method
1553
-
1554
- // //Headers
1555
- // if (headers !== null) {
1556
- // for (let headerName in headers) {
1557
- // if (headers.hasOwnProperty(headerName))
1558
- // formData.push(
1559
- // headerName + '=' + encodeURIComponent(headers[headerName])
1560
- // )
1561
- // }
1562
- // }
1563
-
1564
- // //The original data is repackaged as "content" form var
1565
- // if (data !== null) {
1566
- // formData.push('content=' + encodeURIComponent(data))
1567
- // }
1568
-
1569
- // return {
1570
- // method: 'POST',
1571
- // url: newUrl,
1572
- // headers: {},
1573
- // data: formData.join('&')
1574
- // }
1575
- // }
1576
-
1577
- /*!
1578
- Excerpt from: Math.uuid.js (v1.4)
1579
- http://www.broofa.com
1580
- mailto:robert@broofa.com
1581
- Copyright (c) 2010 Robert Kieffer
1582
- Dual licensed under the MIT and GPL licenses.
1583
- */
1584
- ADL.ruuid = function() {
1585
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
1586
- let r = (Math.random() * 16) | 0,
1587
- v = c == 'x' ? r : (r & 0x3) | 0x8
1588
- return v.toString(16)
1589
- })
1590
- }
1591
-
1592
- /*
1593
- * dateFromISOString
1594
- * parses an ISO string into a date object
1595
- * isostr - the ISO string
1596
- */
1597
- ADL.dateFromISOString = function(isostr) {
1598
- let regexp =
1599
- '([0-9]{4})(-([0-9]{2})(-([0-9]{2})' +
1600
- '([T| ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(.([0-9]+))?)?' +
1601
- '(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?'
1602
- let d = isostr.match(new RegExp(regexp))
1603
-
1604
- let offset = 0
1605
- let date = new Date(d[1], 0, 1)
1606
-
1607
- if (d[3]) {
1608
- date.setMonth(d[3] - 1)
1609
- }
1610
- if (d[5]) {
1611
- date.setDate(d[5])
1612
- }
1613
- if (d[7]) {
1614
- date.setHours(d[7])
1615
- }
1616
- if (d[8]) {
1617
- date.setMinutes(d[8])
1618
- }
1619
- if (d[10]) {
1620
- date.setSeconds(d[10])
1621
- }
1622
- if (d[12]) {
1623
- date.setMilliseconds(Number('0.' + d[12]) * 1000)
1624
- }
1625
- if (d[14]) {
1626
- offset = Number(d[16]) * 60 + Number(d[17])
1627
- offset *= d[15] == '-' ? 1 : -1
1628
- }
1629
-
1630
- offset -= date.getTimezoneOffset()
1631
- let time = Number(date) + offset * 60 * 1000
1632
-
1633
- let dateToReturn = new Date()
1634
- dateToReturn.setTime(Number(time))
1635
- return dateToReturn
1636
- }
1637
-
1638
- // Synchronous if callback is not provided (not recommended)
1639
- /*
1640
- * makes a request to a server (if possible, use functions provided in XAPIWrapper)
1641
- * @param {string} lrs the lrs connection info, such as endpoint, auth, etc
1642
- * @param {string} url the url of this request
1643
- * @param {string} method the http request method
1644
- * @param {string} data the payload
1645
- * @param {string} auth the value for the Authorization header
1646
- * @param {function} callback function to be called after the LRS responds
1647
- * to this request (makes the call asynchronous)
1648
- * @param {object} [callbackargs] additional javascript object to be passed to the callback function
1649
- * @param {boolean} ignore404 allow page not found errors to pass
1650
- * @param {object} extraHeaders other header key-values to be added to this request
1651
- * @param {boolean} withCredentials
1652
- * @param {boolean} strictCallbacks Callback must be executed and first param is error or null if no error
1653
- * @return {object} xhr response object
1654
- */
1655
- ADL.XHR_request = function(
1656
- lrs,
1657
- url,
1658
- method,
1659
- data,
1660
- auth,
1661
- callback,
1662
- callbackargs,
1663
- ignore404,
1664
- extraHeaders,
1665
- withCredentials,
1666
- strictCallbacks
1667
- ) {
1668
- 'use strict'
1669
-
1670
- let xhr,
1671
- finished = false,
1672
- xDomainRequest = false,
1673
- ieXDomain = false,
1674
- ieModeRequest,
1675
- urlparts = url.toLowerCase().match(/^(.+):\/\/([^:/]*):?(\d+)?(\/.*)?$/),
1676
- location = window.location,
1677
- urlPort,
1678
- result,
1679
- extended,
1680
- prop,
1681
- until
1682
-
1683
- //Consolidate headers
1684
- let headers = {}
1685
- headers['Content-Type'] = 'application/json'
1686
- headers['Authorization'] = auth
1687
- headers['X-Experience-API-Version'] = ADL.XAPIWrapper.xapiVersion
1688
- if (extraHeaders !== null) {
1689
- for (let headerName in extraHeaders) {
1690
- if (extraHeaders.hasOwnProperty(headerName))
1691
- headers[headerName] = extraHeaders[headerName]
1692
- }
1693
- }
1694
-
1695
- //See if this really is a cross domain
1696
- xDomainRequest =
1697
- location.protocol.toLowerCase() !== urlparts[1] ||
1698
- location.hostname.toLowerCase() !== urlparts[2]
1699
- if (!xDomainRequest) {
1700
- urlPort =
1701
- urlparts[3] === null
1702
- ? urlparts[1] === 'http'
1703
- ? '80'
1704
- : '443'
1705
- : urlparts[3]
1706
- xDomainRequest = urlPort === location.port
1707
- }
1708
-
1709
- //Add extended LMS-specified values to the URL
1710
- if (lrs !== null && lrs.extended !== undefined) {
1711
- extended = new Array()
1712
- for (prop in lrs.extended) {
1713
- extended.push(prop + '=' + encodeURIComponent(lrs.extended[prop]))
1714
- }
1715
- if (extended.length > 0) {
1716
- url += (url.indexOf('?') > -1 ? '&' : '?') + extended.join('&')
1717
- }
1718
- }
1719
-
1720
- //If it's not cross domain or we're not using IE, use the usual XmlHttpRequest
1721
- let windowsVersionCheck =
1722
- window.XDomainRequest &&
1723
- window.XMLHttpRequest &&
1724
- new XMLHttpRequest().responseType === undefined
1725
- if (
1726
- !xDomainRequest ||
1727
- windowsVersionCheck === undefined ||
1728
- windowsVersionCheck === false
1729
- ) {
1730
- xhr = new XMLHttpRequest()
1731
- xhr.withCredentials = withCredentials //allow cross domain cookie based auth
1732
- xhr.open(method, url, callback != null)
1733
- for (let headerName in headers) {
1734
- xhr.setRequestHeader(headerName, headers[headerName])
1735
- }
1736
- }
1737
- //Otherwise, use IE's XDomainRequest object
1738
- else {
1739
- ieXDomain = true
1740
- // ieModeRequest = ie_request(method, url, headers, data)
1741
- // xhr = new XDomainRequest()
1742
- xhr.open(ieModeRequest.method, ieModeRequest.url)
1743
- }
1744
-
1745
- //Setup request callback
1746
- function requestComplete() {
1747
- if (!finished) {
1748
- // may be in sync or async mode, using XMLHttpRequest or IE XDomainRequest, onreadystatechange or
1749
- // onload or both might fire depending upon browser, just covering all bases with event hooks and
1750
- // using 'finished' flag to avoid triggering events multiple times
1751
- finished = true
1752
- let notFoundOk = ignore404 && xhr.status === 404
1753
- if (
1754
- xhr.status === undefined ||
1755
- (xhr.status >= 200 && xhr.status < 400) ||
1756
- notFoundOk
1757
- ) {
1758
- if (callback) {
1759
- if (callbackargs) {
1760
- strictCallbacks
1761
- ? callback(null, xhr, callbackargs)
1762
- : callback(xhr, callbackargs)
1763
- } else {
1764
- let body
1765
-
1766
- try {
1767
- body = JSON.parse(xhr.responseText)
1768
- } catch (e) {
1769
- body = xhr.responseText
1770
- }
1771
-
1772
- strictCallbacks ? callback(null, xhr, body) : callback(xhr, body)
1773
- }
1774
- } else {
1775
- result = xhr
1776
- return xhr
1777
- }
1778
- } else {
1779
- let warning
1780
- try {
1781
- warning =
1782
- 'There was a problem communicating with the Learning Record Store. ( ' +
1783
- xhr.status +
1784
- ' | ' +
1785
- xhr.response +
1786
- ' )' +
1787
- url
1788
- } catch (ex) {
1789
- warning = ex.toString()
1790
- }
1791
- ADL.XAPIWrapper.log(warning)
1792
- ADL.xhrRequestOnError(
1793
- xhr,
1794
- method,
1795
- url,
1796
- callback,
1797
- callbackargs,
1798
- strictCallbacks
1799
- )
1800
- result = xhr
1801
- return xhr
1802
- }
1803
- } else {
1804
- return result
1805
- }
1806
- }
1807
-
1808
- xhr.onreadystatechange = function() {
1809
- if (xhr.readyState === 4) {
1810
- return requestComplete()
1811
- }
1812
- }
1813
-
1814
- xhr.onload = requestComplete
1815
- xhr.onerror = requestComplete
1816
- //xhr.onerror = ADL.xhrRequestOnError(xhr, method, url);
1817
-
1818
- xhr.send(ieXDomain ? ieModeRequest.data : data)
1819
-
1820
- if (!callback) {
1821
- // synchronous
1822
- if (ieXDomain) {
1823
- // synchronous call in IE, with no asynchronous mode available.
1824
- until = 1000 + new Date()
1825
- while (new Date() < until && xhr.readyState !== 4 && !finished) {
1826
- delay()
1827
- }
1828
- }
1829
- return requestComplete()
1830
- }
1831
- }
1832
-
1833
- /*
1834
- * Holder for custom global error callback
1835
- * @param {object} xhr xhr object or null
1836
- * @param {string} method XMLHttpRequest request method
1837
- * @param {string} url full endpoint url
1838
- * @param {function} callback function to be called after the LRS responds
1839
- * to this request (makes the call asynchronous)
1840
- * @param {object} [callbackargs] additional javascript object to be passed to the callback function
1841
- * @param {boolean} strictCallbacks Callback must be executed and first param is error or null if no error
1842
- * @example
1843
- * ADL.xhrRequestOnError = function(xhr, method, url, callback, callbackargs) {
1844
- * console.log(xhr);
1845
- * alert(xhr.status + " " + xhr.statusText + ": " + xhr.response);
1846
- * };
1847
- */
1848
- ADL.xhrRequestOnError = function(
1849
- xhr,
1850
- method,
1851
- url,
1852
- callback,
1853
- callbackargs,
1854
- strictCallbacks
1855
- ) {
1856
- if (callback && strictCallbacks) {
1857
- let status = xhr ? xhr.status : undefined
1858
- let error
1859
- if (status) {
1860
- error = new Error('Request error: ' + xhr.status)
1861
- } else if (status === 0 || status === null) {
1862
- // 0 and null = aborted
1863
- error = new Error('Request error: aborted')
1864
- } else {
1865
- error = new Error('Reqeust error: unknown')
1866
- }
1867
-
1868
- if (callbackargs) {
1869
- callback(error, xhr, callbackargs)
1870
- } else {
1871
- let body
1872
-
1873
- try {
1874
- body = JSON.parse(xhr.responseText)
1875
- } catch (e) {
1876
- body = xhr.responseText
1877
- }
1878
-
1879
- callback(error, xhr, body)
1880
- }
1881
- }
1882
- }
1883
-
1884
- ADL.formatHash = function(hash) {
1885
- return hash === '*' ? hash : '"' + hash + '"'
1886
- }
1887
-
1888
- ADL.XAPIWrapper = new XAPIWrapper(Config, false)
1889
- //============================= END ==========================================
1890
- }
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 = JSON.parse(r.response)
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, 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
+ // let resp = ADL.XHR_request(
636
+ // this.lrs,
637
+ // this.lrs.endpoint + 'statements',
638
+ // 'POST',
639
+ // JSON.stringify(stmtArray),
640
+ // this.lrs.auth,
641
+ // callback,
642
+ // null,
643
+ // false,
644
+ // null,
645
+ // this.withCredentials,
646
+ // this.strictCallbacks
647
+ // )
648
+ //=============================================
649
+ let headers = {}
650
+ headers['Content-type'] = 'application/json; charset=UTF-8'
651
+ headers['Authorization'] = this.lrs.auth
652
+ headers['X-Experience-API-Version'] = ADL.XAPIWrapper.xapiVersion
653
+
654
+ let payload = JSON.stringify(stmtArray)
655
+ // fecth API used to allow the delivery of the request after the browser is closed
656
+ let resp = await fetch(this.lrs.endpoint + 'statements', {
657
+ method: 'POST',
658
+ headers,
659
+ body: payload,
660
+ keepalive: true // allow the request to outlive the closing of browser tab
661
+ })
662
+
663
+ //=============================================
664
+ if (!callback) {
665
+ return resp
666
+ }
667
+ }
668
+ }
669
+
670
+ /*
671
+ * Get statement(s) based on the searchparams or more url.
672
+ * @param {object} searchparams an ADL.XAPIWrapper.searchParams object of
673
+ * key(search parameter)-value(parameter value) pairs.
674
+ * Example:
675
+ * let myparams = ADL.XAPIWrapper.searchParams();
676
+ * myparams['verb'] = ADL.verbs.completed.id;
677
+ * let completedStmts = ADL.XAPIWrapper.getStatements(myparams);
678
+ * @param {string} more the more url found in the StatementResults object, if there are more
679
+ * statements available based on your get statements request. Pass the
680
+ * more url as this parameter to retrieve those statements.
681
+ * @param {function} [callback] - function to be called after the LRS responds
682
+ * to this request (makes the call asynchronous)
683
+ * the function will be passed the XMLHttpRequest object
684
+ * @return {object} xhr response object or null if 404
685
+ * @example
686
+ * let ret = ADL.XAPIWrapper.getStatements();
687
+ * if (ret)
688
+ * ADL.XAPIWrapper.log(ret.statements);
689
+ *
690
+ * >> <Array of statements>
691
+ */
692
+ XAPIWrapper.prototype.getStatements = async function (
693
+ searchparams,
694
+ more,
695
+ callback
696
+ ) {
697
+ if (this.testConfig()) {
698
+ let url = this.lrs.endpoint + 'statements'
699
+ if (more) {
700
+ url = this.base + more
701
+ } else {
702
+ let urlparams = new Array()
703
+
704
+ for (let s in searchparams) {
705
+ if (searchparams.hasOwnProperty(s)) {
706
+ if (s == 'until' || s == 'since') {
707
+ let d = new Date(searchparams[s])
708
+ urlparams.push(s + '=' + encodeURIComponent(d.toISOString()))
709
+ } else {
710
+ urlparams.push(s + '=' + encodeURIComponent(searchparams[s]))
711
+ }
712
+ }
713
+ }
714
+ if (urlparams.length > 0) url = url + '?' + urlparams.join('&')
715
+ }
716
+ let res = await ADL.XHR_request(
717
+ this.lrs,
718
+ url,
719
+ 'GET',
720
+ null,
721
+ this.lrs.auth,
722
+ callback,
723
+ null,
724
+ false,
725
+ null,
726
+ this.withCredentials,
727
+ this.strictCallbacks
728
+ )
729
+
730
+ if (res === undefined || res.status == 404) {
731
+ return null
732
+ }
733
+
734
+ try {
735
+ return JSON.parse(res.response)
736
+ } catch (e) {
737
+ return res.response
738
+ }
739
+ }
740
+ }
741
+
742
+ /*
743
+ * Gets the Activity object from the LRS.
744
+ * @param {string} activityid the id of the Activity to get
745
+ * @param {function} [callback] function to be called after the LRS responds
746
+ * to this request (makes the call asynchronous)
747
+ * the function will be passed the XMLHttpRequest object
748
+ * @return {object} xhr response object or null if 404
749
+ * @example
750
+ * let res = ADL.XAPIWrapper.getActivities("http://adlnet.gov/expapi/activities/question");
751
+ * ADL.XAPIWrapper.log(res);
752
+ * >> <Activity object>
753
+ */
754
+ XAPIWrapper.prototype.getActivities = function (activityid, callback) {
755
+ if (this.testConfig()) {
756
+ let url = this.lrs.endpoint + 'activities?activityId=<activityid>'
757
+ url = url.replace('<activityid>', encodeURIComponent(activityid))
758
+
759
+ let result = ADL.XHR_request(
760
+ this.lrs,
761
+ url,
762
+ 'GET',
763
+ null,
764
+ this.lrs.auth,
765
+ callback,
766
+ null,
767
+ true,
768
+ null,
769
+ this.withCredentials,
770
+ this.strictCallbacks
771
+ )
772
+
773
+ if (result === undefined || result.status == 404) {
774
+ return null
775
+ }
776
+
777
+ try {
778
+ return JSON.parse(result.response)
779
+ } catch (e) {
780
+ return result.response
781
+ }
782
+ }
783
+ }
784
+
785
+ /*
786
+ * Store activity state in the LRS
787
+ * @param {string} activityid the id of the Activity this state is about
788
+ * @param {object} agent the agent this Activity state is related to
789
+ * @param {string} stateid the id you want associated with this state
790
+ * @param {string} [registration] the registraton id associated with this state
791
+ * @param {string} stateval the state
792
+ * @param {string} [matchHash] the hash of the state to replace or * to replace any
793
+ * @param {string} [noneMatchHash] the hash of the current state or * to indicate no previous state
794
+ * @param {function} [callback] function to be called after the LRS responds
795
+ * to this request (makes the call asynchronous)
796
+ * the function will be passed the XMLHttpRequest object
797
+ * @return {boolean} false if no activity state is included
798
+ * @example
799
+ * let stateval = {"info":"the state info"};
800
+ * ADL.XAPIWrapper.sendState("http://adlnet.gov/expapi/activities/question",
801
+ * {"mbox":"mailto:tom@example.com"},
802
+ * "questionstate", null, stateval);
803
+ */
804
+ XAPIWrapper.prototype.sendState = function (
805
+ activityid,
806
+ agent,
807
+ stateid,
808
+ registration,
809
+ stateval,
810
+ matchHash,
811
+ noneMatchHash,
812
+ callback
813
+ ) {
814
+ if (this.testConfig()) {
815
+ let url =
816
+ this.lrs.endpoint +
817
+ 'activities/state?activityId=<activity ID>&agent=<agent>&stateId=<stateid>'
818
+
819
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
820
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
821
+ url = url.replace('<stateid>', encodeURIComponent(stateid))
822
+
823
+ if (registration) {
824
+ url += '&registration=' + encodeURIComponent(registration)
825
+ }
826
+
827
+ let headers = null
828
+ if (matchHash && noneMatchHash) {
829
+ log("Can't have both If-Match and If-None-Match")
830
+ } else if (matchHash) {
831
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
832
+ } else if (noneMatchHash) {
833
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
834
+ }
835
+
836
+ let method = 'PUT'
837
+ if (stateval) {
838
+ if (stateval instanceof Array) {
839
+ stateval = JSON.stringify(stateval)
840
+ headers = headers || {}
841
+ headers['Content-Type'] = 'application/json'
842
+ } else if (stateval instanceof Object) {
843
+ stateval = JSON.stringify(stateval)
844
+ headers = headers || {}
845
+ headers['Content-Type'] = 'application/json'
846
+ method = 'POST'
847
+ } else {
848
+ headers = headers || {}
849
+ headers['Content-Type'] = 'application/octet-stream'
850
+ }
851
+ } else {
852
+ this.log('No activity state was included.')
853
+ return false
854
+ }
855
+ //(lrs, url, method, data, auth, callback, callbackargs, ignore404, extraHeaders)
856
+
857
+ ADL.XHR_request(
858
+ this.lrs,
859
+ url,
860
+ method,
861
+ stateval,
862
+ this.lrs.auth,
863
+ callback,
864
+ null,
865
+ null,
866
+ headers,
867
+ this.withCredentials,
868
+ this.strictCallbacks
869
+ )
870
+ }
871
+ }
872
+
873
+ /*
874
+ * Get activity state from the LRS
875
+ * @param {string} activityid the id of the Activity this state is about
876
+ * @param {object} agent the agent this Activity state is related to
877
+ * @param {string} [stateid] the id of the state, if not included, the response will be a list of stateids
878
+ * associated with the activity and agent)
879
+ * @param {string} [registration] the registraton id associated with this state
880
+ * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
881
+ * @param {function} [callback] function to be called after the LRS responds
882
+ * to this request (makes the call asynchronous)
883
+ * the function will be passed the XMLHttpRequest object
884
+ * @return {object} xhr response object or null if 404
885
+ * @example
886
+ * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
887
+ * {"mbox":"mailto:tom@example.com"}, "questionstate");
888
+ * >> {info: "the state info"}
889
+ */
890
+ XAPIWrapper.prototype.getState = function (
891
+ activityid,
892
+ agent,
893
+ stateid,
894
+ registration,
895
+ since,
896
+ callback
897
+ ) {
898
+ if (this.testConfig()) {
899
+ let url =
900
+ this.lrs.endpoint +
901
+ 'activities/state?activityId=<activity ID>&agent=<agent>'
902
+
903
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
904
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
905
+
906
+ if (stateid) {
907
+ url += '&stateId=' + encodeURIComponent(stateid)
908
+ }
909
+
910
+ if (registration) {
911
+ url += '&registration=' + encodeURIComponent(registration)
912
+ }
913
+
914
+ if (since) {
915
+ since = isDate(since)
916
+ if (since != null) {
917
+ url += '&since=' + encodeURIComponent(since.toISOString())
918
+ }
919
+ }
920
+
921
+ let result = ADL.XHR_request(
922
+ this.lrs,
923
+ url,
924
+ 'GET',
925
+ null,
926
+ this.lrs.auth,
927
+ callback,
928
+ null,
929
+ true,
930
+ null,
931
+ this.withCredentials,
932
+ this.strictCallbacks
933
+ )
934
+
935
+ if (result === undefined || result.status == 404) {
936
+ return null
937
+ }
938
+
939
+ try {
940
+ return JSON.parse(result.response)
941
+ } catch (e) {
942
+ return result.response
943
+ }
944
+ }
945
+ }
946
+
947
+ /*
948
+ * Delete activity state in the LRS
949
+ * @param {string} activityid the id of the Activity this state is about
950
+ * @param {object} agent the agent this Activity state is related to
951
+ * @param {string} stateid the id you want associated with this state
952
+ * @param {string} [registration] the registraton id associated with this state
953
+ * @param {string} [matchHash] the hash of the state to replace or * to replace any
954
+ * @param {string} [noneMatchHash] the hash of the current state or * to indicate no previous state
955
+ * @param {string} [callback] function to be called after the LRS responds
956
+ * to this request (makes the call asynchronous)
957
+ * the function will be passed the XMLHttpRequest object
958
+ * @return {object} xhr response object or null if 404
959
+ * @example
960
+ * let stateval = {"info":"the state info"};
961
+ * ADL.XAPIWrapper.sendState("http://adlnet.gov/expapi/activities/question",
962
+ * {"mbox":"mailto:tom@example.com"},
963
+ * "questionstate", null, stateval);
964
+ * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
965
+ * {"mbox":"mailto:tom@example.com"}, "questionstate");
966
+ * >> {info: "the state info"}
967
+ *
968
+ * ADL.XAPIWrapper.deleteState("http://adlnet.gov/expapi/activities/question",
969
+ * {"mbox":"mailto:tom@example.com"}, "questionstate");
970
+ * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
971
+ *
972
+ * ADL.XAPIWrapper.getState("http://adlnet.gov/expapi/activities/question",
973
+ * {"mbox":"mailto:tom@example.com"}, "questionstate");
974
+ * >> 404
975
+ */
976
+ XAPIWrapper.prototype.deleteState = function (
977
+ activityid,
978
+ agent,
979
+ stateid,
980
+ registration,
981
+ matchHash,
982
+ noneMatchHash,
983
+ callback
984
+ ) {
985
+ if (this.testConfig()) {
986
+ let url =
987
+ this.lrs.endpoint +
988
+ 'activities/state?activityId=<activity ID>&agent=<agent>&stateId=<stateid>'
989
+
990
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
991
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
992
+ url = url.replace('<stateid>', encodeURIComponent(stateid))
993
+
994
+ if (registration) {
995
+ url += '&registration=' + encodeURIComponent(registration)
996
+ }
997
+
998
+ let headers = null
999
+ if (matchHash && noneMatchHash) {
1000
+ log("Can't have both If-Match and If-None-Match")
1001
+ } else if (matchHash) {
1002
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
1003
+ } else if (noneMatchHash) {
1004
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1005
+ }
1006
+
1007
+ let result = ADL.XHR_request(
1008
+ this.lrs,
1009
+ url,
1010
+ 'DELETE',
1011
+ null,
1012
+ this.lrs.auth,
1013
+ callback,
1014
+ null,
1015
+ false,
1016
+ headers,
1017
+ this.withCredentials,
1018
+ this.strictCallbacks
1019
+ )
1020
+
1021
+ if (result === undefined || result.status == 404) {
1022
+ return null
1023
+ }
1024
+
1025
+ try {
1026
+ return JSON.parse(result.response)
1027
+ } catch (e) {
1028
+ return result
1029
+ }
1030
+ }
1031
+ }
1032
+
1033
+ /*
1034
+ * Store activity profile in the LRS
1035
+ * @param {string} activityid the id of the Activity this profile is about
1036
+ * @param {string} profileid the id you want associated with this profile
1037
+ * @param {string} profileval the profile
1038
+ * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1039
+ * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1040
+ * @param {string} [callback] function to be called after the LRS responds
1041
+ * to this request (makes the call asynchronous)
1042
+ * the function will be passed the XMLHttpRequest object
1043
+ * @return {bolean} false if no activity profile is included
1044
+ * @example
1045
+ * let profile = {"info":"the profile"};
1046
+ * ADL.XAPIWrapper.sendActivityProfile("http://adlnet.gov/expapi/activities/question",
1047
+ * "actprofile", profile, null, "*");
1048
+ */
1049
+ XAPIWrapper.prototype.sendActivityProfile = function (
1050
+ activityid,
1051
+ profileid,
1052
+ profileval,
1053
+ matchHash,
1054
+ noneMatchHash,
1055
+ callback
1056
+ ) {
1057
+ if (this.testConfig()) {
1058
+ let url =
1059
+ this.lrs.endpoint +
1060
+ 'activities/profile?activityId=<activity ID>&profileId=<profileid>'
1061
+
1062
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
1063
+ url = url.replace('<profileid>', encodeURIComponent(profileid))
1064
+
1065
+ let headers = null
1066
+ if (matchHash && noneMatchHash) {
1067
+ log("Can't have both If-Match and If-None-Match")
1068
+ } else if (matchHash) {
1069
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
1070
+ } else if (noneMatchHash) {
1071
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1072
+ }
1073
+
1074
+ let method = 'PUT'
1075
+ if (profileval) {
1076
+ if (profileval instanceof Array) {
1077
+ profileval = JSON.stringify(profileval)
1078
+ headers = headers || {}
1079
+ headers['Content-Type'] = 'application/json'
1080
+ } else if (profileval instanceof Object) {
1081
+ profileval = JSON.stringify(profileval)
1082
+ headers = headers || {}
1083
+ headers['Content-Type'] = 'application/json'
1084
+ method = 'POST'
1085
+ } else {
1086
+ headers = headers || {}
1087
+ headers['Content-Type'] = 'application/octet-stream'
1088
+ }
1089
+ } else {
1090
+ this.log('No activity profile was included.')
1091
+ return false
1092
+ }
1093
+
1094
+ ADL.XHR_request(
1095
+ this.lrs,
1096
+ url,
1097
+ method,
1098
+ profileval,
1099
+ this.lrs.auth,
1100
+ callback,
1101
+ null,
1102
+ false,
1103
+ headers,
1104
+ this.withCredentials,
1105
+ this.strictCallbacks
1106
+ )
1107
+ }
1108
+ }
1109
+
1110
+ /*
1111
+ * Get activity profile from the LRS
1112
+ * @param {string} activityid the id of the Activity this profile is about
1113
+ * @param {string} [profileid] the id of the profile, if not included, the response will be a list of profileids
1114
+ * associated with the activity
1115
+ * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
1116
+ * @param {function [callback] function to be called after the LRS responds
1117
+ * to this request (makes the call asynchronous)
1118
+ * the function will be passed the XMLHttpRequest object
1119
+ * @return {object} xhr response object or null if 404
1120
+ * @example
1121
+ * ADL.XAPIWrapper.getActivityProfile("http://adlnet.gov/expapi/activities/question",
1122
+ * "actprofile", null,
1123
+ * function(r){ADL.XAPIWrapper.log(JSON.parse(r.response));});
1124
+ * >> {info: "the profile"}
1125
+ */
1126
+ XAPIWrapper.prototype.getActivityProfile = function (
1127
+ activityid,
1128
+ profileid,
1129
+ since,
1130
+ callback
1131
+ ) {
1132
+ if (this.testConfig()) {
1133
+ let url =
1134
+ this.lrs.endpoint + 'activities/profile?activityId=<activity ID>'
1135
+
1136
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
1137
+
1138
+ if (profileid) {
1139
+ url += '&profileId=' + encodeURIComponent(profileid)
1140
+ }
1141
+
1142
+ if (since) {
1143
+ since = isDate(since)
1144
+ if (since != null) {
1145
+ url += '&since=' + encodeURIComponent(since.toISOString())
1146
+ }
1147
+ }
1148
+
1149
+ let result = ADL.XHR_request(
1150
+ this.lrs,
1151
+ url,
1152
+ 'GET',
1153
+ null,
1154
+ this.lrs.auth,
1155
+ callback,
1156
+ null,
1157
+ true,
1158
+ null,
1159
+ this.withCredentials,
1160
+ this.strictCallbacks
1161
+ )
1162
+
1163
+ if (result === undefined || result.status == 404) {
1164
+ return null
1165
+ }
1166
+
1167
+ try {
1168
+ return JSON.parse(result.response)
1169
+ } catch (e) {
1170
+ return result.response
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ /*
1176
+ * Delete activity profile in the LRS
1177
+ * @param {string} activityid the id of the Activity this profile is about
1178
+ * @param {string} profileid the id you want associated with this profile
1179
+ * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1180
+ * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1181
+ * @param {string} [callback] function to be called after the LRS responds
1182
+ * to this request (makes the call asynchronous)
1183
+ * the function will be passed the XMLHttpRequest object
1184
+ * @return {object} xhr response object or null if 404
1185
+ * @example
1186
+ * ADL.XAPIWrapper.deleteActivityProfile("http://adlnet.gov/expapi/activities/question",
1187
+ * "actprofile");
1188
+ * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
1189
+ */
1190
+ XAPIWrapper.prototype.deleteActivityProfile = function (
1191
+ activityid,
1192
+ profileid,
1193
+ matchHash,
1194
+ noneMatchHash,
1195
+ callback
1196
+ ) {
1197
+ if (this.testConfig()) {
1198
+ let url =
1199
+ this.lrs.endpoint +
1200
+ 'activities/profile?activityId=<activity ID>&profileId=<profileid>'
1201
+
1202
+ url = url.replace('<activity ID>', encodeURIComponent(activityid))
1203
+ url = url.replace('<profileid>', encodeURIComponent(profileid))
1204
+
1205
+ let headers = null
1206
+ if (matchHash && noneMatchHash) {
1207
+ log("Can't have both If-Match and If-None-Match")
1208
+ } else if (matchHash) {
1209
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
1210
+ } else if (noneMatchHash) {
1211
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1212
+ }
1213
+
1214
+ let result = ADL.XHR_request(
1215
+ this.lrs,
1216
+ url,
1217
+ 'DELETE',
1218
+ null,
1219
+ this.lrs.auth,
1220
+ callback,
1221
+ null,
1222
+ false,
1223
+ headers,
1224
+ this.withCredentials,
1225
+ this.strictCallbacks
1226
+ )
1227
+
1228
+ if (result === undefined || result.status == 404) {
1229
+ return null
1230
+ }
1231
+
1232
+ try {
1233
+ return JSON.parse(result.response)
1234
+ } catch (e) {
1235
+ return result
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ /*
1241
+ * Gets the Person object from the LRS based on an agent object.
1242
+ * The Person object may contain more information about an agent.
1243
+ * See the xAPI Spec for details.
1244
+ * @param {object} agent the agent object to get a Person
1245
+ * @param {function [callback] function to be called after the LRS responds
1246
+ * to this request (makes the call asynchronous)
1247
+ * the function will be passed the XMLHttpRequest object
1248
+ * @return {object} xhr response object or null if 404
1249
+ * @example
1250
+ * let res = ADL.XAPIWrapper.getAgents({"mbox":"mailto:tom@example.com"});
1251
+ * ADL.XAPIWrapper.log(res);
1252
+ * >> <Person object>
1253
+ */
1254
+ XAPIWrapper.prototype.getAgents = function (agent, callback) {
1255
+ if (this.testConfig()) {
1256
+ let url = this.lrs.endpoint + 'agents?agent=<agent>'
1257
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1258
+
1259
+ let result = ADL.XHR_request(
1260
+ this.lrs,
1261
+ url,
1262
+ 'GET',
1263
+ null,
1264
+ this.lrs.auth,
1265
+ callback,
1266
+ null,
1267
+ true,
1268
+ null,
1269
+ this.withCredentials,
1270
+ this.strictCallbacks
1271
+ )
1272
+
1273
+ if (result === undefined || result.status == 404) {
1274
+ return null
1275
+ }
1276
+
1277
+ try {
1278
+ return JSON.parse(result.response)
1279
+ } catch (e) {
1280
+ return result.response
1281
+ }
1282
+ }
1283
+ }
1284
+
1285
+ /*
1286
+ * Store agent profile in the LRS
1287
+ * @param {object} agent the agent this profile is related to
1288
+ * @param {string} profileid the id you want associated with this profile
1289
+ * @param {string} profileval the profile
1290
+ * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1291
+ * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1292
+ * @param {string} [callback] function to be called after the LRS responds
1293
+ * to this request (makes the call asynchronous)
1294
+ * the function will be passed the XMLHttpRequest object
1295
+ * @return {object} false if no agent profile is included
1296
+ * @example
1297
+ * let profile = {"info":"the agent profile"};
1298
+ * ADL.XAPIWrapper.sendAgentProfile({"mbox":"mailto:tom@example.com"},
1299
+ * "agentprofile", profile, null, "*");
1300
+ */
1301
+ XAPIWrapper.prototype.sendAgentProfile = function (
1302
+ agent,
1303
+ profileid,
1304
+ profileval,
1305
+ matchHash,
1306
+ noneMatchHash,
1307
+ callback
1308
+ ) {
1309
+ if (this.testConfig()) {
1310
+ let url =
1311
+ this.lrs.endpoint + 'agents/profile?agent=<agent>&profileId=<profileid>'
1312
+
1313
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1314
+ url = url.replace('<profileid>', encodeURIComponent(profileid))
1315
+
1316
+ let headers = null
1317
+ if (matchHash && noneMatchHash) {
1318
+ log("Can't have both If-Match and If-None-Match")
1319
+ } else if (matchHash) {
1320
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
1321
+ } else if (noneMatchHash) {
1322
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1323
+ }
1324
+
1325
+ let method = 'PUT'
1326
+ if (profileval) {
1327
+ if (profileval instanceof Array) {
1328
+ profileval = JSON.stringify(profileval)
1329
+ headers = headers || {}
1330
+ headers['Content-Type'] = 'application/json'
1331
+ } else if (profileval instanceof Object) {
1332
+ profileval = JSON.stringify(profileval)
1333
+ headers = headers || {}
1334
+ headers['Content-Type'] = 'application/json'
1335
+ method = 'POST'
1336
+ } else {
1337
+ headers = headers || {}
1338
+ headers['Content-Type'] = 'application/octet-stream'
1339
+ }
1340
+ } else {
1341
+ this.log('No agent profile was included.')
1342
+ return false
1343
+ }
1344
+
1345
+ ADL.XHR_request(
1346
+ this.lrs,
1347
+ url,
1348
+ method,
1349
+ profileval,
1350
+ this.lrs.auth,
1351
+ callback,
1352
+ null,
1353
+ false,
1354
+ headers,
1355
+ this.withCredentials,
1356
+ this.strictCallbacks
1357
+ )
1358
+ }
1359
+ }
1360
+
1361
+ /*
1362
+ * Get agnet profile from the LRS
1363
+ * @param {object} agent the agent associated with this profile
1364
+ * @param {string} [profileid] the id of the profile, if not included, the response will be a list of profileids
1365
+ * associated with the agent
1366
+ * @param {object} [since] date object or date string telling the LRS to return objects newer than the date supplied
1367
+ * @param {function} [callback] function to be called after the LRS responds
1368
+ * to this request (makes the call asynchronous)
1369
+ * the function will be passed the XMLHttpRequest object
1370
+ * @return {object} xhr response object or null if 404
1371
+ * @example
1372
+ * ADL.XAPIWrapper.getAgentProfile({"mbox":"mailto:tom@example.com"},
1373
+ * "agentprofile", null,
1374
+ * function(r){ADL.XAPIWrapper.log(JSON.parse(r.response));});
1375
+ * >> {info: "the agent profile"}
1376
+ */
1377
+ XAPIWrapper.prototype.getAgentProfile = function (
1378
+ agent,
1379
+ profileid,
1380
+ since,
1381
+ callback
1382
+ ) {
1383
+ if (this.testConfig()) {
1384
+ let url = this.lrs.endpoint + 'agents/profile?agent=<agent>'
1385
+
1386
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1387
+ url = url.replace('<profileid>', encodeURIComponent(profileid))
1388
+
1389
+ if (profileid) {
1390
+ url += '&profileId=' + encodeURIComponent(profileid)
1391
+ }
1392
+
1393
+ if (since) {
1394
+ since = isDate(since)
1395
+ if (since != null) {
1396
+ url += '&since=' + encodeURIComponent(since.toISOString())
1397
+ }
1398
+ }
1399
+
1400
+ let result = ADL.XHR_request(
1401
+ this.lrs,
1402
+ url,
1403
+ 'GET',
1404
+ null,
1405
+ this.lrs.auth,
1406
+ callback,
1407
+ null,
1408
+ true,
1409
+ null,
1410
+ this.withCredentials,
1411
+ this.strictCallbacks
1412
+ )
1413
+
1414
+ if (result === undefined || result.status == 404) {
1415
+ return null
1416
+ }
1417
+
1418
+ try {
1419
+ return JSON.parse(result.response)
1420
+ } catch (e) {
1421
+ return result.response
1422
+ }
1423
+ }
1424
+ }
1425
+
1426
+ /*
1427
+ * Delete agent profile in the LRS
1428
+ * @param {oject} agent the id of the Agent this profile is about
1429
+ * @param {string} profileid the id you want associated with this profile
1430
+ * @param {string} [matchHash] the hash of the profile to replace or * to replace any
1431
+ * @param {string} [noneMatchHash] the hash of the current profile or * to indicate no previous profile
1432
+ * @param {string} [callback] function to be called after the LRS responds
1433
+ * to this request (makes the call asynchronous)
1434
+ * the function will be passed the XMLHttpRequest object
1435
+ * @return {object} xhr response object or null if 404
1436
+ * @example
1437
+ * ADL.XAPIWrapper.deleteAgentProfile({"mbox":"mailto:tom@example.com"},
1438
+ * "agentprofile");
1439
+ * >> XMLHttpRequest {statusText: "NO CONTENT", status: 204, response: "", responseType: "", responseXML: null…}
1440
+ */
1441
+ XAPIWrapper.prototype.deleteAgentProfile = function (
1442
+ agent,
1443
+ profileid,
1444
+ matchHash,
1445
+ noneMatchHash,
1446
+ callback
1447
+ ) {
1448
+ if (this.testConfig()) {
1449
+ let url =
1450
+ this.lrs.endpoint + 'agents/profile?agent=<agent>&profileId=<profileid>'
1451
+
1452
+ url = url.replace('<agent>', encodeURIComponent(JSON.stringify(agent)))
1453
+ url = url.replace('<profileid>', encodeURIComponent(profileid))
1454
+
1455
+ let headers = null
1456
+ if (matchHash && noneMatchHash) {
1457
+ log("Can't have both If-Match and If-None-Match")
1458
+ } else if (matchHash) {
1459
+ headers = { 'If-Match': ADL.formatHash(matchHash) }
1460
+ } else if (noneMatchHash) {
1461
+ headers = { 'If-None-Match': ADL.formatHash(noneMatchHash) }
1462
+ }
1463
+
1464
+ let result = ADL.XHR_request(
1465
+ this.lrs,
1466
+ url,
1467
+ 'DELETE',
1468
+ null,
1469
+ this.lrs.auth,
1470
+ callback,
1471
+ null,
1472
+ false,
1473
+ headers,
1474
+ this.withCredentials,
1475
+ this.strictCallbacks
1476
+ )
1477
+
1478
+ if (result === undefined || result.status == 404) {
1479
+ return null
1480
+ }
1481
+
1482
+ try {
1483
+ return JSON.parse(result.response)
1484
+ } catch (e) {
1485
+ return result
1486
+ }
1487
+ }
1488
+ }
1489
+
1490
+ /*
1491
+ * Tests the configuration of the lrs object
1492
+ */
1493
+ function testConfig() {
1494
+ try {
1495
+ return this.lrs.endpoint != undefined && this.lrs.endpoint != ''
1496
+ } catch (e) {
1497
+ return false
1498
+ }
1499
+ }
1500
+
1501
+ // outputs the message to the console if available
1502
+ function log(message) {
1503
+ if (!log.debug) return false
1504
+ try {
1505
+ message
1506
+ return true
1507
+ } catch (e) {
1508
+ return false
1509
+ }
1510
+ }
1511
+
1512
+ // merges two object
1513
+ function mergeRecursive(obj1, obj2) {
1514
+ for (let p in obj2) {
1515
+ if (obj2.hasOwnProperty(p) == false) continue
1516
+
1517
+ let prop = obj2[p]
1518
+ log(p + ' : ' + prop)
1519
+ try {
1520
+ // Property in destination object set; update its value.
1521
+ if (obj2[p].constructor == Object) {
1522
+ obj1[p] = mergeRecursive(obj1[p], obj2[p])
1523
+ } else {
1524
+ if (obj1 == undefined) {
1525
+ obj1 = new Object()
1526
+ }
1527
+ obj1[p] = obj2[p]
1528
+ }
1529
+ } catch (e) {
1530
+ if (obj1 == undefined) {
1531
+ obj1 = new Object()
1532
+ }
1533
+ // Property in destination object not set; create it and set its value.
1534
+ obj1[p] = obj2[p]
1535
+ }
1536
+ }
1537
+
1538
+ return obj1
1539
+ }
1540
+
1541
+ // iniitializes an lrs object with settings from
1542
+ // a config file and from the url query string
1543
+ function getLRSObject(config) {
1544
+ let lrsProps = [
1545
+ 'endpoint',
1546
+ 'auth',
1547
+ 'actor',
1548
+ 'registration',
1549
+ 'activity_id',
1550
+ 'grouping',
1551
+ 'activity_platform'
1552
+ ]
1553
+ let lrs = new Object()
1554
+ let qsVars, prop
1555
+
1556
+ qsVars = parseQueryString()
1557
+ if (qsVars !== undefined && Object.keys(qsVars).length !== 0) {
1558
+ for (let i = 0; i < lrsProps.length; i++) {
1559
+ prop = lrsProps[i]
1560
+ if (qsVars[prop]) {
1561
+ lrs[prop] = qsVars[prop]
1562
+ delete qsVars[prop]
1563
+ }
1564
+ }
1565
+ // if (Object.keys(qsVars).length !== 0) {
1566
+ // lrs.extended = qsVars;
1567
+ // }
1568
+
1569
+ lrs = mergeRecursive(config, lrs)
1570
+ } else {
1571
+ lrs = config
1572
+ }
1573
+
1574
+ return lrs
1575
+ }
1576
+
1577
+ // parses the params in the url query string
1578
+ function parseQueryString() {
1579
+ let qs, pairs, pair, ii, parsed
1580
+
1581
+ qs = window.location.search.substring(1)
1582
+
1583
+ pairs = qs.split('&')
1584
+ parsed = {}
1585
+ for (ii = 0; ii < pairs.length; ii++) {
1586
+ pair = pairs[ii].split('=')
1587
+ if (pair.length === 2 && pair[0]) {
1588
+ parsed[pair[0]] = decodeURIComponent(pair[1])
1589
+ }
1590
+ }
1591
+
1592
+ return parsed
1593
+ }
1594
+
1595
+ function delay() {
1596
+ let xhr = new XMLHttpRequest()
1597
+ let url = window.location + '?forcenocache=' + ADL.ruuid()
1598
+ xhr.open('GET', url, false)
1599
+ xhr.send(null)
1600
+ }
1601
+
1602
+ /*
1603
+ * formats a request in a way that IE will allow
1604
+ * @param {string} method the http request method (ex: "PUT", "GET")
1605
+ * @param {string} url the url to the request (ex: ADL.XAPIWrapper.lrs.endpoint + "statements")
1606
+ * @param {array} [headers] headers to include in the request
1607
+ * @param {string} [data] the body of the request, if there is one
1608
+ * @return {object} xhr response object
1609
+ */
1610
+ // function ie_request(method, url, headers, data) {
1611
+ // let newUrl = url
1612
+
1613
+ // //Everything that was on query string goes into form vars
1614
+ // let formData = new Array()
1615
+ // let qsIndex = newUrl.indexOf('?')
1616
+ // if (qsIndex > 0) {
1617
+ // formData.push(newUrl.substring(qsIndex + 1))
1618
+ // newUrl = newUrl.substring(0, qsIndex)
1619
+ // }
1620
+
1621
+ // //Method has to go on querystring, and nothing else
1622
+ // newUrl = newUrl + '?method=' + method
1623
+
1624
+ // //Headers
1625
+ // if (headers !== null) {
1626
+ // for (let headerName in headers) {
1627
+ // if (headers.hasOwnProperty(headerName))
1628
+ // formData.push(
1629
+ // headerName + '=' + encodeURIComponent(headers[headerName])
1630
+ // )
1631
+ // }
1632
+ // }
1633
+
1634
+ // //The original data is repackaged as "content" form var
1635
+ // if (data !== null) {
1636
+ // formData.push('content=' + encodeURIComponent(data))
1637
+ // }
1638
+
1639
+ // return {
1640
+ // method: 'POST',
1641
+ // url: newUrl,
1642
+ // headers: {},
1643
+ // data: formData.join('&')
1644
+ // }
1645
+ // }
1646
+
1647
+ /*!
1648
+ Excerpt from: Math.uuid.js (v1.4)
1649
+ http://www.broofa.com
1650
+ mailto:robert@broofa.com
1651
+ Copyright (c) 2010 Robert Kieffer
1652
+ Dual licensed under the MIT and GPL licenses.
1653
+ */
1654
+ ADL.ruuid = function () {
1655
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
1656
+ /[xy]/g,
1657
+ function (c) {
1658
+ let r = (Math.random() * 16) | 0,
1659
+ v = c == 'x' ? r : (r & 0x3) | 0x8
1660
+ return v.toString(16)
1661
+ }
1662
+ )
1663
+ }
1664
+
1665
+ /*
1666
+ * dateFromISOString
1667
+ * parses an ISO string into a date object
1668
+ * isostr - the ISO string
1669
+ */
1670
+ ADL.dateFromISOString = function (isostr) {
1671
+ let regexp =
1672
+ '([0-9]{4})(-([0-9]{2})(-([0-9]{2})' +
1673
+ '([T| ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(.([0-9]+))?)?' +
1674
+ '(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?'
1675
+ let d = isostr.match(new RegExp(regexp))
1676
+
1677
+ let offset = 0
1678
+ let date = new Date(d[1], 0, 1)
1679
+
1680
+ if (d[3]) {
1681
+ date.setMonth(d[3] - 1)
1682
+ }
1683
+ if (d[5]) {
1684
+ date.setDate(d[5])
1685
+ }
1686
+ if (d[7]) {
1687
+ date.setHours(d[7])
1688
+ }
1689
+ if (d[8]) {
1690
+ date.setMinutes(d[8])
1691
+ }
1692
+ if (d[10]) {
1693
+ date.setSeconds(d[10])
1694
+ }
1695
+ if (d[12]) {
1696
+ date.setMilliseconds(Number('0.' + d[12]) * 1000)
1697
+ }
1698
+ if (d[14]) {
1699
+ offset = Number(d[16]) * 60 + Number(d[17])
1700
+ offset *= d[15] == '-' ? 1 : -1
1701
+ }
1702
+
1703
+ offset -= date.getTimezoneOffset()
1704
+ let time = Number(date) + offset * 60 * 1000
1705
+
1706
+ let dateToReturn = new Date()
1707
+ dateToReturn.setTime(Number(time))
1708
+ return dateToReturn
1709
+ }
1710
+
1711
+ // Synchronous if callback is not provided (not recommended)
1712
+ /*
1713
+ * makes a request to a server (if possible, use functions provided in XAPIWrapper)
1714
+ * @param {string} lrs the lrs connection info, such as endpoint, auth, etc
1715
+ * @param {string} url the url of this request
1716
+ * @param {string} method the http request method
1717
+ * @param {string} data the payload
1718
+ * @param {string} auth the value for the Authorization header
1719
+ * @param {function} callback function to be called after the LRS responds
1720
+ * to this request (makes the call asynchronous)
1721
+ * @param {object} [callbackargs] additional javascript object to be passed to the callback function
1722
+ * @param {boolean} ignore404 allow page not found errors to pass
1723
+ * @param {object} extraHeaders other header key-values to be added to this request
1724
+ * @param {boolean} withCredentials
1725
+ * @param {boolean} strictCallbacks Callback must be executed and first param is error or null if no error
1726
+ * @return {object} xhr response object
1727
+ */
1728
+ ADL.XHR_request = function (
1729
+ lrs,
1730
+ url,
1731
+ method,
1732
+ data,
1733
+ auth,
1734
+ callback,
1735
+ callbackargs,
1736
+ ignore404,
1737
+ extraHeaders,
1738
+ withCredentials,
1739
+ strictCallbacks
1740
+ ) {
1741
+ 'use strict'
1742
+
1743
+ let xhr,
1744
+ finished = false,
1745
+ xDomainRequest = false,
1746
+ ieXDomain = false,
1747
+ ieModeRequest,
1748
+ urlparts = url.toLowerCase().match(/^(.+):\/\/([^:/]*):?(\d+)?(\/.*)?$/),
1749
+ location = window.location,
1750
+ urlPort,
1751
+ result,
1752
+ extended,
1753
+ prop,
1754
+ until
1755
+
1756
+ //Consolidate headers
1757
+ let headers = {}
1758
+ headers['Content-Type'] = 'application/json'
1759
+ headers['Authorization'] = auth
1760
+ headers['X-Experience-API-Version'] = ADL.XAPIWrapper.xapiVersion
1761
+ if (extraHeaders !== null) {
1762
+ for (let headerName in extraHeaders) {
1763
+ if (extraHeaders.hasOwnProperty(headerName))
1764
+ headers[headerName] = extraHeaders[headerName]
1765
+ }
1766
+ }
1767
+
1768
+ //See if this really is a cross domain
1769
+ xDomainRequest =
1770
+ location.protocol.toLowerCase() !== urlparts[1] ||
1771
+ location.hostname.toLowerCase() !== urlparts[2]
1772
+ if (!xDomainRequest) {
1773
+ urlPort =
1774
+ urlparts[3] === null
1775
+ ? urlparts[1] === 'http'
1776
+ ? '80'
1777
+ : '443'
1778
+ : urlparts[3]
1779
+ xDomainRequest = urlPort === location.port
1780
+ }
1781
+
1782
+ //Add extended LMS-specified values to the URL
1783
+ if (lrs !== null && lrs.extended !== undefined) {
1784
+ extended = new Array()
1785
+ for (prop in lrs.extended) {
1786
+ extended.push(prop + '=' + encodeURIComponent(lrs.extended[prop]))
1787
+ }
1788
+ if (extended.length > 0) {
1789
+ url += (url.indexOf('?') > -1 ? '&' : '?') + extended.join('&')
1790
+ }
1791
+ }
1792
+
1793
+ //If it's not cross domain or we're not using IE, use the usual XmlHttpRequest
1794
+ let windowsVersionCheck =
1795
+ window.XDomainRequest &&
1796
+ window.XMLHttpRequest &&
1797
+ new XMLHttpRequest().responseType === undefined
1798
+ if (
1799
+ !xDomainRequest ||
1800
+ windowsVersionCheck === undefined ||
1801
+ windowsVersionCheck === false
1802
+ ) {
1803
+ xhr = new XMLHttpRequest()
1804
+ xhr.withCredentials = withCredentials //allow cross domain cookie based auth
1805
+ xhr.open(method, url, callback != null)
1806
+ for (let headerName in headers) {
1807
+ xhr.setRequestHeader(headerName, headers[headerName])
1808
+ }
1809
+ }
1810
+ //Otherwise, use IE's XDomainRequest object
1811
+ else {
1812
+ ieXDomain = true
1813
+ // ieModeRequest = ie_request(method, url, headers, data)
1814
+ // xhr = new XDomainRequest()
1815
+ xhr.open(ieModeRequest.method, ieModeRequest.url)
1816
+ }
1817
+
1818
+ //Setup request callback
1819
+ function requestComplete() {
1820
+ if (!finished) {
1821
+ // may be in sync or async mode, using XMLHttpRequest or IE XDomainRequest, onreadystatechange or
1822
+ // onload or both might fire depending upon browser, just covering all bases with event hooks and
1823
+ // using 'finished' flag to avoid triggering events multiple times
1824
+ finished = true
1825
+ let notFoundOk = ignore404 && xhr.status === 404
1826
+ if (
1827
+ xhr.status === undefined ||
1828
+ (xhr.status >= 200 && xhr.status < 400) ||
1829
+ notFoundOk
1830
+ ) {
1831
+ if (callback) {
1832
+ if (callbackargs) {
1833
+ strictCallbacks
1834
+ ? callback(null, xhr, callbackargs)
1835
+ : callback(xhr, callbackargs)
1836
+ } else {
1837
+ let body
1838
+
1839
+ try {
1840
+ body = JSON.parse(xhr.responseText)
1841
+ } catch (e) {
1842
+ body = xhr.responseText
1843
+ }
1844
+
1845
+ strictCallbacks ? callback(null, xhr, body) : callback(xhr, body)
1846
+ }
1847
+ } else {
1848
+ result = xhr
1849
+ return xhr
1850
+ }
1851
+ } else {
1852
+ let warning
1853
+ try {
1854
+ warning =
1855
+ 'There was a problem communicating with the Learning Record Store. ( ' +
1856
+ xhr.status +
1857
+ ' | ' +
1858
+ xhr.response +
1859
+ ' )' +
1860
+ url
1861
+ } catch (ex) {
1862
+ warning = ex.toString()
1863
+ }
1864
+ ADL.XAPIWrapper.log(warning)
1865
+ ADL.xhrRequestOnError(
1866
+ xhr,
1867
+ method,
1868
+ url,
1869
+ callback,
1870
+ callbackargs,
1871
+ strictCallbacks
1872
+ )
1873
+ result = xhr
1874
+ return xhr
1875
+ }
1876
+ } else {
1877
+ return result
1878
+ }
1879
+ }
1880
+
1881
+ xhr.onreadystatechange = function () {
1882
+ if (xhr.readyState === 4) {
1883
+ return requestComplete()
1884
+ }
1885
+ }
1886
+
1887
+ xhr.onload = requestComplete
1888
+ xhr.onerror = requestComplete
1889
+ //xhr.onerror = ADL.xhrRequestOnError(xhr, method, url);
1890
+
1891
+ xhr.send(ieXDomain ? ieModeRequest.data : data)
1892
+
1893
+ if (!callback) {
1894
+ // synchronous
1895
+ if (ieXDomain) {
1896
+ // synchronous call in IE, with no asynchronous mode available.
1897
+ until = 1000 + new Date()
1898
+ while (new Date() < until && xhr.readyState !== 4 && !finished) {
1899
+ delay()
1900
+ }
1901
+ }
1902
+ return requestComplete()
1903
+ }
1904
+ }
1905
+
1906
+ /*
1907
+ * Holder for custom global error callback
1908
+ * @param {object} xhr xhr object or null
1909
+ * @param {string} method XMLHttpRequest request method
1910
+ * @param {string} url full endpoint url
1911
+ * @param {function} callback function to be called after the LRS responds
1912
+ * to this request (makes the call asynchronous)
1913
+ * @param {object} [callbackargs] additional javascript object to be passed to the callback function
1914
+ * @param {boolean} strictCallbacks Callback must be executed and first param is error or null if no error
1915
+ * @example
1916
+ * ADL.xhrRequestOnError = function(xhr, method, url, callback, callbackargs) {
1917
+ * console.log(xhr);
1918
+ * alert(xhr.status + " " + xhr.statusText + ": " + xhr.response);
1919
+ * };
1920
+ */
1921
+ ADL.xhrRequestOnError = function (
1922
+ xhr,
1923
+ method,
1924
+ url,
1925
+ callback,
1926
+ callbackargs,
1927
+ strictCallbacks
1928
+ ) {
1929
+ if (callback && strictCallbacks) {
1930
+ let status = xhr ? xhr.status : undefined
1931
+ let error
1932
+ if (status) {
1933
+ error = new Error('Request error: ' + xhr.status)
1934
+ } else if (status === 0 || status === null) {
1935
+ // 0 and null = aborted
1936
+ error = new Error('Request error: aborted')
1937
+ } else {
1938
+ error = new Error('Reqeust error: unknown')
1939
+ }
1940
+
1941
+ if (callbackargs) {
1942
+ callback(error, xhr, callbackargs)
1943
+ } else {
1944
+ let body
1945
+
1946
+ try {
1947
+ body = JSON.parse(xhr.responseText)
1948
+ } catch (e) {
1949
+ body = xhr.responseText
1950
+ }
1951
+
1952
+ callback(error, xhr, body)
1953
+ }
1954
+ }
1955
+ }
1956
+
1957
+ ADL.formatHash = function (hash) {
1958
+ return hash === '*' ? hash : '"' + hash + '"'
1959
+ }
1960
+
1961
+ ADL.XAPIWrapper = new XAPIWrapper(Config, false)
1962
+ //============================= END ==========================================
1963
+ }