From 59d7d87233993da2d2b5afa562cc88bf219c62d0 Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Tue, 6 Aug 2013 15:43:43 -0500 Subject: [PATCH 01/14] Simple State object test coverage --- test/js/State.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/test/js/State.js b/test/js/State.js index adefec1..c2b0c47 100644 --- a/test/js/State.js +++ b/test/js/State.js @@ -14,7 +14,9 @@ limitations under the License. */ (function () { - var session = null; + var session = null, + commonId = "testState", + commonContentString = "test content"; module("State Statics"); @@ -30,4 +32,67 @@ ok(result instanceof TinCan.State, "returns TinCan.State"); } ); + + module("State Instance"); + + test( + "state Object", + function () { + var obj = new TinCan.State (), + nullProps = [ + "id", + "contents", + "etag" + ], + i + ; + + ok(obj instanceof TinCan.State, "object is TinCan.State"); + + for (i = 0; i < nullProps.length; i += 1) { + ok(obj.hasOwnProperty(nullProps[i]), "object has property: " + nullProps[i]); + strictEqual(obj[nullProps[i]], null, "object property initial value: " + nullProps[i]); + } + + ok(obj.hasOwnProperty("updated"), "object has property: updated"); + strictEqual(obj.updated, false, "object property initial value: updated"); + + strictEqual(obj.LOG_SRC, "State", "object property LOG_SRC initial value"); + } + ); + + test( + "state variants", + function () { + var set = [ + { + name: "basic properties: string content", + instanceConfig: { + id: commonId, + contents: commonContentString + }, + checkProps: { + id: commonId, + contents: commonContentString + } + } + ], + i, + obj, + result + ; + + for (i = 0; i < set.length; i += 1) { + row = set[i]; + obj = new TinCan.State (row.instanceConfig); + + ok(obj instanceof TinCan.State, "object is TinCan.State (" + row.name + ")"); + if (typeof row.checkProps !== "undefined") { + for (key in row.checkProps) { + deepEqual(obj[key], row.checkProps[key], "object property initial value: " + key + " (" + row.name + ")"); + } + } + } + } + ); }()); From 3b31487bbc0a882fd896a1914fdf5103be3bf67b Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Wed, 7 Aug 2013 10:01:57 -0500 Subject: [PATCH 02/14] Improvements to XDomainRequest handling around aborted requests --- src/LRS.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/LRS.js b/src/LRS.js index ed2e24f..01bd798 100644 --- a/src/LRS.js +++ b/src/LRS.js @@ -461,6 +461,16 @@ TinCan client library xhr.onerror = function () { requestComplete(); }; + + // IE likes to randomly abort requests when some handlers + // aren't defined, so define them with no-ops, see: + // + // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ + // http://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified + // + xhr.ontimeout = function () {}; + xhr.onprogress = function () {}; + xhr.timeout = 0; } else { this.log("sendRequest unrecognized _requestMode: " + this._requestMode); @@ -472,8 +482,22 @@ TinCan client library // including jQuery (https://github.com/jquery/jquery/blob/1.10.2/src/ajax.js#L549 // https://github.com/jquery/jquery/blob/1.10.2/src/ajax/xhr.js#L97) // + // I'm wondering if the setTimeout wrapper suggested in the links + // above for XDR solves the random exception issue, but don't know + // for sure + // try { - xhr.send(data); + if (this._requestMode === XDR) { + setTimeout( + function () { + xhr.send(data); + }, + 0 + ); + } + else { + xhr.send(data); + } } catch (ex) { this.log("sendRequest caught send exception: " + ex); From f866ef18b28287721e2d1fab76c11e75c0ff2a39 Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Wed, 7 Aug 2013 15:37:25 -0500 Subject: [PATCH 03/14] Correct handling of Content-Type header * Breaks backwards compat, no longer send 'application/json' in every request * Default to sending 'application/octet-stream' for document APIs when no content type is specified * Add basic activity profile test coverage * DRY TinCan unit tests for version loop * Add setting etag during State construction * Adds special handling for when content type is 'application/json', for automatic serialize/deserialize of objects --- src/ActivityProfile.js | 9 +- src/LRS.js | 101 ++++++++++++++++--- src/State.js | 10 +- src/TinCan.js | 8 ++ test/complete.html | 5 +- test/index.html | 1 + test/js/ActivityProfile.js | 102 +++++++++++++++++++ test/js/State.js | 10 +- test/js/TinCan.js | 168 ++++++++++++++++++++++--------- test/single/ActivityProfile.html | 36 +++++++ 10 files changed, 380 insertions(+), 70 deletions(-) create mode 100644 test/js/ActivityProfile.js create mode 100644 test/single/ActivityProfile.html diff --git a/src/ActivityProfile.js b/src/ActivityProfile.js index a59b496..534a5e9 100644 --- a/src/ActivityProfile.js +++ b/src/ActivityProfile.js @@ -63,6 +63,12 @@ TinCan client library */ this.etag = null; + /** + @property contentType + @type String + */ + this.contentType = null; + this.init(cfg); }; ActivityProfile.prototype = { @@ -86,7 +92,8 @@ TinCan client library directProps = [ "id", "contents", - "etag" + "etag", + "contentType" ], val ; diff --git a/src/LRS.js b/src/LRS.js index 01bd798..c0f33f9 100644 --- a/src/LRS.js +++ b/src/LRS.js @@ -368,7 +368,6 @@ TinCan client library } // consolidate headers - headers["Content-Type"] = "application/json"; headers.Authorization = this.auth; if (this.version !== "0.9") { headers["X-Experience-API-Version"] = this.version; @@ -552,7 +551,10 @@ TinCan client library requestCfg = { url: "statements", - data: JSON.stringify(stmt.asVersion( this.version )) + data: JSON.stringify(stmt.asVersion( this.version )), + headers: { + "Content-Type": "application/json" + } }; if (stmt.id !== null) { requestCfg.method = "PUT"; @@ -730,7 +732,10 @@ TinCan client library requestCfg = { url: "statements", method: "POST", - data: JSON.stringify(versionedStatements) + data: JSON.stringify(versionedStatements), + headers: { + "Content-Type": "application/json" + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; @@ -1126,6 +1131,21 @@ TinCan client library // result.etag = TinCan.Utils.getSHA1String(xhr.responseText); } + + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + + if (result.contentType === "application/json") { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveState - failed to deserialize JSON: " + ex); + } + } } } @@ -1154,6 +1174,19 @@ TinCan client library // requestResult.state.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + requestResult.state.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.state.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (requestResult.state.contentType === "application/json") { + try { + requestResult.state.contents = JSON.parse(requestResult.state.contents); + } catch (ex) { + this.log("retrieveState - failed to deserialize JSON: " + ex); + } + } } } @@ -1171,6 +1204,7 @@ TinCan client library @param {Object} cfg.agent TinCan.Agent @param {String} [cfg.registration] Registration @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing state + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') @param {Function} [cfg.callback] Callback to execute on completion */ saveState: function (key, val, cfg) { @@ -1188,7 +1222,11 @@ TinCan client library return; } - if (typeof val === "object") { + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && cfg.contentType === "application/json") { val = JSON.stringify(val); } @@ -1215,15 +1253,16 @@ TinCan client library url: "activities/state", method: "PUT", params: requestParams, - data: val + data: val, + headers: { + "Content-Type": cfg.contentType + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; } if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { - requestCfg.headers = { - "If-Match": cfg.lastSHA1 - }; + requestCfg.headers["If-Match"] = cfg.lastSHA1; } return this.sendRequest(requestCfg); @@ -1347,6 +1386,19 @@ TinCan client library // result.etag = TinCan.Utils.getSHA1String(xhr.responseText); } + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + if (result.contentType === "application/json") { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex); + } + } } } @@ -1376,6 +1428,19 @@ TinCan client library // requestResult.profile.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + requestResult.profile.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (requestResult.profile.contentType === "application/json") { + try { + requestResult.profile.contents = JSON.parse(requestResult.profile.contents); + } catch (ex) { + this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex); + } + } } } @@ -1390,6 +1455,7 @@ TinCan client library @param {Object} cfg Configuration options @param {Object} cfg.activity TinCan.Activity @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') @param {Function} [cfg.callback] Callback to execute on completion */ saveActivityProfile: function (key, val, cfg) { @@ -1404,7 +1470,11 @@ TinCan client library return; } - if (typeof val === "object") { + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && cfg.contentType === "application/json") { val = JSON.stringify(val); } @@ -1415,20 +1485,19 @@ TinCan client library profileId: key, activityId: cfg.activity.id }, - data: val + data: val, + headers: { + "Content-Type": cfg.contentType + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; } if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { - requestCfg.headers = { - "If-Match": cfg.lastSHA1 - }; + requestCfg.headers["If-Match"] = cfg.lastSHA1; } else { - requestCfg.headers = { - "If-None-Match": "*" - }; + requestCfg.headers["If-None-Match"] = "*"; } return this.sendRequest(requestCfg); diff --git a/src/State.js b/src/State.js index eafb354..e602448 100644 --- a/src/State.js +++ b/src/State.js @@ -54,6 +54,12 @@ TinCan client library */ this.etag = null; + /** + @property contentType + @type String + */ + this.contentType = null; + this.init(cfg); }; State.prototype = { @@ -76,7 +82,9 @@ TinCan client library var i, directProps = [ "id", - "contents" + "contents", + "etag", + "contentType" ], val ; diff --git a/src/TinCan.js b/src/TinCan.js index ed31601..05073dc 100644 --- a/src/TinCan.js +++ b/src/TinCan.js @@ -887,6 +887,7 @@ var TinCan; @param {Object} [cfg.registration] Registration used in query, defaults to 'registration' property if empty @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing state + @param {String} [cfg.contentType] Content-Type to specify in headers @param {Function} [cfg.callback] Function to run with state */ setState: function (key, val, cfg) { @@ -922,6 +923,9 @@ var TinCan; if (typeof cfg.lastSHA1 !== "undefined") { queryCfg.lastSHA1 = cfg.lastSHA1; } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } if (typeof cfg.callback !== "undefined") { queryCfg.callback = cfg.callback; } @@ -1051,6 +1055,7 @@ var TinCan; @param {Object} [cfg.activity] Activity used in query, defaults to 'activity' property if empty @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers @param {Function} [cfg.callback] Function to run with activity profile */ setActivityProfile: function (key, val, cfg) { @@ -1082,6 +1087,9 @@ var TinCan; if (typeof cfg.lastSHA1 !== "undefined") { queryCfg.lastSHA1 = cfg.lastSHA1; } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } return lrs.saveActivityProfile(key, val, queryCfg); } diff --git a/test/complete.html b/test/complete.html index 034230e..6e6158c 100644 --- a/test/complete.html +++ b/test/complete.html @@ -60,9 +60,8 @@

- - + @@ -78,5 +77,7 @@

+ + diff --git a/test/index.html b/test/index.html index 019edc7..4e67a13 100644 --- a/test/index.html +++ b/test/index.html @@ -28,6 +28,7 @@

Single Test Files

  • TinCan
  • LRS
  • State
  • +
  • ActivityProfile
  • StatementsResult
  • Agent
  • Group
  • diff --git a/test/js/ActivityProfile.js b/test/js/ActivityProfile.js new file mode 100644 index 0000000..2c9f2f0 --- /dev/null +++ b/test/js/ActivityProfile.js @@ -0,0 +1,102 @@ +/*! + Copyright 2013 Rustici Software + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +(function () { + var session = null, + commonId = "testActivityProfile", + commonContentString = "test content", + commonContentStringType = "text/plain"; + + module("ActivityProfile Statics"); + + test( + "ActivityProfile.fromJSON", + function () { + var raw = {}, + string, + result + ; + + result = TinCan.ActivityProfile.fromJSON(JSON.stringify(raw)); + ok(result instanceof TinCan.ActivityProfile, "returns TinCan.ActivityProfile"); + } + ); + + module("ActivityProfile Instance"); + + test( + "activityProfile Object", + function () { + var obj = new TinCan.ActivityProfile (), + nullProps = [ + "id", + "contents", + "contentType", + "etag" + ], + i + ; + + ok(obj instanceof TinCan.ActivityProfile, "object is TinCan.ActivityProfile"); + + for (i = 0; i < nullProps.length; i += 1) { + ok(obj.hasOwnProperty(nullProps[i]), "object has property: " + nullProps[i]); + strictEqual(obj[nullProps[i]], null, "object property initial value: " + nullProps[i]); + } + + ok(obj.hasOwnProperty("updated"), "object has property: updated"); + strictEqual(obj.updated, false, "object property initial value: updated"); + + strictEqual(obj.LOG_SRC, "ActivityProfile", "object property LOG_SRC initial value"); + } + ); + + test( + "activityProfile variants", + function () { + var set = [ + { + name: "basic properties: string content", + instanceConfig: { + id: commonId, + contents: commonContentString, + contentType: commonContentStringType + }, + checkProps: { + id: commonId, + contents: commonContentString, + contentType: commonContentStringType + } + } + ], + i, + obj, + result + ; + + for (i = 0; i < set.length; i += 1) { + row = set[i]; + obj = new TinCan.ActivityProfile (row.instanceConfig); + + ok(obj instanceof TinCan.ActivityProfile, "object is TinCan.ActivityProfile (" + row.name + ")"); + if (typeof row.checkProps !== "undefined") { + for (key in row.checkProps) { + deepEqual(obj[key], row.checkProps[key], "object property initial value: " + key + " (" + row.name + ")"); + } + } + } + } + ); +}()); diff --git a/test/js/State.js b/test/js/State.js index c2b0c47..1ed65ed 100644 --- a/test/js/State.js +++ b/test/js/State.js @@ -16,7 +16,8 @@ (function () { var session = null, commonId = "testState", - commonContentString = "test content"; + commonContentString = "test content", + commonContentStringType = "text/plain"; module("State Statics"); @@ -42,6 +43,7 @@ nullProps = [ "id", "contents", + "contentType", "etag" ], i @@ -69,11 +71,13 @@ name: "basic properties: string content", instanceConfig: { id: commonId, - contents: commonContentString + contents: commonContentString, + contentType: commonContentStringType }, checkProps: { id: commonId, - contents: commonContentString + contents: commonContentString, + contentType: commonContentStringType } } ], diff --git a/test/js/TinCan.js b/test/js/TinCan.js index 73a55c9..aacd1af 100644 --- a/test/js/TinCan.js +++ b/test/js/TinCan.js @@ -262,13 +262,6 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doSendStatementSyncTest(version); - } - } - doGetStatementSyncTest = function (v) { test( "tincan.getStatement (sync): " + v, @@ -322,13 +315,6 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doGetStatementSyncTest(version); - } - } - doVoidStatementSyncTest = function (v) { test( "tincan.getVoidedStatement (sync): " + v, @@ -428,13 +414,6 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doVoidStatementSyncTest(version); - } - } - doSendStatementAsyncTest = function (v) { asyncTest( "sendStatement (prepared, async): " + v, @@ -487,13 +466,6 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doSendStatementAsyncTest(version); - } - } - doGetStatementAsyncTest = function (v) { asyncTest( "getStatement (async): " + v, @@ -550,19 +522,12 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doGetStatementAsyncTest(version); - } - } - doStateSyncTest = function (v) { test( - "tincan state (prepared, sync): " + v, + "tincan state (sync): " + v, function () { var setResult, - key = "setState (prepared, sync)", + key = "setState (sync)", val = "TinCanJS", mbox ="mailto:tincanjs-test-tincan+" + Date.now() + "@tincanapi.com", options = { @@ -584,6 +549,10 @@ ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); getResult = session[v].getState(key, options); + ok(getResult.hasOwnProperty("state"), "getResult has property: state (" + v + ")"); + ok(getResult.state instanceof TinCan.State, "getResult state property is TinCan.State (" + v + ")"); + deepEqual(getResult.state.contents, val, "getResult state property contents (" + v + ")"); + deepEqual(getResult.state.contentType, "application/octet-stream", "getResult state property contentType (" + v + ")"); // // reset the state to make sure we test the concurrency handling @@ -597,19 +566,63 @@ ); }; - for (i = 0; i < versions.length; i += 1) { - version = versions[i]; - if (TinCanTestCfg.recordStores[version]) { - doStateSyncTest(version); - } - } + doStateSyncContentTypeJSONTest = function (v) { + test( + "tincan state (sync): " + v, + function () { + var setResult, + key = "setState (sync, json content)", + val = { + testObj: { + key1: "val1" + }, + testBool: true, + testNum: 1 + }, + mbox ="mailto:tincanjs-test-tincan+" + Date.now() + "@tincanapi.com", + options = { + contentType: "application/json", + agent: new TinCan.Agent( + { + mbox: mbox + } + ), + activity: new TinCan.Activity( + { + id: "http://tincanapi.com/TinCanJS/Test/TinCan_setState/syncContentType/" + v + } + ) + }; + + setResult = session[v].setState(key, val, options); + + ok(setResult.hasOwnProperty("err"), "setResult has property: err (" + v + ")"); + ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); + + getResult = session[v].getState(key, options); + ok(getResult.hasOwnProperty("state"), "getResult has property: state (" + v + ")"); + ok(getResult.state instanceof TinCan.State, "getResult state property is TinCan.State (" + v + ")"); + deepEqual(getResult.state.contents, val, "getResult state property contents (" + v + ")"); + deepEqual(getResult.state.contentType, "application/json", "getResult state property contentType (" + v + ")"); + + // + // reset the state to make sure we test the concurrency handling + // + options.lastSHA1 = getResult.state.etag; + setResult = session[v].setState(key, val + 1, options); + delete options.lastSHA1; + + deleteResult = session[v].deleteState(key, options); + } + ); + }; doActivityProfileSyncTest = function (v) { test( - "tincan activityProfile (prepared, sync): " + v, + "tincan activityProfile (sync): " + v, function () { var setResult, - key = "activityProfile (prepared, sync)", + key = "activityProfile (sync)", val = "TinCanJS", options = { activity: new TinCan.Activity( @@ -625,6 +638,10 @@ ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); getResult = session[v].getActivityProfile(key, options); + ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); + ok(getResult.profile instanceof TinCan.ActivityProfile, "getResult profile property is TinCan.ActivityProfile (" + v + ")"); + deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); + deepEqual(getResult.profile.contentType, "application/octet-stream", "getResult profile property contentType (" + v + ")"); // this should "fail" session[v].recordStores[0].alertOnRequestFailure = false; @@ -632,7 +649,57 @@ session[v].recordStores[0].alertOnRequestFailure = true; // - // reset the state to make sure we test the concurrency handling + // reset the profile to make sure we test the concurrency handling + // + options.lastSHA1 = getResult.profile.etag; + setResult = session[v].setActivityProfile(key, val + 2, options); + delete options.lastSHA1; + + deleteResult = session[v].deleteActivityProfile(key, options); + } + ); + }; + + doActivityProfileSyncContentTypeJSONTest = function (v) { + test( + "tincan activityProfile (sync): " + v, + function () { + var setResult, + key = "activityProfile (sync)", + val = { + testObj: { + key1: "val1" + }, + testBool: true, + testNum: 1 + }, + options = { + activity: new TinCan.Activity( + { + id: "http://tincanapi.com/TinCanJS/Test/TinCan_setActivityProfile/sync/" + v + } + ), + contentType: "application/json" + }; + + setResult = session[v].setActivityProfile(key, val, options); + + ok(setResult.hasOwnProperty("err"), "setResult has property: err (" + v + ")"); + ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); + + getResult = session[v].getActivityProfile(key, options); + ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); + ok(getResult.profile instanceof TinCan.ActivityProfile, "getResult profile property is TinCan.ActivityProfile (" + v + ")"); + deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); + deepEqual(getResult.profile.contentType, "application/json", "getResult profile property contentType (" + v + ")"); + + // this should "fail" + session[v].recordStores[0].alertOnRequestFailure = false; + setResult = session[v].setActivityProfile(key, val + 1, options); + session[v].recordStores[0].alertOnRequestFailure = true; + + // + // reset the profile to make sure we test the concurrency handling // options.lastSHA1 = getResult.profile.etag; setResult = session[v].setActivityProfile(key, val + 2, options); @@ -646,8 +713,15 @@ for (i = 0; i < versions.length; i += 1) { version = versions[i]; if (TinCanTestCfg.recordStores[version]) { + doSendStatementSyncTest(version); + doGetStatementSyncTest(version); + doVoidStatementSyncTest(version); + doSendStatementAsyncTest(version); + doGetStatementAsyncTest(version); + doStateSyncTest(version); + doStateSyncContentTypeJSONTest(version); doActivityProfileSyncTest(version); + doActivityProfileSyncContentTypeJSONTest(version); } } - }()); diff --git a/test/single/ActivityProfile.html b/test/single/ActivityProfile.html new file mode 100644 index 0000000..c6caf2c --- /dev/null +++ b/test/single/ActivityProfile.html @@ -0,0 +1,36 @@ + + + + + TinCanJS Tests (ActivityProfile) + + + + + + +

    TinCanJS Test Suite (ActivityProfile)

    +

    +
    +

    +
      + + + + + + From 8864c87693999e41d834e8c3f90e35881cdc220f Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Thu, 8 Aug 2013 07:49:58 -0500 Subject: [PATCH 04/14] Minor syntax cleanup --- src/LRS.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/LRS.js b/src/LRS.js index c0f33f9..bba1c8d 100644 --- a/src/LRS.js +++ b/src/LRS.js @@ -1281,8 +1281,8 @@ TinCan client library */ dropState: function (key, cfg) { this.log("dropState"); - var requestParams = {}, - requestCfg = {} + var requestParams, + requestCfg ; // TODO: it would be better to make a subclass that knows @@ -1514,8 +1514,8 @@ TinCan client library */ dropActivityProfile: function (key, cfg) { this.log("dropActivityProfile"); - var requestParams = {}, - requestCfg = {} + var requestParams, + requestCfg ; // TODO: it would be better to make a subclass that knows @@ -1527,11 +1527,9 @@ TinCan client library } requestParams = { + profileId: key, activityId: cfg.activity.id }; - if (key !== null) { - requestParams.profileId = key; - } requestCfg = { url: "activities/profile", From b6d1eacf6d2a183187f52a06b485e9aa5509cea5 Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Thu, 8 Aug 2013 07:53:20 -0500 Subject: [PATCH 05/14] Implement AgentProfile --- build.js | 1 + src/AgentProfile.js | 133 +++++++++++++++++++ src/LRS.js | 236 +++++++++++++++++++++++++++++++++- src/TinCan.js | 150 +++++++++++++++++++++ test/complete.html | 2 + test/index.html | 1 + test/js/AgentProfile.js | 102 +++++++++++++++ test/js/TinCan.js | 103 ++++++++++++++- test/single/AgentProfile.html | 36 ++++++ 9 files changed, 761 insertions(+), 3 deletions(-) create mode 100644 src/AgentProfile.js create mode 100644 test/js/AgentProfile.js create mode 100644 test/single/AgentProfile.html diff --git a/build.js b/build.js index e2507db..8935a4b 100755 --- a/build.js +++ b/build.js @@ -34,6 +34,7 @@ new gear.Queue( ,'src/StatementsResult.js' ,'src/State.js' ,'src/ActivityProfile.js' + ,'src/AgentProfile.js' ] ) .log("Linting") diff --git a/src/AgentProfile.js b/src/AgentProfile.js new file mode 100644 index 0000000..93a6f94 --- /dev/null +++ b/src/AgentProfile.js @@ -0,0 +1,133 @@ +/* + Copyright 2013 Rustici Software + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/** +TinCan client library + +@module TinCan +@submodule TinCan.AgentProfile +**/ +(function () { + "use strict"; + + /** + @class TinCan.AgentProfile + @constructor + */ + var AgentProfile = TinCan.AgentProfile = function (cfg) { + this.log("constructor"); + + /** + @property id + @type String + */ + this.id = null; + + /** + @property agent + @type TinCan.Agent + */ + this.agent = null; + + /** + @property updated + @type String + */ + this.updated = null; + + /** + @property contents + @type String + */ + this.contents = null; + + /** + SHA1 of contents as provided by the server during last fetch, + this should be passed through to saveAgentProfile + + @property etag + @type String + */ + this.etag = null; + + /** + @property contentType + @type String + */ + this.contentType = null; + + this.init(cfg); + }; + AgentProfile.prototype = { + /** + @property LOG_SRC + */ + LOG_SRC: 'AgentProfile', + + /** + @method log + */ + log: TinCan.prototype.log, + + /** + @method init + @param {Object} [options] Configuration used to initialize + */ + init: function (cfg) { + this.log("init"); + var i, + directProps = [ + "id", + "contents", + "etag", + "contentType" + ], + val + ; + + cfg = cfg || {}; + + if (cfg.hasOwnProperty("agent")) { + if (cfg.agent instanceof TinCan.Agent) { + this.agent = cfg.agent; + } + else { + this.agent = new TinCan.Agent (cfg.agent); + } + } + + for (i = 0; i < directProps.length; i += 1) { + if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) { + this[directProps[i]] = cfg[directProps[i]]; + } + } + + this.updated = false; + } + }; + + /** + @method fromJSON + @return {Object} AgentProfile + @static + */ + AgentProfile.fromJSON = function (stateJSON) { + AgentProfile.prototype.log("fromJSON"); + var _state = JSON.parse(stateJSON); + + return new AgentProfile(_state); + }; +}()); diff --git a/src/LRS.js b/src/LRS.js index bba1c8d..e20be32 100644 --- a/src/LRS.js +++ b/src/LRS.js @@ -1,5 +1,5 @@ /* - Copyright 2012 Rustici Software + Copyright 2012-2013 Rustici Software Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1543,6 +1543,240 @@ TinCan client library return this.sendRequest(requestCfg); }, + /** + Retrieve an agent profile value, when used from a browser sends to the endpoint using the RESTful interface. + + @method retrieveAgentProfile + @param {String} key Key of agent profile to retrieve + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {Function} [cfg.callback] Callback to execute on completion + @return {Object} Value retrieved + */ + retrieveAgentProfile: function (key, cfg) { + this.log("retrieveAgentProfile"); + var requestCfg = {}, + requestResult, + callbackWrapper + ; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + requestCfg = { + method: "GET", + params: { + profileId: key + }, + ignore404: true + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestCfg.params.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestCfg.params.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + callbackWrapper = function (err, xhr) { + var result = xhr; + + if (err === null) { + if (xhr.status === 404) { + result = null; + } + else { + result = new TinCan.AgentProfile( + { + id: key, + agent: cfg.agent, + contents: xhr.responseText + } + ); + if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("ETag") !== null && xhr.getResponseHeader("ETag") !== "") { + result.etag = xhr.getResponseHeader("ETag"); + } else { + // + // either XHR didn't have getResponseHeader (probably cause it is an IE + // XDomainRequest object which doesn't) or not populated by LRS so create + // the hash ourselves + // + result.etag = TinCan.Utils.getSHA1String(xhr.responseText); + } + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + if (result.contentType === "application/json") { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveAgentProfile - failed to deserialize JSON: " + ex); + } + } + } + } + + cfg.callback(err, result); + }; + requestCfg.callback = callbackWrapper; + } + + requestResult = this.sendRequest(requestCfg); + if (! callbackWrapper) { + requestResult.profile = null; + if (requestResult.err === null && requestResult.xhr.status !== 404) { + requestResult.profile = new TinCan.AgentProfile( + { + id: key, + agent: cfg.agent, + contents: requestResult.xhr.responseText + } + ); + if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("ETag") !== null && requestResult.xhr.getResponseHeader("ETag") !== "") { + requestResult.profile.etag = requestResult.xhr.getResponseHeader("ETag"); + } else { + // + // either XHR didn't have getResponseHeader (probably cause it is an IE + // XDomainRequest object which doesn't) or not populated by LRS so create + // the hash ourselves + // + requestResult.profile.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); + } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + requestResult.profile.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (requestResult.profile.contentType === "application/json") { + try { + requestResult.profile.contents = JSON.parse(requestResult.profile.contents); + } catch (ex) { + this.log("retrieveAgentProfile - failed to deserialize JSON: " + ex); + } + } + } + } + + return requestResult; + }, + + /** + Save an agent profile value, when used from a browser sends to the endpoint using the RESTful interface. + + @method saveAgentProfile + @param {String} key Key of agent profile to retrieve + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') + @param {Function} [cfg.callback] Callback to execute on completion + */ + saveAgentProfile: function (key, val, cfg) { + this.log("saveAgentProfile"); + var requestCfg; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && cfg.contentType === "application/json") { + val = JSON.stringify(val); + } + + requestCfg = { + method: "PUT", + params: { + profileId: key + }, + data: val, + headers: { + "Content-Type": cfg.contentType + } + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestCfg.params.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestCfg.params.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + requestCfg.callback = cfg.callback; + } + if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { + requestCfg.headers["If-Match"] = cfg.lastSHA1; + } + else { + requestCfg.headers["If-None-Match"] = "*"; + } + + return this.sendRequest(requestCfg); + }, + + /** + Drop an agent profile value or all of the agent profile, when used from a browser sends to the endpoint using the RESTful interface. + + @method dropAgentProfile + @param {String|null} key Key of agent profile to delete, or null for all + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {Function} [cfg.callback] Callback to execute on completion + */ + dropAgentProfile: function (key, cfg) { + this.log("dropAgentProfile"); + var requestParams, + requestCfg + ; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + requestParams = { + profileId: key + }; + requestCfg = { + method: "DELETE", + params: requestParams + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestParams.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestParams.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + requestCfg.callback = cfg.callback; + } + + return this.sendRequest(requestCfg); + }, + /** Non-environment safe method used to create a delay to give impression of synchronous response diff --git a/src/TinCan.js b/src/TinCan.js index 05073dc..a02b8a4 100644 --- a/src/TinCan.js +++ b/src/TinCan.js @@ -1148,6 +1148,156 @@ var TinCan; else { this.log(msg); } + }, + + /** + @method getAgentProfile + @param {String} key Key to retrieve from the profile + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {Function} [cfg.callback] Function to run with agent profile + */ + getAgentProfile: function (key, cfg) { + this.log("getAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profiles (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + + return lrs.retrieveAgentProfile(key, queryCfg); + } + + msg = "[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } + }, + + /** + @method setAgentProfile + @param {String} key Key to store into the agent profile + @param {String|Object} val Value to store into the agent profile, objects will be stringified to JSON + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers + @param {Function} [cfg.callback] Function to run with agent profile + */ + setAgentProfile: function (key, val, cfg) { + this.log("setAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profile (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + if (typeof cfg.lastSHA1 !== "undefined") { + queryCfg.lastSHA1 = cfg.lastSHA1; + } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } + + return lrs.saveAgentProfile(key, val, queryCfg); + } + + msg = "[warning] setAgentProfile: No LRSs added yet (agent profile not saved)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } + }, + + /** + @method deleteAgentProfile + @param {String|null} key Key to remove from the agent profile, or null to clear all + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {Function} [cfg.callback] Function to run with agent profile + */ + deleteAgentProfile: function (key, cfg) { + this.log("deleteAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profile (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + + return lrs.dropAgentProfile(key, queryCfg); + } + + msg = "[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } } }; diff --git a/test/complete.html b/test/complete.html index 6e6158c..a28ab3f 100644 --- a/test/complete.html +++ b/test/complete.html @@ -49,6 +49,7 @@ + --> @@ -62,6 +63,7 @@

      + diff --git a/test/index.html b/test/index.html index 4e67a13..9bf2702 100644 --- a/test/index.html +++ b/test/index.html @@ -29,6 +29,7 @@

      Single Test Files

    1. LRS
    2. State
    3. ActivityProfile
    4. +
    5. AgentProfile
    6. StatementsResult
    7. Agent
    8. Group
    9. diff --git a/test/js/AgentProfile.js b/test/js/AgentProfile.js new file mode 100644 index 0000000..971067c --- /dev/null +++ b/test/js/AgentProfile.js @@ -0,0 +1,102 @@ +/*! + Copyright 2013 Rustici Software + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +(function () { + var session = null, + commonId = "testAgentProfile", + commonContentString = "test content", + commonContentStringType = "text/plain"; + + module("AgentProfile Statics"); + + test( + "AgentProfile.fromJSON", + function () { + var raw = {}, + string, + result + ; + + result = TinCan.AgentProfile.fromJSON(JSON.stringify(raw)); + ok(result instanceof TinCan.AgentProfile, "returns TinCan.AgentProfile"); + } + ); + + module("AgentProfile Instance"); + + test( + "agentProfile Object", + function () { + var obj = new TinCan.AgentProfile (), + nullProps = [ + "id", + "contents", + "contentType", + "etag" + ], + i + ; + + ok(obj instanceof TinCan.AgentProfile, "object is TinCan.AgentProfile"); + + for (i = 0; i < nullProps.length; i += 1) { + ok(obj.hasOwnProperty(nullProps[i]), "object has property: " + nullProps[i]); + strictEqual(obj[nullProps[i]], null, "object property initial value: " + nullProps[i]); + } + + ok(obj.hasOwnProperty("updated"), "object has property: updated"); + strictEqual(obj.updated, false, "object property initial value: updated"); + + strictEqual(obj.LOG_SRC, "AgentProfile", "object property LOG_SRC initial value"); + } + ); + + test( + "agentProfile variants", + function () { + var set = [ + { + name: "basic properties: string content", + instanceConfig: { + id: commonId, + contents: commonContentString, + contentType: commonContentStringType + }, + checkProps: { + id: commonId, + contents: commonContentString, + contentType: commonContentStringType + } + } + ], + i, + obj, + result + ; + + for (i = 0; i < set.length; i += 1) { + row = set[i]; + obj = new TinCan.AgentProfile (row.instanceConfig); + + ok(obj instanceof TinCan.AgentProfile, "object is TinCan.AgentProfile (" + row.name + ")"); + if (typeof row.checkProps !== "undefined") { + for (key in row.checkProps) { + deepEqual(obj[key], row.checkProps[key], "object property initial value: " + key + " (" + row.name + ")"); + } + } + } + } + ); +}()); diff --git a/test/js/TinCan.js b/test/js/TinCan.js index aacd1af..5ec203f 100644 --- a/test/js/TinCan.js +++ b/test/js/TinCan.js @@ -31,7 +31,9 @@ doSendStatementAsyncTest, doGetStatementAsyncTest, doStateSyncTest, - doActivityProfileSyncTest; + doStateSyncContentTypeJSONTest, + doActivityProfileSyncTest, + doActivityProfileSyncContentTypeJSONTest; module("TinCan Statics"); @@ -662,7 +664,7 @@ doActivityProfileSyncContentTypeJSONTest = function (v) { test( - "tincan activityProfile (sync): " + v, + "tincan activityProfile (sync, JSON content type): " + v, function () { var setResult, key = "activityProfile (sync)", @@ -710,6 +712,101 @@ ); }; + doAgentProfileSyncTest = function (v) { + test( + "tincan agentProfile (sync): " + v, + function () { + var setResult, + key = "agentProfile (sync)", + val = "TinCanJS", + mbox ="mailto:tincanjs-test-tincan+" + Date.now() + "@tincanapi.com", + options = { + agent: new TinCan.Agent( + { + mbox: mbox + } + ) + }; + + setResult = session[v].setAgentProfile(key, val, options); + + ok(setResult.hasOwnProperty("err"), "setResult has property: err (" + v + ")"); + ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); + + getResult = session[v].getAgentProfile(key, options); + ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); + ok(getResult.profile instanceof TinCan.AgentProfile, "getResult profile property is TinCan.AgentProfile (" + v + ")"); + deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); + deepEqual(getResult.profile.contentType, "application/octet-stream", "getResult profile property contentType (" + v + ")"); + + // this should "fail" + session[v].recordStores[0].alertOnRequestFailure = false; + setResult = session[v].setAgentProfile(key, val + 1, options); + session[v].recordStores[0].alertOnRequestFailure = true; + + // + // reset the profile to make sure we test the concurrency handling + // + options.lastSHA1 = getResult.profile.etag; + setResult = session[v].setAgentProfile(key, val + 2, options); + delete options.lastSHA1; + + deleteResult = session[v].deleteAgentProfile(key, options); + } + ); + }; + + doAgentProfileSyncContentTypeJSONTest = function (v) { + test( + "tincan agentProfile (sync, JSON content type): " + v, + function () { + var setResult, + key = "agentProfile (sync)", + val = { + testObj: { + key1: "val1" + }, + testBool: true, + testNum: 1 + }, + mbox ="mailto:tincanjs-test-tincan+" + Date.now() + "@tincanapi.com", + options = { + agent: new TinCan.Agent( + { + mbox: mbox + } + ), + contentType: "application/json" + }; + + setResult = session[v].setAgentProfile(key, val, options); + + ok(setResult.hasOwnProperty("err"), "setResult has property: err (" + v + ")"); + ok(setResult.hasOwnProperty("xhr"), "setResult has property: xhr (" + v + ")"); + + getResult = session[v].getAgentProfile(key, options); + ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); + ok(getResult.profile instanceof TinCan.AgentProfile, "getResult profile property is TinCan.AgentProfile (" + v + ")"); + deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); + deepEqual(getResult.profile.contentType, "application/json", "getResult profile property contentType (" + v + ")"); + + // this should "fail" + session[v].recordStores[0].alertOnRequestFailure = false; + setResult = session[v].setAgentProfile(key, val + 1, options); + session[v].recordStores[0].alertOnRequestFailure = true; + + // + // reset the profile to make sure we test the concurrency handling + // + options.lastSHA1 = getResult.profile.etag; + setResult = session[v].setAgentProfile(key, val + 2, options); + delete options.lastSHA1; + + deleteResult = session[v].deleteAgentProfile(key, options); + } + ); + }; + for (i = 0; i < versions.length; i += 1) { version = versions[i]; if (TinCanTestCfg.recordStores[version]) { @@ -722,6 +819,8 @@ doStateSyncContentTypeJSONTest(version); doActivityProfileSyncTest(version); doActivityProfileSyncContentTypeJSONTest(version); + doAgentProfileSyncTest(version); + doAgentProfileSyncContentTypeJSONTest(version); } } }()); diff --git a/test/single/AgentProfile.html b/test/single/AgentProfile.html new file mode 100644 index 0000000..f0b3e5b --- /dev/null +++ b/test/single/AgentProfile.html @@ -0,0 +1,36 @@ + + + + + TinCanJS Tests (AgentProfile) + + + + + + +

      TinCanJS Test Suite (AgentProfile)

      +

      +
      +

      +
        + + + + + + From 578ff0e05044679eb5ef59eb0bd094b898c39ef9 Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Thu, 8 Aug 2013 14:31:14 -0500 Subject: [PATCH 06/14] Correct handling of aborted, offline, and CORS denied requests * Add unit test coverage file for testing LRS .sendRequest in specific failure scenarios where an HTTP status of 0 is hit * Adjust alert warning message depending on http status being 0 or not --- src/LRS.js | 26 +++++---- test/index.html | 5 ++ test/js/TinCan.js | 1 - test/js/offline.js | 121 +++++++++++++++++++++++++++++++++++++++ test/single/offline.html | 36 ++++++++++++ 5 files changed, 176 insertions(+), 13 deletions(-) create mode 100644 test/js/offline.js create mode 100644 test/single/offline.html diff --git a/src/LRS.js b/src/LRS.js index e20be32..27cfada 100644 --- a/src/LRS.js +++ b/src/LRS.js @@ -263,8 +263,8 @@ TinCan client library Method used to send a request via browser objects to the LRS @method sendRequest - @param {Object} [cfg] Configuration for request - @param {String} [cfg.url] URL portion to add to endpoint + @param {Object} cfg Configuration for request + @param {String} cfg.url URL portion to add to endpoint @param {String} [cfg.method] GET, PUT, POST, etc. @param {Object} [cfg.params] Parameters to set on the querystring @param {String} [cfg.data] String of body content @@ -325,19 +325,21 @@ TinCan client library } } else { - // Alert all errors except cancelled XHR requests - if (httpStatus > 0) { - requestCompleteResult = { - err: httpStatus, - xhr: xhr - }; - if (self.alertOnRequestFailure) { - alert("[warning] There was a problem communicating with the Learning Record Store. (" + httpStatus + " | " + xhr.responseText+ ")"); + requestCompleteResult = { + err: httpStatus, + xhr: xhr + }; + if (self.alertOnRequestFailure) { + if (httpStatus === 0) { + alert("[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint (" + httpStatus + ")"); } - if (cfg.callback) { - cfg.callback(httpStatus, xhr); + else { + alert("[warning] There was a problem communicating with the Learning Record Store. (" + httpStatus + " | " + xhr.responseText+ ")"); } } + if (cfg.callback) { + cfg.callback(httpStatus, xhr); + } return requestCompleteResult; } } diff --git a/test/index.html b/test/index.html index 9bf2702..5d2c9c0 100644 --- a/test/index.html +++ b/test/index.html @@ -48,5 +48,10 @@

        Single Test Files

      1. Utils
      2. +

        Special Conditions

        + + diff --git a/test/js/TinCan.js b/test/js/TinCan.js index 5ec203f..cbafa39 100644 --- a/test/js/TinCan.js +++ b/test/js/TinCan.js @@ -103,7 +103,6 @@ mockAlerts = []; alertBuiltin = window.alert; window.alert = alertFunc; - }, teardown: function () { session = null; diff --git a/test/js/offline.js b/test/js/offline.js new file mode 100644 index 0000000..9a00297 --- /dev/null +++ b/test/js/offline.js @@ -0,0 +1,121 @@ +/*! + Copyright 2013 Rustici Software + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +(function () { + var session = null, + mockAlerts = null, + alertFunc = function (msg) { + mockAlerts.push(msg); + }, + alertBuiltin; + + module( + "LRS .sendRequest to CORS denied server or offline", + { + setup: function () { + // + // endpoint here shouldn't really matter much cause + // it should be offline and not get through, alternatively + // we get the same processing for a server that denies + // CORS requests (by not specifying correct headers) which + // is why I chose what I did here, so can also use this + // test online for testing CORS failure + // + session = new TinCan.LRS ( + { + endpoint: "http://tincanapi.com", + auth: "test" + } + ); + mockAlerts = []; + alertBuiltin = window.alert; + window.alert = alertFunc; + }, + teardown: function () { + session = null; + mockAlerts = null; + window.alert = alertBuiltin; + } + } + ); + + test( + "basic (sync)", + function () { + var result = session.sendRequest( + { + url: "test" + } + ); + ok(result.hasOwnProperty("err"), "result has property: err"); + deepEqual(result.err, 0, "result property value: err"); + ok(result.hasOwnProperty("xhr"), "result has property: xhr"); + TinCanTest.assertHttpRequestType(result.xhr, "result property value: xhr"); + deepEqual(mockAlerts, ["[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint (0)"], "caught alert: 0"); + } + ); + test( + "no alert (sync)", + function () { + session.alertOnRequestFailure = false; + + var result = session.sendRequest( + { + url: "test" + } + ); + ok(result.hasOwnProperty("err"), "result has property: err"); + deepEqual(result.err, 0, "result property value: err"); + ok(result.hasOwnProperty("xhr"), "result has property: xhr"); + TinCanTest.assertHttpRequestType(result.xhr, "result property value: xhr"); + deepEqual(mockAlerts, [], "no alerts since turned off"); + } + ); + + asyncTest( + "basic (async)", + function () { + var result = session.sendRequest( + { + url: "test", + callback: function (err, xhr) { + start(); + deepEqual(err, 0, "err arg value"); + TinCanTest.assertHttpRequestType(xhr, "xhr arg value"); + deepEqual(mockAlerts, ["[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint (0)"], "caught alert: 0"); + } + } + ); + } + ); + asyncTest( + "no alert (async)", + function () { + session.alertOnRequestFailure = false; + + var result = session.sendRequest( + { + url: "test", + callback: function (err, xhr) { + start(); + deepEqual(err, 0, "err arg value"); + TinCanTest.assertHttpRequestType(xhr, "xhr arg value"); + deepEqual(mockAlerts, [], "no alerts since turned off"); + } + } + ); + } + ); +}()); diff --git a/test/single/offline.html b/test/single/offline.html new file mode 100644 index 0000000..2f33879 --- /dev/null +++ b/test/single/offline.html @@ -0,0 +1,36 @@ + + + + + TinCanJS Tests (offline) + + + + + + +

        TinCanJS Test Suite (offline)

        +

        +
        +

        +
          + + + + + + From 61a277e3494fe2f14bc1d9f7e28b446c24da6076 Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Thu, 8 Aug 2013 14:50:49 -0500 Subject: [PATCH 07/14] Round out options to TinCan constructor and docs --- src/TinCan.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/TinCan.js b/src/TinCan.js index a02b8a4..29daa8f 100644 --- a/src/TinCan.js +++ b/src/TinCan.js @@ -67,7 +67,10 @@ var TinCan; @param {String} [options.url] URL for determining launch provided configuration options @param {Array} [options.recordStores] list of pre-configured LRSes + @param {Object|TinCan.Agent} [options.actor] default actor @param {Object|TinCan.Activity} [options.activity] default activity + @param {String} [options.registration] default registration + @param {Object|TinCan.Context} [options.context] default context **/ TinCan = function (cfg) { this.log("constructor"); @@ -171,6 +174,25 @@ var TinCan; this.activity = new TinCan.Activity (cfg.activity); } } + if (cfg.hasOwnProperty("actor")) { + if (cfg.actor instanceof TinCan.Agent) { + this.actor = cfg.actor; + } + else { + this.actor = new TinCan.Agent (cfg.actor); + } + } + if (cfg.hasOwnProperty("context")) { + if (cfg.context instanceof TinCan.Context) { + this.context = cfg.context; + } + else { + this.context = new TinCan.Context (cfg.context); + } + } + if (cfg.hasOwnProperty("registration")) { + this.registration = cfg.registration; + } }, /** From 58a75389cb1192db97d84973939b8f396f5ebc6c Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Thu, 8 Aug 2013 14:52:25 -0500 Subject: [PATCH 08/14] Build 0.8.0 --- build/tincan-min.js | 2 +- build/tincan.js | 733 +++++++++++++++++++++++++++++++++++++++++--- yuidoc.json | 6 +- 3 files changed, 698 insertions(+), 43 deletions(-) diff --git a/build/tincan-min.js b/build/tincan-min.js index 114b254..53ff2fc 100644 --- a/build/tincan-min.js +++ b/build/tincan-min.js @@ -14,4 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -;var TinCan;(function(){"use strict";var _environment=null,_reservedQSParams={statementId:!0,voidedStatementId:!0,verb:!0,object:!0,registration:!0,context:!0,actor:!0,since:!0,until:!0,limit:!0,authoritative:!0,sparse:!0,instructor:!0,ascending:!0,continueToken:!0,agent:!0,activityId:!0,stateId:!0,profileId:!0,activity_platform:!0,grouping:!0,"Accept-Language":!0};TinCan=function(a){this.log("constructor"),this.environment=null,this.recordStores=[],this.actor=null,this.activity=null,this.registration=null,this.context=null,this.init(a)},TinCan.prototype={LOG_SRC:"TinCan",log:function(a,b){TinCan.DEBUG&&typeof console!="undefined"&&console.log&&(b=b||this.LOG_SRC||"TinCan",console.log("TinCan."+b+": "+a))},init:function(a){this.log("init");var b;a=a||{},a.hasOwnProperty("url")&&a.url!==""&&this._initFromQueryString(a.url);if(a.hasOwnProperty("recordStores")&&a.recordStores!==undefined)for(b=0;b0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatement - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatement - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],c.retrieveStatement(a,{callback:b});d="[warning] getStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},voidStatement:function(a,b,c){this.log("voidStatement");var d=this,e,f,g,h=this.recordStores.length,i,j,k=[],l,m=[];a instanceof TinCan.Statement&&(a=a.id),typeof c.actor!="undefined"?f=c.actor:this.actor!==null&&(f=this.actor),g=new TinCan.Statement({actor:f,verb:{id:"http://adlnet.gov/expapi/verbs/voided"},target:{objectType:"StatementRef",id:a}});if(h>0){typeof b=="function"&&(l=function(a,c){var e;d.log("voidStatement - callbackWrapper: "+h),h>1?(h-=1,m.push({err:a,xhr:c})):h===1?(m.push({err:a,xhr:c}),e=[m,g],b.apply(this,e)):d.log("voidStatement - unexpected record store count: "+h)});for(i=0;i0)return c=this.recordStores[0],c.retrieveVoidedStatement(a,{callback:b});d="[warning] getVoidedStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},sendStatements:function(a,b){this.log("sendStatements");var c=this,d,e=[],f=this.recordStores.length,g,h,i=[],j,k=[];if(a.length===0)typeof b=="function"&&b.apply(this,[null,e]);else{for(g=0;g0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatements - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatements - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],a=a||{},d=a.params||{},a.sendActor&&this.actor!==null&&(c.version==="0.9"||c.version==="0.95"?d.actor=this.actor:d.agent=this.actor),a.sendActivity&&this.activity!==null&&(c.version==="0.9"||c.version==="0.95"?d.target=this.activity:d.activity=this.activity),typeof d.registration=="undefined"&&this.registration!==null&&(d.registration=this.registration),b={params:d},typeof a.callback!="undefined"&&(b.callback=a.callback),c.queryStatements(b);e="[warning] getStatements: No LRSs added yet (statements not read)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getState:function(a,b){this.log("getState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveState(a,c);e="[warning] getState: No LRSs added yet (state not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setState:function(a,b,c){this.log("setState");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor,activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.registration!="undefined"?d.registration=c.registration:this.registration!==null&&(d.registration=this.registration),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.callback!="undefined"&&(d.callback=c.callback),e.saveState(a,b,d);f="[warning] setState: No LRSs added yet (state not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteState:function(a,b){this.log("deleteState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropState(a,c);e="[warning] deleteState: No LRSs added yet (state not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getActivityProfile:function(a,b){this.log("getActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveActivityProfile(a,c);e="[warning] getActivityProfile: No LRSs added yet (activity profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setActivityProfile:function(a,b,c){this.log("setActivityProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),e.saveActivityProfile(a,b,d);f="[warning] setActivityProfile: No LRSs added yet (activity profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteActivityProfile:function(a,b){this.log("deleteActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropActivityProfile(a,c);e="[warning] deleteActivityProfile: No LRSs added yet (activity profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)}},TinCan.DEBUG=!1,TinCan.enableDebug=function(){TinCan.DEBUG=!0},TinCan.disableDebug=function(){TinCan.DEBUG=!1},TinCan.versions=function(){return["1.0.0","0.95","0.9"]},TinCan.environment=function(){return _environment===null&&(_environment={},typeof window!="undefined"?(_environment.isBrowser=!0,_environment.hasCORS=!1,_environment.useXDR=!1,typeof XMLHttpRequest!="undefined"&&typeof (new XMLHttpRequest).withCredentials!="undefined"?_environment.hasCORS=!0:typeof XDomainRequest!="undefined"&&(_environment.hasCORS=!0,_environment.useXDR=!0)):_environment.isBrowser=!1),_environment},TinCan.environment().isBrowser&&(window.JSON||(window.JSON={parse:function(sJSON){return eval("("+sJSON+")")},stringify:function(a){var b="",c,d;if(a instanceof Object){if(a.constructor===Array){for(c=0;c1)d="0"+d,c/=10;return d}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())+"."+b(a.getUTCMilliseconds(),3)+"Z"},getSHA1String:function(a){return CryptoJS.SHA1(a).toString(CryptoJS.enc.Hex)},getBase64String:function(a){return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(a))},getLangDictionaryValue:function(a,b){var c=this[a],d;if(typeof b!="undefined"&&typeof c[b]!="undefined")return c[b];if(typeof c.und!="undefined")return c.und;if(typeof c["en-US"]!="undefined")return c["en-US"];for(d in c)if(c.hasOwnProperty(d))return c[d];return""},parseURL:function(a){var b=String(a).split("?"),c,d,e,f={};if(b.length===2){c=b[1].split("&");for(e=0;e=200&&b<400||a))return b>0&&(j={err:b,xhr:d},n.alertOnRequestFailure&&alert("[warning] There was a problem communicating with the Learning Record Store. ("+b+" | "+d.responseText+")"),c.callback&&c.callback(b,d)),j;if(!c.callback)return j={err:null,xhr:d},j;c.callback(null,d)}this.log("sendRequest");var d,e=!1,f=window.location,g=this.endpoint+c.url,h={},i,j,k,l,m=[],n=this;c.url.indexOf("http")===0&&(g=c.url);if(this.extended!==null){c.params=c.params||{};for(l in this.extended)this.extended.hasOwnProperty(l)&&(c.params.hasOwnProperty(l)||this.extended[l]!==null&&(c.params[l]=this.extended[l]))}h["Content-Type"]="application/json",h.Authorization=this.auth,this.version!=="0.9"&&(h["X-Experience-API-Version"]=this.version);for(l in c.headers)c.headers.hasOwnProperty(l)&&(h[l]=c.headers[l]);if(this._requestMode===b){this.log("sendRequest using XMLHttpRequest");for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));m.length>0&&(g+="?"+m.join("&")),this.log("sendRequest using XMLHttpRequest - async: "+(typeof c.callback!="undefined")),typeof XMLHttpRequest!="undefined"?d=new XMLHttpRequest:d=new ActiveXObject("Microsoft.XMLHTTP"),d.open(c.method,g,typeof c.callback!="undefined");for(l in h)h.hasOwnProperty(l)&&d.setRequestHeader(l,h[l]);typeof c.data!="undefined"&&(c.data+=""),i=c.data,d.onreadystatechange=function(){n.log("xhr.onreadystatechange - xhr.readyState: "+d.readyState),d.readyState===4&&o()}}else if(this._requestMode===a){this.log("sendRequest using XDomainRequest"),g+="?method="+c.method;for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));for(l in h)h.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(h[l]));c.data!==null&&m.push("content="+encodeURIComponent(c.data)),i=m.join("&"),d=new XDomainRequest,d.open("POST",g),d.onload=function(){o()},d.onerror=function(){o()}}else this.log("sendRequest unrecognized _requestMode: "+this._requestMode);try{d.send(i)}catch(p){this.log("sendRequest caught send exception: "+p)}if(!c.callback){if(this._requestMode===a){k=1e3+Date.now(),this.log("sendRequest - until: "+k+", finished: "+e);while(Date.now()0&&(a.name=a.firstName[0],a.firstName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.lastName!="undefined"&&a.lastName.length>0&&(a.name+=a.lastName[0],a.lastName.length>1&&(this.degraded=!0));else if(typeof a.familyName!="undefined"||typeof a.givenName!="undefined")a.name="",typeof a.givenName!="undefined"&&a.givenName.length>0&&(a.name=a.givenName[0],a.givenName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.familyName!="undefined"&&a.familyName.length>0&&(a.name+=a.familyName[0],a.familyName.length>1&&(this.degraded=!0));typeof a.name=="object"&&a.name!==null&&(a.name.length>1&&(this.degraded=!0),a.name=a.name[0]),typeof a.mbox=="object"&&a.mbox!==null&&(a.mbox.length>1&&(this.degraded=!0),a.mbox=a.mbox[0]),typeof a.mbox_sha1sum=="object"&&a.mbox_sha1sum!==null&&(a.mbox_sha1sum.length>1&&(this.degraded=!0),a.mbox_sha1sum=a.mbox_sha1sum[0]),typeof a.openid=="object"&&a.openid!==null&&(a.openid.length>1&&(this.degraded=!0),a.openid=a.openid[0]),typeof a.account=="object"&&a.account!==null&&typeof a.account.homePage=="undefined"&&typeof a.account.name=="undefined"&&(a.account.length===0?delete a.account:(a.account.length>1&&(this.degraded=!0),a.account=a.account[0])),a.hasOwnProperty("account")&&(a.account instanceof TinCan.AgentAccount?this.account=a.account:this.account=new TinCan.AgentAccount(a.account));for(b=0;b0){b.member=[];for(c=0;c0)for(c=0;c0)if(a==="0.9"||a==="0.95")this[c[d]].length>1&&this.log("[WARNING] version does not support multiple values in: "+c[d]),b[c[d]]=this[c[d]][0].asVersion(a);else{b[c[d]]=[];for(e=0;e>>2]|=(c[e>>>2]>>>24-8*(e%4)&255)<<24-8*((d+e)%4);else if(65535>>2]=c[e>>>2];else b.push.apply(b,c);return this.sigBytes+=a,this},clamp:function(){var b=this.words,c=this.sigBytes;b[c>>>2]&=4294967295<<32-8*(c%4),b.length=a.ceil(c/4)},clone:function(){var a=e.clone.call(this);return a.words=this.words.slice(0),a},random:function(b){for(var c=[],d=0;d>>2]>>>24-8*(d%4)&255;c.push((e>>>4).toString(16)),c.push((e&15).toString(16))}return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>3]|=parseInt(a.substr(d,2),16)<<24-4*(d%8);return f.create(c,b/2)}},i=g.Latin1={stringify:function(a){for(var b=a.words,a=a.sigBytes,c=[],d=0;d>>2]>>>24-8*(d%4)&255));return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>2]|=(a.charCodeAt(d)&255)<<24-8*(d%4);return f.create(c,b)}},j=g.Utf8={stringify:function(a){try{return decodeURIComponent(escape(i.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data")}},parse:function(a){return i.parse(unescape(encodeURIComponent(a)))}},k=d.BufferedBlockAlgorithm=e.extend({reset:function(){this._data=f.create(),this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a)),this._data.concat(a),this._nDataBytes+=a.sigBytes},_process:function(b){var c=this._data,d=c.words,e=c.sigBytes,g=this.blockSize,h=e/(4*g),h=b?a.ceil(h):a.max((h|0)-this._minBufferSize,0),b=h*g,e=a.min(4*b,e);if(b){for(var i=0;ik;k++){if(16>k)d[k]=a[b+k]|0;else{var l=d[k-3]^d[k-8]^d[k-14]^d[k-16];d[k]=l<<1|l>>>31}l=(e<<5|e>>>27)+j+d[k],l=20>k?l+((f&g|~f&i)+1518500249):40>k?l+((f^g^i)+1859775393):60>k?l+((f&g|f&i|g&i)-1894007588):l+((f^g^i)-899497514),j=i,i=g,g=f<<30|f>>>2,f=e,e=l}c[0]=c[0]+e|0,c[1]=c[1]+f|0,c[2]=c[2]+g|0,c[3]=c[3]+i|0,c[4]=c[4]+j|0},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,d=8*a.sigBytes;b[d>>>5]|=128<<24-d%32,b[(d+64>>>9<<4)+15]=c,a.sigBytes=4*b.length,this._process()}});a.SHA1=b._createHelper(e),a.HmacSHA1=b._createHmacHelper(e)})(),function(){var a=CryptoJS,b=a.lib,c=b.WordArray,d=a.enc,e=d.Base64={stringify:function(a){var b=a.words,c=a.sigBytes,d=this._map;a.clamp();var e=[];for(var f=0;f>>2]>>>24-f%4*8&255,h=b[f+1>>>2]>>>24-(f+1)%4*8&255,i=b[f+2>>>2]>>>24-(f+2)%4*8&255,j=g<<16|h<<8|i;for(var k=0;k<4&&f+k*.75>>6*(3-k)&63))}var l=d.charAt(64);if(l)while(e.length%4)e.push(l);return e.join("")},parse:function(a){a=a.replace(/\s/g,"");var b=a.length,d=this._map,e=d.charAt(64);if(e){var f=a.indexOf(e);f!=-1&&(b=f)}var g=[],h=0;for(var i=0;i>>6-i%4*2;g[h>>>2]|=(j|k)<<24-h%4*8,h++}return c.create(g,h)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(); +;var TinCan;(function(){"use strict";var _environment=null,_reservedQSParams={statementId:!0,voidedStatementId:!0,verb:!0,object:!0,registration:!0,context:!0,actor:!0,since:!0,until:!0,limit:!0,authoritative:!0,sparse:!0,instructor:!0,ascending:!0,continueToken:!0,agent:!0,activityId:!0,stateId:!0,profileId:!0,activity_platform:!0,grouping:!0,"Accept-Language":!0};TinCan=function(a){this.log("constructor"),this.environment=null,this.recordStores=[],this.actor=null,this.activity=null,this.registration=null,this.context=null,this.init(a)},TinCan.prototype={LOG_SRC:"TinCan",log:function(a,b){TinCan.DEBUG&&typeof console!="undefined"&&console.log&&(b=b||this.LOG_SRC||"TinCan",console.log("TinCan."+b+": "+a))},init:function(a){this.log("init");var b;a=a||{},a.hasOwnProperty("url")&&a.url!==""&&this._initFromQueryString(a.url);if(a.hasOwnProperty("recordStores")&&a.recordStores!==undefined)for(b=0;b0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatement - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatement - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],c.retrieveStatement(a,{callback:b});d="[warning] getStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},voidStatement:function(a,b,c){this.log("voidStatement");var d=this,e,f,g,h=this.recordStores.length,i,j,k=[],l,m=[];a instanceof TinCan.Statement&&(a=a.id),typeof c.actor!="undefined"?f=c.actor:this.actor!==null&&(f=this.actor),g=new TinCan.Statement({actor:f,verb:{id:"http://adlnet.gov/expapi/verbs/voided"},target:{objectType:"StatementRef",id:a}});if(h>0){typeof b=="function"&&(l=function(a,c){var e;d.log("voidStatement - callbackWrapper: "+h),h>1?(h-=1,m.push({err:a,xhr:c})):h===1?(m.push({err:a,xhr:c}),e=[m,g],b.apply(this,e)):d.log("voidStatement - unexpected record store count: "+h)});for(i=0;i0)return c=this.recordStores[0],c.retrieveVoidedStatement(a,{callback:b});d="[warning] getVoidedStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},sendStatements:function(a,b){this.log("sendStatements");var c=this,d,e=[],f=this.recordStores.length,g,h,i=[],j,k=[];if(a.length===0)typeof b=="function"&&b.apply(this,[null,e]);else{for(g=0;g0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatements - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatements - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],a=a||{},d=a.params||{},a.sendActor&&this.actor!==null&&(c.version==="0.9"||c.version==="0.95"?d.actor=this.actor:d.agent=this.actor),a.sendActivity&&this.activity!==null&&(c.version==="0.9"||c.version==="0.95"?d.target=this.activity:d.activity=this.activity),typeof d.registration=="undefined"&&this.registration!==null&&(d.registration=this.registration),b={params:d},typeof a.callback!="undefined"&&(b.callback=a.callback),c.queryStatements(b);e="[warning] getStatements: No LRSs added yet (statements not read)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getState:function(a,b){this.log("getState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveState(a,c);e="[warning] getState: No LRSs added yet (state not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setState:function(a,b,c){this.log("setState");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor,activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.registration!="undefined"?d.registration=c.registration:this.registration!==null&&(d.registration=this.registration),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),typeof c.callback!="undefined"&&(d.callback=c.callback),e.saveState(a,b,d);f="[warning] setState: No LRSs added yet (state not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteState:function(a,b){this.log("deleteState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropState(a,c);e="[warning] deleteState: No LRSs added yet (state not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getActivityProfile:function(a,b){this.log("getActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveActivityProfile(a,c);e="[warning] getActivityProfile: No LRSs added yet (activity profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setActivityProfile:function(a,b,c){this.log("setActivityProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveActivityProfile(a,b,d);f="[warning] setActivityProfile: No LRSs added yet (activity profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteActivityProfile:function(a,b){this.log("deleteActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropActivityProfile(a,c);e="[warning] deleteActivityProfile: No LRSs added yet (activity profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getAgentProfile:function(a,b){this.log("getAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveAgentProfile(a,c);e="[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setAgentProfile:function(a,b,c){this.log("setAgentProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveAgentProfile(a,b,d);f="[warning] setAgentProfile: No LRSs added yet (agent profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteAgentProfile:function(a,b){this.log("deleteAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropAgentProfile(a,c);e="[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)}},TinCan.DEBUG=!1,TinCan.enableDebug=function(){TinCan.DEBUG=!0},TinCan.disableDebug=function(){TinCan.DEBUG=!1},TinCan.versions=function(){return["1.0.0","0.95","0.9"]},TinCan.environment=function(){return _environment===null&&(_environment={},typeof window!="undefined"?(_environment.isBrowser=!0,_environment.hasCORS=!1,_environment.useXDR=!1,typeof XMLHttpRequest!="undefined"&&typeof (new XMLHttpRequest).withCredentials!="undefined"?_environment.hasCORS=!0:typeof XDomainRequest!="undefined"&&(_environment.hasCORS=!0,_environment.useXDR=!0)):_environment.isBrowser=!1),_environment},TinCan.environment().isBrowser&&(window.JSON||(window.JSON={parse:function(sJSON){return eval("("+sJSON+")")},stringify:function(a){var b="",c,d;if(a instanceof Object){if(a.constructor===Array){for(c=0;c1)d="0"+d,c/=10;return d}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())+"."+b(a.getUTCMilliseconds(),3)+"Z"},getSHA1String:function(a){return CryptoJS.SHA1(a).toString(CryptoJS.enc.Hex)},getBase64String:function(a){return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(a))},getLangDictionaryValue:function(a,b){var c=this[a],d;if(typeof b!="undefined"&&typeof c[b]!="undefined")return c[b];if(typeof c.und!="undefined")return c.und;if(typeof c["en-US"]!="undefined")return c["en-US"];for(d in c)if(c.hasOwnProperty(d))return c[d];return""},parseURL:function(a){var b=String(a).split("?"),c,d,e,f={};if(b.length===2){c=b[1].split("&");for(e=0;e=200&&b<400||a))return j={err:b,xhr:d},n.alertOnRequestFailure&&(b===0?alert("[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint ("+b+")"):alert("[warning] There was a problem communicating with the Learning Record Store. ("+b+" | "+d.responseText+")")),c.callback&&c.callback(b,d),j;if(!c.callback)return j={err:null,xhr:d},j;c.callback(null,d)}this.log("sendRequest");var d,e=!1,f=window.location,g=this.endpoint+c.url,h={},i,j,k,l,m=[],n=this;c.url.indexOf("http")===0&&(g=c.url);if(this.extended!==null){c.params=c.params||{};for(l in this.extended)this.extended.hasOwnProperty(l)&&(c.params.hasOwnProperty(l)||this.extended[l]!==null&&(c.params[l]=this.extended[l]))}h.Authorization=this.auth,this.version!=="0.9"&&(h["X-Experience-API-Version"]=this.version);for(l in c.headers)c.headers.hasOwnProperty(l)&&(h[l]=c.headers[l]);if(this._requestMode===b){this.log("sendRequest using XMLHttpRequest");for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));m.length>0&&(g+="?"+m.join("&")),this.log("sendRequest using XMLHttpRequest - async: "+(typeof c.callback!="undefined")),typeof XMLHttpRequest!="undefined"?d=new XMLHttpRequest:d=new ActiveXObject("Microsoft.XMLHTTP"),d.open(c.method,g,typeof c.callback!="undefined");for(l in h)h.hasOwnProperty(l)&&d.setRequestHeader(l,h[l]);typeof c.data!="undefined"&&(c.data+=""),i=c.data,d.onreadystatechange=function(){n.log("xhr.onreadystatechange - xhr.readyState: "+d.readyState),d.readyState===4&&o()}}else if(this._requestMode===a){this.log("sendRequest using XDomainRequest"),g+="?method="+c.method;for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));for(l in h)h.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(h[l]));c.data!==null&&m.push("content="+encodeURIComponent(c.data)),i=m.join("&"),d=new XDomainRequest,d.open("POST",g),d.onload=function(){o()},d.onerror=function(){o()},d.ontimeout=function(){},d.onprogress=function(){},d.timeout=0}else this.log("sendRequest unrecognized _requestMode: "+this._requestMode);try{this._requestMode===a?setTimeout(function(){d.send(i)},0):d.send(i)}catch(p){this.log("sendRequest caught send exception: "+p)}if(!c.callback){if(this._requestMode===a){k=1e3+Date.now(),this.log("sendRequest - until: "+k+", finished: "+e);while(Date.now()0&&(a.name=a.firstName[0],a.firstName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.lastName!="undefined"&&a.lastName.length>0&&(a.name+=a.lastName[0],a.lastName.length>1&&(this.degraded=!0));else if(typeof a.familyName!="undefined"||typeof a.givenName!="undefined")a.name="",typeof a.givenName!="undefined"&&a.givenName.length>0&&(a.name=a.givenName[0],a.givenName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.familyName!="undefined"&&a.familyName.length>0&&(a.name+=a.familyName[0],a.familyName.length>1&&(this.degraded=!0));typeof a.name=="object"&&a.name!==null&&(a.name.length>1&&(this.degraded=!0),a.name=a.name[0]),typeof a.mbox=="object"&&a.mbox!==null&&(a.mbox.length>1&&(this.degraded=!0),a.mbox=a.mbox[0]),typeof a.mbox_sha1sum=="object"&&a.mbox_sha1sum!==null&&(a.mbox_sha1sum.length>1&&(this.degraded=!0),a.mbox_sha1sum=a.mbox_sha1sum[0]),typeof a.openid=="object"&&a.openid!==null&&(a.openid.length>1&&(this.degraded=!0),a.openid=a.openid[0]),typeof a.account=="object"&&a.account!==null&&typeof a.account.homePage=="undefined"&&typeof a.account.name=="undefined"&&(a.account.length===0?delete a.account:(a.account.length>1&&(this.degraded=!0),a.account=a.account[0])),a.hasOwnProperty("account")&&(a.account instanceof TinCan.AgentAccount?this.account=a.account:this.account=new TinCan.AgentAccount(a.account));for(b=0;b0){b.member=[];for(c=0;c0)for(c=0;c0)if(a==="0.9"||a==="0.95")this[c[d]].length>1&&this.log("[WARNING] version does not support multiple values in: "+c[d]),b[c[d]]=this[c[d]][0].asVersion(a);else{b[c[d]]=[];for(e=0;e>>2]|=(c[e>>>2]>>>24-8*(e%4)&255)<<24-8*((d+e)%4);else if(65535>>2]=c[e>>>2];else b.push.apply(b,c);return this.sigBytes+=a,this},clamp:function(){var b=this.words,c=this.sigBytes;b[c>>>2]&=4294967295<<32-8*(c%4),b.length=a.ceil(c/4)},clone:function(){var a=e.clone.call(this);return a.words=this.words.slice(0),a},random:function(b){for(var c=[],d=0;d>>2]>>>24-8*(d%4)&255;c.push((e>>>4).toString(16)),c.push((e&15).toString(16))}return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>3]|=parseInt(a.substr(d,2),16)<<24-4*(d%8);return f.create(c,b/2)}},i=g.Latin1={stringify:function(a){for(var b=a.words,a=a.sigBytes,c=[],d=0;d>>2]>>>24-8*(d%4)&255));return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>2]|=(a.charCodeAt(d)&255)<<24-8*(d%4);return f.create(c,b)}},j=g.Utf8={stringify:function(a){try{return decodeURIComponent(escape(i.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data")}},parse:function(a){return i.parse(unescape(encodeURIComponent(a)))}},k=d.BufferedBlockAlgorithm=e.extend({reset:function(){this._data=f.create(),this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a)),this._data.concat(a),this._nDataBytes+=a.sigBytes},_process:function(b){var c=this._data,d=c.words,e=c.sigBytes,g=this.blockSize,h=e/(4*g),h=b?a.ceil(h):a.max((h|0)-this._minBufferSize,0),b=h*g,e=a.min(4*b,e);if(b){for(var i=0;ik;k++){if(16>k)d[k]=a[b+k]|0;else{var l=d[k-3]^d[k-8]^d[k-14]^d[k-16];d[k]=l<<1|l>>>31}l=(e<<5|e>>>27)+j+d[k],l=20>k?l+((f&g|~f&i)+1518500249):40>k?l+((f^g^i)+1859775393):60>k?l+((f&g|f&i|g&i)-1894007588):l+((f^g^i)-899497514),j=i,i=g,g=f<<30|f>>>2,f=e,e=l}c[0]=c[0]+e|0,c[1]=c[1]+f|0,c[2]=c[2]+g|0,c[3]=c[3]+i|0,c[4]=c[4]+j|0},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,d=8*a.sigBytes;b[d>>>5]|=128<<24-d%32,b[(d+64>>>9<<4)+15]=c,a.sigBytes=4*b.length,this._process()}});a.SHA1=b._createHelper(e),a.HmacSHA1=b._createHmacHelper(e)})(),function(){var a=CryptoJS,b=a.lib,c=b.WordArray,d=a.enc,e=d.Base64={stringify:function(a){var b=a.words,c=a.sigBytes,d=this._map;a.clamp();var e=[];for(var f=0;f>>2]>>>24-f%4*8&255,h=b[f+1>>>2]>>>24-(f+1)%4*8&255,i=b[f+2>>>2]>>>24-(f+2)%4*8&255,j=g<<16|h<<8|i;for(var k=0;k<4&&f+k*.75>>6*(3-k)&63))}var l=d.charAt(64);if(l)while(e.length%4)e.push(l);return e.join("")},parse:function(a){a=a.replace(/\s/g,"");var b=a.length,d=this._map,e=d.charAt(64);if(e){var f=a.indexOf(e);f!=-1&&(b=f)}var g=[],h=0;for(var i=0;i>>6-i%4*2;g[h>>>2]|=(j|k)<<24-h%4*8,h++}return c.create(g,h)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(); diff --git a/build/tincan.js b/build/tincan.js index bca4628..442a754 100644 --- a/build/tincan.js +++ b/build/tincan.js @@ -67,7 +67,10 @@ var TinCan; @param {String} [options.url] URL for determining launch provided configuration options @param {Array} [options.recordStores] list of pre-configured LRSes + @param {Object|TinCan.Agent} [options.actor] default actor @param {Object|TinCan.Activity} [options.activity] default activity + @param {String} [options.registration] default registration + @param {Object|TinCan.Context} [options.context] default context **/ TinCan = function (cfg) { this.log("constructor"); @@ -171,6 +174,25 @@ var TinCan; this.activity = new TinCan.Activity (cfg.activity); } } + if (cfg.hasOwnProperty("actor")) { + if (cfg.actor instanceof TinCan.Agent) { + this.actor = cfg.actor; + } + else { + this.actor = new TinCan.Agent (cfg.actor); + } + } + if (cfg.hasOwnProperty("context")) { + if (cfg.context instanceof TinCan.Context) { + this.context = cfg.context; + } + else { + this.context = new TinCan.Context (cfg.context); + } + } + if (cfg.hasOwnProperty("registration")) { + this.registration = cfg.registration; + } }, /** @@ -887,6 +909,7 @@ var TinCan; @param {Object} [cfg.registration] Registration used in query, defaults to 'registration' property if empty @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing state + @param {String} [cfg.contentType] Content-Type to specify in headers @param {Function} [cfg.callback] Function to run with state */ setState: function (key, val, cfg) { @@ -922,6 +945,9 @@ var TinCan; if (typeof cfg.lastSHA1 !== "undefined") { queryCfg.lastSHA1 = cfg.lastSHA1; } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } if (typeof cfg.callback !== "undefined") { queryCfg.callback = cfg.callback; } @@ -1051,6 +1077,7 @@ var TinCan; @param {Object} [cfg.activity] Activity used in query, defaults to 'activity' property if empty @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers @param {Function} [cfg.callback] Function to run with activity profile */ setActivityProfile: function (key, val, cfg) { @@ -1082,6 +1109,9 @@ var TinCan; if (typeof cfg.lastSHA1 !== "undefined") { queryCfg.lastSHA1 = cfg.lastSHA1; } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } return lrs.saveActivityProfile(key, val, queryCfg); } @@ -1140,6 +1170,156 @@ var TinCan; else { this.log(msg); } + }, + + /** + @method getAgentProfile + @param {String} key Key to retrieve from the profile + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {Function} [cfg.callback] Function to run with agent profile + */ + getAgentProfile: function (key, cfg) { + this.log("getAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profiles (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + + return lrs.retrieveAgentProfile(key, queryCfg); + } + + msg = "[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } + }, + + /** + @method setAgentProfile + @param {String} key Key to store into the agent profile + @param {String|Object} val Value to store into the agent profile, objects will be stringified to JSON + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers + @param {Function} [cfg.callback] Function to run with agent profile + */ + setAgentProfile: function (key, val, cfg) { + this.log("setAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profile (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + if (typeof cfg.lastSHA1 !== "undefined") { + queryCfg.lastSHA1 = cfg.lastSHA1; + } + if (typeof cfg.contentType !== "undefined") { + queryCfg.contentType = cfg.contentType; + } + + return lrs.saveAgentProfile(key, val, queryCfg); + } + + msg = "[warning] setAgentProfile: No LRSs added yet (agent profile not saved)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } + }, + + /** + @method deleteAgentProfile + @param {String|null} key Key to remove from the agent profile, or null to clear all + @param {Object} [cfg] Configuration for request + @param {Object} [cfg.agent] Agent used in query, + defaults to 'actor' property if empty + @param {Function} [cfg.callback] Function to run with agent profile + */ + deleteAgentProfile: function (key, cfg) { + this.log("deleteAgentProfile"); + var queryCfg, + lrs, + msg + ; + + if (this.recordStores.length > 0) { + // + // for agent profile (for now) we are only going to store to the first LRS + // so only get from there too + // + // TODO: make this the first non-allowFail LRS but for now it should + // be good enough to make it the first since we know the LMS provided + // LRS is the first + // + lrs = this.recordStores[0]; + + cfg = cfg || {}; + + queryCfg = { + agent: (typeof cfg.agent !== "undefined" ? cfg.agent : this.actor) + }; + if (typeof cfg.callback !== "undefined") { + queryCfg.callback = cfg.callback; + } + + return lrs.dropAgentProfile(key, queryCfg); + } + + msg = "[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)"; + if (TinCan.environment().isBrowser) { + alert(this.LOG_SRC + ": " + msg); + } + else { + this.log(msg); + } } }; @@ -1449,7 +1629,7 @@ TinCan client library }; }()); /* - Copyright 2012 Rustici Software + Copyright 2012-2013 Rustici Software Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1713,8 +1893,8 @@ TinCan client library Method used to send a request via browser objects to the LRS @method sendRequest - @param {Object} [cfg] Configuration for request - @param {String} [cfg.url] URL portion to add to endpoint + @param {Object} cfg Configuration for request + @param {String} cfg.url URL portion to add to endpoint @param {String} [cfg.method] GET, PUT, POST, etc. @param {Object} [cfg.params] Parameters to set on the querystring @param {String} [cfg.data] String of body content @@ -1775,19 +1955,21 @@ TinCan client library } } else { - // Alert all errors except cancelled XHR requests - if (httpStatus > 0) { - requestCompleteResult = { - err: httpStatus, - xhr: xhr - }; - if (self.alertOnRequestFailure) { - alert("[warning] There was a problem communicating with the Learning Record Store. (" + httpStatus + " | " + xhr.responseText+ ")"); + requestCompleteResult = { + err: httpStatus, + xhr: xhr + }; + if (self.alertOnRequestFailure) { + if (httpStatus === 0) { + alert("[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint (" + httpStatus + ")"); } - if (cfg.callback) { - cfg.callback(httpStatus, xhr); + else { + alert("[warning] There was a problem communicating with the Learning Record Store. (" + httpStatus + " | " + xhr.responseText+ ")"); } } + if (cfg.callback) { + cfg.callback(httpStatus, xhr); + } return requestCompleteResult; } } @@ -1818,7 +2000,6 @@ TinCan client library } // consolidate headers - headers["Content-Type"] = "application/json"; headers.Authorization = this.auth; if (this.version !== "0.9") { headers["X-Experience-API-Version"] = this.version; @@ -1911,6 +2092,16 @@ TinCan client library xhr.onerror = function () { requestComplete(); }; + + // IE likes to randomly abort requests when some handlers + // aren't defined, so define them with no-ops, see: + // + // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ + // http://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified + // + xhr.ontimeout = function () {}; + xhr.onprogress = function () {}; + xhr.timeout = 0; } else { this.log("sendRequest unrecognized _requestMode: " + this._requestMode); @@ -1922,8 +2113,22 @@ TinCan client library // including jQuery (https://github.com/jquery/jquery/blob/1.10.2/src/ajax.js#L549 // https://github.com/jquery/jquery/blob/1.10.2/src/ajax/xhr.js#L97) // + // I'm wondering if the setTimeout wrapper suggested in the links + // above for XDR solves the random exception issue, but don't know + // for sure + // try { - xhr.send(data); + if (this._requestMode === XDR) { + setTimeout( + function () { + xhr.send(data); + }, + 0 + ); + } + else { + xhr.send(data); + } } catch (ex) { this.log("sendRequest caught send exception: " + ex); @@ -1978,7 +2183,10 @@ TinCan client library requestCfg = { url: "statements", - data: JSON.stringify(stmt.asVersion( this.version )) + data: JSON.stringify(stmt.asVersion( this.version )), + headers: { + "Content-Type": "application/json" + } }; if (stmt.id !== null) { requestCfg.method = "PUT"; @@ -2156,7 +2364,10 @@ TinCan client library requestCfg = { url: "statements", method: "POST", - data: JSON.stringify(versionedStatements) + data: JSON.stringify(versionedStatements), + headers: { + "Content-Type": "application/json" + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; @@ -2552,6 +2763,21 @@ TinCan client library // result.etag = TinCan.Utils.getSHA1String(xhr.responseText); } + + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + + if (result.contentType === "application/json") { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveState - failed to deserialize JSON: " + ex); + } + } } } @@ -2580,6 +2806,19 @@ TinCan client library // requestResult.state.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + requestResult.state.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.state.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (requestResult.state.contentType === "application/json") { + try { + requestResult.state.contents = JSON.parse(requestResult.state.contents); + } catch (ex) { + this.log("retrieveState - failed to deserialize JSON: " + ex); + } + } } } @@ -2597,6 +2836,7 @@ TinCan client library @param {Object} cfg.agent TinCan.Agent @param {String} [cfg.registration] Registration @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing state + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') @param {Function} [cfg.callback] Callback to execute on completion */ saveState: function (key, val, cfg) { @@ -2614,7 +2854,11 @@ TinCan client library return; } - if (typeof val === "object") { + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && cfg.contentType === "application/json") { val = JSON.stringify(val); } @@ -2641,15 +2885,16 @@ TinCan client library url: "activities/state", method: "PUT", params: requestParams, - data: val + data: val, + headers: { + "Content-Type": cfg.contentType + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; } if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { - requestCfg.headers = { - "If-Match": cfg.lastSHA1 - }; + requestCfg.headers["If-Match"] = cfg.lastSHA1; } return this.sendRequest(requestCfg); @@ -2668,8 +2913,8 @@ TinCan client library */ dropState: function (key, cfg) { this.log("dropState"); - var requestParams = {}, - requestCfg = {} + var requestParams, + requestCfg ; // TODO: it would be better to make a subclass that knows @@ -2773,6 +3018,19 @@ TinCan client library // result.etag = TinCan.Utils.getSHA1String(xhr.responseText); } + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + if (result.contentType === "application/json") { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex); + } + } } } @@ -2802,6 +3060,19 @@ TinCan client library // requestResult.profile.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + requestResult.profile.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (requestResult.profile.contentType === "application/json") { + try { + requestResult.profile.contents = JSON.parse(requestResult.profile.contents); + } catch (ex) { + this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex); + } + } } } @@ -2816,6 +3087,7 @@ TinCan client library @param {Object} cfg Configuration options @param {Object} cfg.activity TinCan.Activity @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') @param {Function} [cfg.callback] Callback to execute on completion */ saveActivityProfile: function (key, val, cfg) { @@ -2830,7 +3102,11 @@ TinCan client library return; } - if (typeof val === "object") { + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && cfg.contentType === "application/json") { val = JSON.stringify(val); } @@ -2841,20 +3117,19 @@ TinCan client library profileId: key, activityId: cfg.activity.id }, - data: val + data: val, + headers: { + "Content-Type": cfg.contentType + } }; if (typeof cfg.callback !== "undefined") { requestCfg.callback = cfg.callback; } if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { - requestCfg.headers = { - "If-Match": cfg.lastSHA1 - }; + requestCfg.headers["If-Match"] = cfg.lastSHA1; } else { - requestCfg.headers = { - "If-None-Match": "*" - }; + requestCfg.headers["If-None-Match"] = "*"; } return this.sendRequest(requestCfg); @@ -2871,8 +3146,8 @@ TinCan client library */ dropActivityProfile: function (key, cfg) { this.log("dropActivityProfile"); - var requestParams = {}, - requestCfg = {} + var requestParams, + requestCfg ; // TODO: it would be better to make a subclass that knows @@ -2884,11 +3159,9 @@ TinCan client library } requestParams = { + profileId: key, activityId: cfg.activity.id }; - if (key !== null) { - requestParams.profileId = key; - } requestCfg = { url: "activities/profile", @@ -2902,6 +3175,240 @@ TinCan client library return this.sendRequest(requestCfg); }, + /** + Retrieve an agent profile value, when used from a browser sends to the endpoint using the RESTful interface. + + @method retrieveAgentProfile + @param {String} key Key of agent profile to retrieve + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {Function} [cfg.callback] Callback to execute on completion + @return {Object} Value retrieved + */ + retrieveAgentProfile: function (key, cfg) { + this.log("retrieveAgentProfile"); + var requestCfg = {}, + requestResult, + callbackWrapper + ; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + requestCfg = { + method: "GET", + params: { + profileId: key + }, + ignore404: true + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestCfg.params.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestCfg.params.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + callbackWrapper = function (err, xhr) { + var result = xhr; + + if (err === null) { + if (xhr.status === 404) { + result = null; + } + else { + result = new TinCan.AgentProfile( + { + id: key, + agent: cfg.agent, + contents: xhr.responseText + } + ); + if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("ETag") !== null && xhr.getResponseHeader("ETag") !== "") { + result.etag = xhr.getResponseHeader("ETag"); + } else { + // + // either XHR didn't have getResponseHeader (probably cause it is an IE + // XDomainRequest object which doesn't) or not populated by LRS so create + // the hash ourselves + // + result.etag = TinCan.Utils.getSHA1String(xhr.responseText); + } + if (typeof xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + result.contentType = xhr.contentType; + } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { + result.contentType = xhr.getResponseHeader("Content-Type"); + } + if (result.contentType === "application/json") { + try { + result.contents = JSON.parse(result.contents); + } catch (ex) { + this.log("retrieveAgentProfile - failed to deserialize JSON: " + ex); + } + } + } + } + + cfg.callback(err, result); + }; + requestCfg.callback = callbackWrapper; + } + + requestResult = this.sendRequest(requestCfg); + if (! callbackWrapper) { + requestResult.profile = null; + if (requestResult.err === null && requestResult.xhr.status !== 404) { + requestResult.profile = new TinCan.AgentProfile( + { + id: key, + agent: cfg.agent, + contents: requestResult.xhr.responseText + } + ); + if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("ETag") !== null && requestResult.xhr.getResponseHeader("ETag") !== "") { + requestResult.profile.etag = requestResult.xhr.getResponseHeader("ETag"); + } else { + // + // either XHR didn't have getResponseHeader (probably cause it is an IE + // XDomainRequest object which doesn't) or not populated by LRS so create + // the hash ourselves + // + requestResult.profile.etag = TinCan.Utils.getSHA1String(requestResult.xhr.responseText); + } + if (typeof requestResult.xhr.contentType !== "undefined") { + // most likely an XDomainRequest which has .contentType + requestResult.profile.contentType = requestResult.xhr.contentType; + } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { + requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); + } + if (requestResult.profile.contentType === "application/json") { + try { + requestResult.profile.contents = JSON.parse(requestResult.profile.contents); + } catch (ex) { + this.log("retrieveAgentProfile - failed to deserialize JSON: " + ex); + } + } + } + } + + return requestResult; + }, + + /** + Save an agent profile value, when used from a browser sends to the endpoint using the RESTful interface. + + @method saveAgentProfile + @param {String} key Key of agent profile to retrieve + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {String} [cfg.lastSHA1] SHA1 of the previously seen existing profile + @param {String} [cfg.contentType] Content-Type to specify in headers (defaults to 'application/octet-stream') + @param {Function} [cfg.callback] Callback to execute on completion + */ + saveAgentProfile: function (key, val, cfg) { + this.log("saveAgentProfile"); + var requestCfg; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + if (typeof cfg.contentType === "undefined") { + cfg.contentType = "application/octet-stream"; + } + + if (typeof val === "object" && cfg.contentType === "application/json") { + val = JSON.stringify(val); + } + + requestCfg = { + method: "PUT", + params: { + profileId: key + }, + data: val, + headers: { + "Content-Type": cfg.contentType + } + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestCfg.params.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestCfg.params.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + requestCfg.callback = cfg.callback; + } + if (typeof cfg.lastSHA1 !== "undefined" && cfg.lastSHA1 !== null) { + requestCfg.headers["If-Match"] = cfg.lastSHA1; + } + else { + requestCfg.headers["If-None-Match"] = "*"; + } + + return this.sendRequest(requestCfg); + }, + + /** + Drop an agent profile value or all of the agent profile, when used from a browser sends to the endpoint using the RESTful interface. + + @method dropAgentProfile + @param {String|null} key Key of agent profile to delete, or null for all + @param {Object} cfg Configuration options + @param {Object} cfg.agent TinCan.Agent + @param {Function} [cfg.callback] Callback to execute on completion + */ + dropAgentProfile: function (key, cfg) { + this.log("dropAgentProfile"); + var requestParams, + requestCfg + ; + + // TODO: it would be better to make a subclass that knows + // its own environment and just implements the protocol + // that it needs to + if (! TinCan.environment().isBrowser) { + this.log("error: environment not implemented"); + return; + } + + requestParams = { + profileId: key + }; + requestCfg = { + method: "DELETE", + params: requestParams + }; + if (this.version === "0.9") { + requestCfg.url = "actors/profile"; + requestParams.actor = JSON.stringify(cfg.agent.asVersion(this.version)); + } + else { + requestCfg.url = "agents/profile"; + requestParams.agent = JSON.stringify(cfg.agent.asVersion(this.version)); + } + if (typeof cfg.callback !== "undefined") { + requestCfg.callback = cfg.callback; + } + + return this.sendRequest(requestCfg); + }, + /** Non-environment safe method used to create a delay to give impression of synchronous response @@ -5954,6 +6461,12 @@ TinCan client library */ this.etag = null; + /** + @property contentType + @type String + */ + this.contentType = null; + this.init(cfg); }; State.prototype = { @@ -5976,7 +6489,9 @@ TinCan client library var i, directProps = [ "id", - "contents" + "contents", + "etag", + "contentType" ], val ; @@ -6070,6 +6585,12 @@ TinCan client library */ this.etag = null; + /** + @property contentType + @type String + */ + this.contentType = null; + this.init(cfg); }; ActivityProfile.prototype = { @@ -6093,7 +6614,8 @@ TinCan client library directProps = [ "id", "contents", - "etag" + "etag", + "contentType" ], val ; @@ -6131,6 +6653,139 @@ TinCan client library return new ActivityProfile(_state); }; }()); +/* + Copyright 2013 Rustici Software + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/** +TinCan client library + +@module TinCan +@submodule TinCan.AgentProfile +**/ +(function () { + "use strict"; + + /** + @class TinCan.AgentProfile + @constructor + */ + var AgentProfile = TinCan.AgentProfile = function (cfg) { + this.log("constructor"); + + /** + @property id + @type String + */ + this.id = null; + + /** + @property agent + @type TinCan.Agent + */ + this.agent = null; + + /** + @property updated + @type String + */ + this.updated = null; + + /** + @property contents + @type String + */ + this.contents = null; + + /** + SHA1 of contents as provided by the server during last fetch, + this should be passed through to saveAgentProfile + + @property etag + @type String + */ + this.etag = null; + + /** + @property contentType + @type String + */ + this.contentType = null; + + this.init(cfg); + }; + AgentProfile.prototype = { + /** + @property LOG_SRC + */ + LOG_SRC: 'AgentProfile', + + /** + @method log + */ + log: TinCan.prototype.log, + + /** + @method init + @param {Object} [options] Configuration used to initialize + */ + init: function (cfg) { + this.log("init"); + var i, + directProps = [ + "id", + "contents", + "etag", + "contentType" + ], + val + ; + + cfg = cfg || {}; + + if (cfg.hasOwnProperty("agent")) { + if (cfg.agent instanceof TinCan.Agent) { + this.agent = cfg.agent; + } + else { + this.agent = new TinCan.Agent (cfg.agent); + } + } + + for (i = 0; i < directProps.length; i += 1) { + if (cfg.hasOwnProperty(directProps[i]) && cfg[directProps[i]] !== null) { + this[directProps[i]] = cfg[directProps[i]]; + } + } + + this.updated = false; + } + }; + + /** + @method fromJSON + @return {Object} AgentProfile + @static + */ + AgentProfile.fromJSON = function (stateJSON) { + AgentProfile.prototype.log("fromJSON"); + var _state = JSON.parse(stateJSON); + + return new AgentProfile(_state); + }; +}()); /* CryptoJS v3.0.2 code.google.com/p/crypto-js diff --git a/yuidoc.json b/yuidoc.json index 677a173..0597a84 100644 --- a/yuidoc.json +++ b/yuidoc.json @@ -1,8 +1,8 @@ { - "version": "0.7.2", + "version": "0.8.0", "name": "TinCanJS", - "description": "Client library for working with TinCan API in JavaScript", - "url": "https://github.com/RusticiSoftware/TinCanJS", + "description": "Library for working with Tin Can API in JavaScript", + "url": "http://rusticisoftware.github.com/TinCanJS/", "options": { "outdir": "doc/api" }, From d4df9e5bf1283132617aabc8502c8424846b29de Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Thu, 8 Aug 2013 14:55:18 -0500 Subject: [PATCH 09/14] Correct README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 48bf571..02d809a 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,4 @@ Alternatively you can just link to the individual files themselves like so: + From d0cedfbeb723c0588c00ec14563748c8a5455cf7 Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Fri, 9 Aug 2013 09:47:19 -0500 Subject: [PATCH 10/14] Correct detection of application/json content type * Adds utility method for matching content type headers --- src/LRS.js | 18 +++++++++--------- src/Utils.js | 9 +++++++++ test/js/Utils.js | 23 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/LRS.js b/src/LRS.js index 27cfada..3b71ed0 100644 --- a/src/LRS.js +++ b/src/LRS.js @@ -1141,7 +1141,7 @@ TinCan client library result.contentType = xhr.getResponseHeader("Content-Type"); } - if (result.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(result.contentType)) { try { result.contents = JSON.parse(result.contents); } catch (ex) { @@ -1182,7 +1182,7 @@ TinCan client library } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.state.contentType = requestResult.xhr.getResponseHeader("Content-Type"); } - if (requestResult.state.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(requestResult.state.contentType)) { try { requestResult.state.contents = JSON.parse(requestResult.state.contents); } catch (ex) { @@ -1228,7 +1228,7 @@ TinCan client library cfg.contentType = "application/octet-stream"; } - if (typeof val === "object" && cfg.contentType === "application/json") { + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { val = JSON.stringify(val); } @@ -1394,7 +1394,7 @@ TinCan client library } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { result.contentType = xhr.getResponseHeader("Content-Type"); } - if (result.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(result.contentType)) { try { result.contents = JSON.parse(result.contents); } catch (ex) { @@ -1436,7 +1436,7 @@ TinCan client library } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); } - if (requestResult.profile.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) { try { requestResult.profile.contents = JSON.parse(requestResult.profile.contents); } catch (ex) { @@ -1476,7 +1476,7 @@ TinCan client library cfg.contentType = "application/octet-stream"; } - if (typeof val === "object" && cfg.contentType === "application/json") { + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { val = JSON.stringify(val); } @@ -1617,7 +1617,7 @@ TinCan client library } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { result.contentType = xhr.getResponseHeader("Content-Type"); } - if (result.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(result.contentType)) { try { result.contents = JSON.parse(result.contents); } catch (ex) { @@ -1659,7 +1659,7 @@ TinCan client library } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); } - if (requestResult.profile.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) { try { requestResult.profile.contents = JSON.parse(requestResult.profile.contents); } catch (ex) { @@ -1699,7 +1699,7 @@ TinCan client library cfg.contentType = "application/octet-stream"; } - if (typeof val === "object" && cfg.contentType === "application/json") { + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { val = JSON.stringify(val); } diff --git a/src/Utils.js b/src/Utils.js index 29ea883..c867ddd 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -182,6 +182,15 @@ TinCan client library getServerRoot: function (absoluteUrl) { var urlParts = absoluteUrl.split("/"); return urlParts[0] + "//" + urlParts[2]; + }, + + /** + @method isApplicationJSON + @param {String} Content-Type header value + @return {Boolean} whether "application/json" was matched + */ + isApplicationJSON: function (header) { + return (String(header).split(";"))[0].toLowerCase().indexOf("application/json") === 0; } }; }()); diff --git a/test/js/Utils.js b/test/js/Utils.js index 9927c4b..943e8a6 100644 --- a/test/js/Utils.js +++ b/test/js/Utils.js @@ -111,4 +111,27 @@ ok(result === "http://tincanapi.com:8080", "return value: with port"); } ); + test( + "isApplicationJSON", + function () { + var okStrings = [ + "application/json", + "application/json; charset=UTF-8", + "application/json ", + "Application/JSON", + "Application/JSON " + ], + notOkStrings = [ + "application/octet-stream", + "text/plain" + ], + i; + for (i = 0; i < okStrings.length; i += 1) { + ok(TinCan.Utils.isApplicationJSON(okStrings[i]), "'" + okStrings[i] + "' matched"); + } + for (i = 0; i < notOkStrings.length; i += 1) { + ok(! TinCan.Utils.isApplicationJSON(notOkStrings[i]), "'" + notOkStrings[i] + "' not matched"); + } + } + ); }()); From 3afeb63c349294ac20d724ab7f5a80b0ec29eef9 Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Fri, 9 Aug 2013 10:27:54 -0500 Subject: [PATCH 11/14] Add method for getting content type from Content-Type header * Needed for unit tests primarily --- src/Utils.js | 13 ++++++++++++- test/js/TinCan.js | 12 ++++++------ test/js/Utils.js | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/Utils.js b/src/Utils.js index c867ddd..8cba5aa 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -184,13 +184,24 @@ TinCan client library return urlParts[0] + "//" + urlParts[2]; }, + /** + @method getContentTypeFromHeader + @static + @param {String} Content-Type header value + @return {String} Primary value from Content-Type + */ + getContentTypeFromHeader: function (header) { + return (String(header).split(";"))[0]; + }, + /** @method isApplicationJSON + @static @param {String} Content-Type header value @return {Boolean} whether "application/json" was matched */ isApplicationJSON: function (header) { - return (String(header).split(";"))[0].toLowerCase().indexOf("application/json") === 0; + return TinCan.Utils.getContentTypeFromHeader(header).toLowerCase().indexOf("application/json") === 0; } }; }()); diff --git a/test/js/TinCan.js b/test/js/TinCan.js index cbafa39..0179959 100644 --- a/test/js/TinCan.js +++ b/test/js/TinCan.js @@ -553,7 +553,7 @@ ok(getResult.hasOwnProperty("state"), "getResult has property: state (" + v + ")"); ok(getResult.state instanceof TinCan.State, "getResult state property is TinCan.State (" + v + ")"); deepEqual(getResult.state.contents, val, "getResult state property contents (" + v + ")"); - deepEqual(getResult.state.contentType, "application/octet-stream", "getResult state property contentType (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.state.contentType), "application/octet-stream", "getResult state property contentType (" + v + ")"); // // reset the state to make sure we test the concurrency handling @@ -604,7 +604,7 @@ ok(getResult.hasOwnProperty("state"), "getResult has property: state (" + v + ")"); ok(getResult.state instanceof TinCan.State, "getResult state property is TinCan.State (" + v + ")"); deepEqual(getResult.state.contents, val, "getResult state property contents (" + v + ")"); - deepEqual(getResult.state.contentType, "application/json", "getResult state property contentType (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.state.contentType), "application/json", "getResult state property contentType (" + v + ")"); // // reset the state to make sure we test the concurrency handling @@ -642,7 +642,7 @@ ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); ok(getResult.profile instanceof TinCan.ActivityProfile, "getResult profile property is TinCan.ActivityProfile (" + v + ")"); deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); - deepEqual(getResult.profile.contentType, "application/octet-stream", "getResult profile property contentType (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.profile.contentType), "application/octet-stream", "getResult profile property contentType (" + v + ")"); // this should "fail" session[v].recordStores[0].alertOnRequestFailure = false; @@ -692,7 +692,7 @@ ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); ok(getResult.profile instanceof TinCan.ActivityProfile, "getResult profile property is TinCan.ActivityProfile (" + v + ")"); deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); - deepEqual(getResult.profile.contentType, "application/json", "getResult profile property contentType (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.profile.contentType), "application/json", "getResult profile property contentType (" + v + ")"); // this should "fail" session[v].recordStores[0].alertOnRequestFailure = false; @@ -736,7 +736,7 @@ ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); ok(getResult.profile instanceof TinCan.AgentProfile, "getResult profile property is TinCan.AgentProfile (" + v + ")"); deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); - deepEqual(getResult.profile.contentType, "application/octet-stream", "getResult profile property contentType (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.profile.contentType), "application/octet-stream", "getResult profile property contentType (" + v + ")"); // this should "fail" session[v].recordStores[0].alertOnRequestFailure = false; @@ -787,7 +787,7 @@ ok(getResult.hasOwnProperty("profile"), "getResult has property: profile (" + v + ")"); ok(getResult.profile instanceof TinCan.AgentProfile, "getResult profile property is TinCan.AgentProfile (" + v + ")"); deepEqual(getResult.profile.contents, val, "getResult profile property contents (" + v + ")"); - deepEqual(getResult.profile.contentType, "application/json", "getResult profile property contentType (" + v + ")"); + deepEqual(TinCan.Utils.getContentTypeFromHeader(getResult.profile.contentType), "application/json", "getResult profile property contentType (" + v + ")"); // this should "fail" session[v].recordStores[0].alertOnRequestFailure = false; diff --git a/test/js/Utils.js b/test/js/Utils.js index 943e8a6..f4814b0 100644 --- a/test/js/Utils.js +++ b/test/js/Utils.js @@ -111,6 +111,21 @@ ok(result === "http://tincanapi.com:8080", "return value: with port"); } ); + test( + "getContentTypeFromHeader", + function () { + var appJSON = "application/json", + strings = { + "application/json": appJSON, + "application/json; charset=UTF-8": appJSON, + "text/plain;charset=UTF-8": "text/plain" + }, + prop; + for (prop in strings) { + ok(TinCan.Utils.getContentTypeFromHeader(prop) === strings[prop], "'" + prop + "' matches '" + strings[prop]); + } + } + ); test( "isApplicationJSON", function () { From 5f5f839c3a86d528fa15c20e4aa76a43de496de4 Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Fri, 9 Aug 2013 10:53:41 -0500 Subject: [PATCH 12/14] Build 0.8.1 --- build/tincan-min.js | 2 +- build/tincan.js | 38 +++++++++++++++++++++++++++++--------- yuidoc.json | 2 +- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/build/tincan-min.js b/build/tincan-min.js index 53ff2fc..18375d6 100644 --- a/build/tincan-min.js +++ b/build/tincan-min.js @@ -14,4 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -;var TinCan;(function(){"use strict";var _environment=null,_reservedQSParams={statementId:!0,voidedStatementId:!0,verb:!0,object:!0,registration:!0,context:!0,actor:!0,since:!0,until:!0,limit:!0,authoritative:!0,sparse:!0,instructor:!0,ascending:!0,continueToken:!0,agent:!0,activityId:!0,stateId:!0,profileId:!0,activity_platform:!0,grouping:!0,"Accept-Language":!0};TinCan=function(a){this.log("constructor"),this.environment=null,this.recordStores=[],this.actor=null,this.activity=null,this.registration=null,this.context=null,this.init(a)},TinCan.prototype={LOG_SRC:"TinCan",log:function(a,b){TinCan.DEBUG&&typeof console!="undefined"&&console.log&&(b=b||this.LOG_SRC||"TinCan",console.log("TinCan."+b+": "+a))},init:function(a){this.log("init");var b;a=a||{},a.hasOwnProperty("url")&&a.url!==""&&this._initFromQueryString(a.url);if(a.hasOwnProperty("recordStores")&&a.recordStores!==undefined)for(b=0;b0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatement - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatement - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],c.retrieveStatement(a,{callback:b});d="[warning] getStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},voidStatement:function(a,b,c){this.log("voidStatement");var d=this,e,f,g,h=this.recordStores.length,i,j,k=[],l,m=[];a instanceof TinCan.Statement&&(a=a.id),typeof c.actor!="undefined"?f=c.actor:this.actor!==null&&(f=this.actor),g=new TinCan.Statement({actor:f,verb:{id:"http://adlnet.gov/expapi/verbs/voided"},target:{objectType:"StatementRef",id:a}});if(h>0){typeof b=="function"&&(l=function(a,c){var e;d.log("voidStatement - callbackWrapper: "+h),h>1?(h-=1,m.push({err:a,xhr:c})):h===1?(m.push({err:a,xhr:c}),e=[m,g],b.apply(this,e)):d.log("voidStatement - unexpected record store count: "+h)});for(i=0;i0)return c=this.recordStores[0],c.retrieveVoidedStatement(a,{callback:b});d="[warning] getVoidedStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},sendStatements:function(a,b){this.log("sendStatements");var c=this,d,e=[],f=this.recordStores.length,g,h,i=[],j,k=[];if(a.length===0)typeof b=="function"&&b.apply(this,[null,e]);else{for(g=0;g0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatements - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatements - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],a=a||{},d=a.params||{},a.sendActor&&this.actor!==null&&(c.version==="0.9"||c.version==="0.95"?d.actor=this.actor:d.agent=this.actor),a.sendActivity&&this.activity!==null&&(c.version==="0.9"||c.version==="0.95"?d.target=this.activity:d.activity=this.activity),typeof d.registration=="undefined"&&this.registration!==null&&(d.registration=this.registration),b={params:d},typeof a.callback!="undefined"&&(b.callback=a.callback),c.queryStatements(b);e="[warning] getStatements: No LRSs added yet (statements not read)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getState:function(a,b){this.log("getState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveState(a,c);e="[warning] getState: No LRSs added yet (state not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setState:function(a,b,c){this.log("setState");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor,activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.registration!="undefined"?d.registration=c.registration:this.registration!==null&&(d.registration=this.registration),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),typeof c.callback!="undefined"&&(d.callback=c.callback),e.saveState(a,b,d);f="[warning] setState: No LRSs added yet (state not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteState:function(a,b){this.log("deleteState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropState(a,c);e="[warning] deleteState: No LRSs added yet (state not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getActivityProfile:function(a,b){this.log("getActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveActivityProfile(a,c);e="[warning] getActivityProfile: No LRSs added yet (activity profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setActivityProfile:function(a,b,c){this.log("setActivityProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveActivityProfile(a,b,d);f="[warning] setActivityProfile: No LRSs added yet (activity profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteActivityProfile:function(a,b){this.log("deleteActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropActivityProfile(a,c);e="[warning] deleteActivityProfile: No LRSs added yet (activity profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getAgentProfile:function(a,b){this.log("getAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveAgentProfile(a,c);e="[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setAgentProfile:function(a,b,c){this.log("setAgentProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveAgentProfile(a,b,d);f="[warning] setAgentProfile: No LRSs added yet (agent profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteAgentProfile:function(a,b){this.log("deleteAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropAgentProfile(a,c);e="[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)}},TinCan.DEBUG=!1,TinCan.enableDebug=function(){TinCan.DEBUG=!0},TinCan.disableDebug=function(){TinCan.DEBUG=!1},TinCan.versions=function(){return["1.0.0","0.95","0.9"]},TinCan.environment=function(){return _environment===null&&(_environment={},typeof window!="undefined"?(_environment.isBrowser=!0,_environment.hasCORS=!1,_environment.useXDR=!1,typeof XMLHttpRequest!="undefined"&&typeof (new XMLHttpRequest).withCredentials!="undefined"?_environment.hasCORS=!0:typeof XDomainRequest!="undefined"&&(_environment.hasCORS=!0,_environment.useXDR=!0)):_environment.isBrowser=!1),_environment},TinCan.environment().isBrowser&&(window.JSON||(window.JSON={parse:function(sJSON){return eval("("+sJSON+")")},stringify:function(a){var b="",c,d;if(a instanceof Object){if(a.constructor===Array){for(c=0;c1)d="0"+d,c/=10;return d}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())+"."+b(a.getUTCMilliseconds(),3)+"Z"},getSHA1String:function(a){return CryptoJS.SHA1(a).toString(CryptoJS.enc.Hex)},getBase64String:function(a){return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(a))},getLangDictionaryValue:function(a,b){var c=this[a],d;if(typeof b!="undefined"&&typeof c[b]!="undefined")return c[b];if(typeof c.und!="undefined")return c.und;if(typeof c["en-US"]!="undefined")return c["en-US"];for(d in c)if(c.hasOwnProperty(d))return c[d];return""},parseURL:function(a){var b=String(a).split("?"),c,d,e,f={};if(b.length===2){c=b[1].split("&");for(e=0;e=200&&b<400||a))return j={err:b,xhr:d},n.alertOnRequestFailure&&(b===0?alert("[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint ("+b+")"):alert("[warning] There was a problem communicating with the Learning Record Store. ("+b+" | "+d.responseText+")")),c.callback&&c.callback(b,d),j;if(!c.callback)return j={err:null,xhr:d},j;c.callback(null,d)}this.log("sendRequest");var d,e=!1,f=window.location,g=this.endpoint+c.url,h={},i,j,k,l,m=[],n=this;c.url.indexOf("http")===0&&(g=c.url);if(this.extended!==null){c.params=c.params||{};for(l in this.extended)this.extended.hasOwnProperty(l)&&(c.params.hasOwnProperty(l)||this.extended[l]!==null&&(c.params[l]=this.extended[l]))}h.Authorization=this.auth,this.version!=="0.9"&&(h["X-Experience-API-Version"]=this.version);for(l in c.headers)c.headers.hasOwnProperty(l)&&(h[l]=c.headers[l]);if(this._requestMode===b){this.log("sendRequest using XMLHttpRequest");for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));m.length>0&&(g+="?"+m.join("&")),this.log("sendRequest using XMLHttpRequest - async: "+(typeof c.callback!="undefined")),typeof XMLHttpRequest!="undefined"?d=new XMLHttpRequest:d=new ActiveXObject("Microsoft.XMLHTTP"),d.open(c.method,g,typeof c.callback!="undefined");for(l in h)h.hasOwnProperty(l)&&d.setRequestHeader(l,h[l]);typeof c.data!="undefined"&&(c.data+=""),i=c.data,d.onreadystatechange=function(){n.log("xhr.onreadystatechange - xhr.readyState: "+d.readyState),d.readyState===4&&o()}}else if(this._requestMode===a){this.log("sendRequest using XDomainRequest"),g+="?method="+c.method;for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));for(l in h)h.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(h[l]));c.data!==null&&m.push("content="+encodeURIComponent(c.data)),i=m.join("&"),d=new XDomainRequest,d.open("POST",g),d.onload=function(){o()},d.onerror=function(){o()},d.ontimeout=function(){},d.onprogress=function(){},d.timeout=0}else this.log("sendRequest unrecognized _requestMode: "+this._requestMode);try{this._requestMode===a?setTimeout(function(){d.send(i)},0):d.send(i)}catch(p){this.log("sendRequest caught send exception: "+p)}if(!c.callback){if(this._requestMode===a){k=1e3+Date.now(),this.log("sendRequest - until: "+k+", finished: "+e);while(Date.now()0&&(a.name=a.firstName[0],a.firstName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.lastName!="undefined"&&a.lastName.length>0&&(a.name+=a.lastName[0],a.lastName.length>1&&(this.degraded=!0));else if(typeof a.familyName!="undefined"||typeof a.givenName!="undefined")a.name="",typeof a.givenName!="undefined"&&a.givenName.length>0&&(a.name=a.givenName[0],a.givenName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.familyName!="undefined"&&a.familyName.length>0&&(a.name+=a.familyName[0],a.familyName.length>1&&(this.degraded=!0));typeof a.name=="object"&&a.name!==null&&(a.name.length>1&&(this.degraded=!0),a.name=a.name[0]),typeof a.mbox=="object"&&a.mbox!==null&&(a.mbox.length>1&&(this.degraded=!0),a.mbox=a.mbox[0]),typeof a.mbox_sha1sum=="object"&&a.mbox_sha1sum!==null&&(a.mbox_sha1sum.length>1&&(this.degraded=!0),a.mbox_sha1sum=a.mbox_sha1sum[0]),typeof a.openid=="object"&&a.openid!==null&&(a.openid.length>1&&(this.degraded=!0),a.openid=a.openid[0]),typeof a.account=="object"&&a.account!==null&&typeof a.account.homePage=="undefined"&&typeof a.account.name=="undefined"&&(a.account.length===0?delete a.account:(a.account.length>1&&(this.degraded=!0),a.account=a.account[0])),a.hasOwnProperty("account")&&(a.account instanceof TinCan.AgentAccount?this.account=a.account:this.account=new TinCan.AgentAccount(a.account));for(b=0;b0){b.member=[];for(c=0;c0)for(c=0;c0)if(a==="0.9"||a==="0.95")this[c[d]].length>1&&this.log("[WARNING] version does not support multiple values in: "+c[d]),b[c[d]]=this[c[d]][0].asVersion(a);else{b[c[d]]=[];for(e=0;e>>2]|=(c[e>>>2]>>>24-8*(e%4)&255)<<24-8*((d+e)%4);else if(65535>>2]=c[e>>>2];else b.push.apply(b,c);return this.sigBytes+=a,this},clamp:function(){var b=this.words,c=this.sigBytes;b[c>>>2]&=4294967295<<32-8*(c%4),b.length=a.ceil(c/4)},clone:function(){var a=e.clone.call(this);return a.words=this.words.slice(0),a},random:function(b){for(var c=[],d=0;d>>2]>>>24-8*(d%4)&255;c.push((e>>>4).toString(16)),c.push((e&15).toString(16))}return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>3]|=parseInt(a.substr(d,2),16)<<24-4*(d%8);return f.create(c,b/2)}},i=g.Latin1={stringify:function(a){for(var b=a.words,a=a.sigBytes,c=[],d=0;d>>2]>>>24-8*(d%4)&255));return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>2]|=(a.charCodeAt(d)&255)<<24-8*(d%4);return f.create(c,b)}},j=g.Utf8={stringify:function(a){try{return decodeURIComponent(escape(i.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data")}},parse:function(a){return i.parse(unescape(encodeURIComponent(a)))}},k=d.BufferedBlockAlgorithm=e.extend({reset:function(){this._data=f.create(),this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a)),this._data.concat(a),this._nDataBytes+=a.sigBytes},_process:function(b){var c=this._data,d=c.words,e=c.sigBytes,g=this.blockSize,h=e/(4*g),h=b?a.ceil(h):a.max((h|0)-this._minBufferSize,0),b=h*g,e=a.min(4*b,e);if(b){for(var i=0;ik;k++){if(16>k)d[k]=a[b+k]|0;else{var l=d[k-3]^d[k-8]^d[k-14]^d[k-16];d[k]=l<<1|l>>>31}l=(e<<5|e>>>27)+j+d[k],l=20>k?l+((f&g|~f&i)+1518500249):40>k?l+((f^g^i)+1859775393):60>k?l+((f&g|f&i|g&i)-1894007588):l+((f^g^i)-899497514),j=i,i=g,g=f<<30|f>>>2,f=e,e=l}c[0]=c[0]+e|0,c[1]=c[1]+f|0,c[2]=c[2]+g|0,c[3]=c[3]+i|0,c[4]=c[4]+j|0},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,d=8*a.sigBytes;b[d>>>5]|=128<<24-d%32,b[(d+64>>>9<<4)+15]=c,a.sigBytes=4*b.length,this._process()}});a.SHA1=b._createHelper(e),a.HmacSHA1=b._createHmacHelper(e)})(),function(){var a=CryptoJS,b=a.lib,c=b.WordArray,d=a.enc,e=d.Base64={stringify:function(a){var b=a.words,c=a.sigBytes,d=this._map;a.clamp();var e=[];for(var f=0;f>>2]>>>24-f%4*8&255,h=b[f+1>>>2]>>>24-(f+1)%4*8&255,i=b[f+2>>>2]>>>24-(f+2)%4*8&255,j=g<<16|h<<8|i;for(var k=0;k<4&&f+k*.75>>6*(3-k)&63))}var l=d.charAt(64);if(l)while(e.length%4)e.push(l);return e.join("")},parse:function(a){a=a.replace(/\s/g,"");var b=a.length,d=this._map,e=d.charAt(64);if(e){var f=a.indexOf(e);f!=-1&&(b=f)}var g=[],h=0;for(var i=0;i>>6-i%4*2;g[h>>>2]|=(j|k)<<24-h%4*8,h++}return c.create(g,h)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(); +;var TinCan;(function(){"use strict";var _environment=null,_reservedQSParams={statementId:!0,voidedStatementId:!0,verb:!0,object:!0,registration:!0,context:!0,actor:!0,since:!0,until:!0,limit:!0,authoritative:!0,sparse:!0,instructor:!0,ascending:!0,continueToken:!0,agent:!0,activityId:!0,stateId:!0,profileId:!0,activity_platform:!0,grouping:!0,"Accept-Language":!0};TinCan=function(a){this.log("constructor"),this.environment=null,this.recordStores=[],this.actor=null,this.activity=null,this.registration=null,this.context=null,this.init(a)},TinCan.prototype={LOG_SRC:"TinCan",log:function(a,b){TinCan.DEBUG&&typeof console!="undefined"&&console.log&&(b=b||this.LOG_SRC||"TinCan",console.log("TinCan."+b+": "+a))},init:function(a){this.log("init");var b;a=a||{},a.hasOwnProperty("url")&&a.url!==""&&this._initFromQueryString(a.url);if(a.hasOwnProperty("recordStores")&&a.recordStores!==undefined)for(b=0;b0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatement - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatement - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],c.retrieveStatement(a,{callback:b});d="[warning] getStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},voidStatement:function(a,b,c){this.log("voidStatement");var d=this,e,f,g,h=this.recordStores.length,i,j,k=[],l,m=[];a instanceof TinCan.Statement&&(a=a.id),typeof c.actor!="undefined"?f=c.actor:this.actor!==null&&(f=this.actor),g=new TinCan.Statement({actor:f,verb:{id:"http://adlnet.gov/expapi/verbs/voided"},target:{objectType:"StatementRef",id:a}});if(h>0){typeof b=="function"&&(l=function(a,c){var e;d.log("voidStatement - callbackWrapper: "+h),h>1?(h-=1,m.push({err:a,xhr:c})):h===1?(m.push({err:a,xhr:c}),e=[m,g],b.apply(this,e)):d.log("voidStatement - unexpected record store count: "+h)});for(i=0;i0)return c=this.recordStores[0],c.retrieveVoidedStatement(a,{callback:b});d="[warning] getVoidedStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},sendStatements:function(a,b){this.log("sendStatements");var c=this,d,e=[],f=this.recordStores.length,g,h,i=[],j,k=[];if(a.length===0)typeof b=="function"&&b.apply(this,[null,e]);else{for(g=0;g0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatements - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatements - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],a=a||{},d=a.params||{},a.sendActor&&this.actor!==null&&(c.version==="0.9"||c.version==="0.95"?d.actor=this.actor:d.agent=this.actor),a.sendActivity&&this.activity!==null&&(c.version==="0.9"||c.version==="0.95"?d.target=this.activity:d.activity=this.activity),typeof d.registration=="undefined"&&this.registration!==null&&(d.registration=this.registration),b={params:d},typeof a.callback!="undefined"&&(b.callback=a.callback),c.queryStatements(b);e="[warning] getStatements: No LRSs added yet (statements not read)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getState:function(a,b){this.log("getState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveState(a,c);e="[warning] getState: No LRSs added yet (state not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setState:function(a,b,c){this.log("setState");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor,activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.registration!="undefined"?d.registration=c.registration:this.registration!==null&&(d.registration=this.registration),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),typeof c.callback!="undefined"&&(d.callback=c.callback),e.saveState(a,b,d);f="[warning] setState: No LRSs added yet (state not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteState:function(a,b){this.log("deleteState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropState(a,c);e="[warning] deleteState: No LRSs added yet (state not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getActivityProfile:function(a,b){this.log("getActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveActivityProfile(a,c);e="[warning] getActivityProfile: No LRSs added yet (activity profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setActivityProfile:function(a,b,c){this.log("setActivityProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveActivityProfile(a,b,d);f="[warning] setActivityProfile: No LRSs added yet (activity profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteActivityProfile:function(a,b){this.log("deleteActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropActivityProfile(a,c);e="[warning] deleteActivityProfile: No LRSs added yet (activity profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getAgentProfile:function(a,b){this.log("getAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveAgentProfile(a,c);e="[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setAgentProfile:function(a,b,c){this.log("setAgentProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveAgentProfile(a,b,d);f="[warning] setAgentProfile: No LRSs added yet (agent profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteAgentProfile:function(a,b){this.log("deleteAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropAgentProfile(a,c);e="[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)}},TinCan.DEBUG=!1,TinCan.enableDebug=function(){TinCan.DEBUG=!0},TinCan.disableDebug=function(){TinCan.DEBUG=!1},TinCan.versions=function(){return["1.0.0","0.95","0.9"]},TinCan.environment=function(){return _environment===null&&(_environment={},typeof window!="undefined"?(_environment.isBrowser=!0,_environment.hasCORS=!1,_environment.useXDR=!1,typeof XMLHttpRequest!="undefined"&&typeof (new XMLHttpRequest).withCredentials!="undefined"?_environment.hasCORS=!0:typeof XDomainRequest!="undefined"&&(_environment.hasCORS=!0,_environment.useXDR=!0)):_environment.isBrowser=!1),_environment},TinCan.environment().isBrowser&&(window.JSON||(window.JSON={parse:function(sJSON){return eval("("+sJSON+")")},stringify:function(a){var b="",c,d;if(a instanceof Object){if(a.constructor===Array){for(c=0;c1)d="0"+d,c/=10;return d}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())+"."+b(a.getUTCMilliseconds(),3)+"Z"},getSHA1String:function(a){return CryptoJS.SHA1(a).toString(CryptoJS.enc.Hex)},getBase64String:function(a){return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(a))},getLangDictionaryValue:function(a,b){var c=this[a],d;if(typeof b!="undefined"&&typeof c[b]!="undefined")return c[b];if(typeof c.und!="undefined")return c.und;if(typeof c["en-US"]!="undefined")return c["en-US"];for(d in c)if(c.hasOwnProperty(d))return c[d];return""},parseURL:function(a){var b=String(a).split("?"),c,d,e,f={};if(b.length===2){c=b[1].split("&");for(e=0;e=200&&b<400||a))return j={err:b,xhr:d},n.alertOnRequestFailure&&(b===0?alert("[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint ("+b+")"):alert("[warning] There was a problem communicating with the Learning Record Store. ("+b+" | "+d.responseText+")")),c.callback&&c.callback(b,d),j;if(!c.callback)return j={err:null,xhr:d},j;c.callback(null,d)}this.log("sendRequest");var d,e=!1,f=window.location,g=this.endpoint+c.url,h={},i,j,k,l,m=[],n=this;c.url.indexOf("http")===0&&(g=c.url);if(this.extended!==null){c.params=c.params||{};for(l in this.extended)this.extended.hasOwnProperty(l)&&(c.params.hasOwnProperty(l)||this.extended[l]!==null&&(c.params[l]=this.extended[l]))}h.Authorization=this.auth,this.version!=="0.9"&&(h["X-Experience-API-Version"]=this.version);for(l in c.headers)c.headers.hasOwnProperty(l)&&(h[l]=c.headers[l]);if(this._requestMode===b){this.log("sendRequest using XMLHttpRequest");for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));m.length>0&&(g+="?"+m.join("&")),this.log("sendRequest using XMLHttpRequest - async: "+(typeof c.callback!="undefined")),typeof XMLHttpRequest!="undefined"?d=new XMLHttpRequest:d=new ActiveXObject("Microsoft.XMLHTTP"),d.open(c.method,g,typeof c.callback!="undefined");for(l in h)h.hasOwnProperty(l)&&d.setRequestHeader(l,h[l]);typeof c.data!="undefined"&&(c.data+=""),i=c.data,d.onreadystatechange=function(){n.log("xhr.onreadystatechange - xhr.readyState: "+d.readyState),d.readyState===4&&o()}}else if(this._requestMode===a){this.log("sendRequest using XDomainRequest"),g+="?method="+c.method;for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));for(l in h)h.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(h[l]));c.data!==null&&m.push("content="+encodeURIComponent(c.data)),i=m.join("&"),d=new XDomainRequest,d.open("POST",g),d.onload=function(){o()},d.onerror=function(){o()},d.ontimeout=function(){},d.onprogress=function(){},d.timeout=0}else this.log("sendRequest unrecognized _requestMode: "+this._requestMode);try{this._requestMode===a?setTimeout(function(){d.send(i)},0):d.send(i)}catch(p){this.log("sendRequest caught send exception: "+p)}if(!c.callback){if(this._requestMode===a){k=1e3+Date.now(),this.log("sendRequest - until: "+k+", finished: "+e);while(Date.now()0&&(a.name=a.firstName[0],a.firstName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.lastName!="undefined"&&a.lastName.length>0&&(a.name+=a.lastName[0],a.lastName.length>1&&(this.degraded=!0));else if(typeof a.familyName!="undefined"||typeof a.givenName!="undefined")a.name="",typeof a.givenName!="undefined"&&a.givenName.length>0&&(a.name=a.givenName[0],a.givenName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.familyName!="undefined"&&a.familyName.length>0&&(a.name+=a.familyName[0],a.familyName.length>1&&(this.degraded=!0));typeof a.name=="object"&&a.name!==null&&(a.name.length>1&&(this.degraded=!0),a.name=a.name[0]),typeof a.mbox=="object"&&a.mbox!==null&&(a.mbox.length>1&&(this.degraded=!0),a.mbox=a.mbox[0]),typeof a.mbox_sha1sum=="object"&&a.mbox_sha1sum!==null&&(a.mbox_sha1sum.length>1&&(this.degraded=!0),a.mbox_sha1sum=a.mbox_sha1sum[0]),typeof a.openid=="object"&&a.openid!==null&&(a.openid.length>1&&(this.degraded=!0),a.openid=a.openid[0]),typeof a.account=="object"&&a.account!==null&&typeof a.account.homePage=="undefined"&&typeof a.account.name=="undefined"&&(a.account.length===0?delete a.account:(a.account.length>1&&(this.degraded=!0),a.account=a.account[0])),a.hasOwnProperty("account")&&(a.account instanceof TinCan.AgentAccount?this.account=a.account:this.account=new TinCan.AgentAccount(a.account));for(b=0;b0){b.member=[];for(c=0;c0)for(c=0;c0)if(a==="0.9"||a==="0.95")this[c[d]].length>1&&this.log("[WARNING] version does not support multiple values in: "+c[d]),b[c[d]]=this[c[d]][0].asVersion(a);else{b[c[d]]=[];for(e=0;e>>2]|=(c[e>>>2]>>>24-8*(e%4)&255)<<24-8*((d+e)%4);else if(65535>>2]=c[e>>>2];else b.push.apply(b,c);return this.sigBytes+=a,this},clamp:function(){var b=this.words,c=this.sigBytes;b[c>>>2]&=4294967295<<32-8*(c%4),b.length=a.ceil(c/4)},clone:function(){var a=e.clone.call(this);return a.words=this.words.slice(0),a},random:function(b){for(var c=[],d=0;d>>2]>>>24-8*(d%4)&255;c.push((e>>>4).toString(16)),c.push((e&15).toString(16))}return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>3]|=parseInt(a.substr(d,2),16)<<24-4*(d%8);return f.create(c,b/2)}},i=g.Latin1={stringify:function(a){for(var b=a.words,a=a.sigBytes,c=[],d=0;d>>2]>>>24-8*(d%4)&255));return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>2]|=(a.charCodeAt(d)&255)<<24-8*(d%4);return f.create(c,b)}},j=g.Utf8={stringify:function(a){try{return decodeURIComponent(escape(i.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data")}},parse:function(a){return i.parse(unescape(encodeURIComponent(a)))}},k=d.BufferedBlockAlgorithm=e.extend({reset:function(){this._data=f.create(),this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a)),this._data.concat(a),this._nDataBytes+=a.sigBytes},_process:function(b){var c=this._data,d=c.words,e=c.sigBytes,g=this.blockSize,h=e/(4*g),h=b?a.ceil(h):a.max((h|0)-this._minBufferSize,0),b=h*g,e=a.min(4*b,e);if(b){for(var i=0;ik;k++){if(16>k)d[k]=a[b+k]|0;else{var l=d[k-3]^d[k-8]^d[k-14]^d[k-16];d[k]=l<<1|l>>>31}l=(e<<5|e>>>27)+j+d[k],l=20>k?l+((f&g|~f&i)+1518500249):40>k?l+((f^g^i)+1859775393):60>k?l+((f&g|f&i|g&i)-1894007588):l+((f^g^i)-899497514),j=i,i=g,g=f<<30|f>>>2,f=e,e=l}c[0]=c[0]+e|0,c[1]=c[1]+f|0,c[2]=c[2]+g|0,c[3]=c[3]+i|0,c[4]=c[4]+j|0},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,d=8*a.sigBytes;b[d>>>5]|=128<<24-d%32,b[(d+64>>>9<<4)+15]=c,a.sigBytes=4*b.length,this._process()}});a.SHA1=b._createHelper(e),a.HmacSHA1=b._createHmacHelper(e)})(),function(){var a=CryptoJS,b=a.lib,c=b.WordArray,d=a.enc,e=d.Base64={stringify:function(a){var b=a.words,c=a.sigBytes,d=this._map;a.clamp();var e=[];for(var f=0;f>>2]>>>24-f%4*8&255,h=b[f+1>>>2]>>>24-(f+1)%4*8&255,i=b[f+2>>>2]>>>24-(f+2)%4*8&255,j=g<<16|h<<8|i;for(var k=0;k<4&&f+k*.75>>6*(3-k)&63))}var l=d.charAt(64);if(l)while(e.length%4)e.push(l);return e.join("")},parse:function(a){a=a.replace(/\s/g,"");var b=a.length,d=this._map,e=d.charAt(64);if(e){var f=a.indexOf(e);f!=-1&&(b=f)}var g=[],h=0;for(var i=0;i>>6-i%4*2;g[h>>>2]|=(j|k)<<24-h%4*8,h++}return c.create(g,h)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(); diff --git a/build/tincan.js b/build/tincan.js index 442a754..8ba3abf 100644 --- a/build/tincan.js +++ b/build/tincan.js @@ -1625,6 +1625,26 @@ TinCan client library getServerRoot: function (absoluteUrl) { var urlParts = absoluteUrl.split("/"); return urlParts[0] + "//" + urlParts[2]; + }, + + /** + @method getContentTypeFromHeader + @static + @param {String} Content-Type header value + @return {String} Primary value from Content-Type + */ + getContentTypeFromHeader: function (header) { + return (String(header).split(";"))[0]; + }, + + /** + @method isApplicationJSON + @static + @param {String} Content-Type header value + @return {Boolean} whether "application/json" was matched + */ + isApplicationJSON: function (header) { + return TinCan.Utils.getContentTypeFromHeader(header).toLowerCase().indexOf("application/json") === 0; } }; }()); @@ -2771,7 +2791,7 @@ TinCan client library result.contentType = xhr.getResponseHeader("Content-Type"); } - if (result.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(result.contentType)) { try { result.contents = JSON.parse(result.contents); } catch (ex) { @@ -2812,7 +2832,7 @@ TinCan client library } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.state.contentType = requestResult.xhr.getResponseHeader("Content-Type"); } - if (requestResult.state.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(requestResult.state.contentType)) { try { requestResult.state.contents = JSON.parse(requestResult.state.contents); } catch (ex) { @@ -2858,7 +2878,7 @@ TinCan client library cfg.contentType = "application/octet-stream"; } - if (typeof val === "object" && cfg.contentType === "application/json") { + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { val = JSON.stringify(val); } @@ -3024,7 +3044,7 @@ TinCan client library } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { result.contentType = xhr.getResponseHeader("Content-Type"); } - if (result.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(result.contentType)) { try { result.contents = JSON.parse(result.contents); } catch (ex) { @@ -3066,7 +3086,7 @@ TinCan client library } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); } - if (requestResult.profile.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) { try { requestResult.profile.contents = JSON.parse(requestResult.profile.contents); } catch (ex) { @@ -3106,7 +3126,7 @@ TinCan client library cfg.contentType = "application/octet-stream"; } - if (typeof val === "object" && cfg.contentType === "application/json") { + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { val = JSON.stringify(val); } @@ -3247,7 +3267,7 @@ TinCan client library } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { result.contentType = xhr.getResponseHeader("Content-Type"); } - if (result.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(result.contentType)) { try { result.contents = JSON.parse(result.contents); } catch (ex) { @@ -3289,7 +3309,7 @@ TinCan client library } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); } - if (requestResult.profile.contentType === "application/json") { + if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) { try { requestResult.profile.contents = JSON.parse(requestResult.profile.contents); } catch (ex) { @@ -3329,7 +3349,7 @@ TinCan client library cfg.contentType = "application/octet-stream"; } - if (typeof val === "object" && cfg.contentType === "application/json") { + if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) { val = JSON.stringify(val); } diff --git a/yuidoc.json b/yuidoc.json index 0597a84..d39aa3a 100644 --- a/yuidoc.json +++ b/yuidoc.json @@ -1,5 +1,5 @@ { - "version": "0.8.0", + "version": "0.8.1", "name": "TinCanJS", "description": "Library for working with Tin Can API in JavaScript", "url": "http://rusticisoftware.github.com/TinCanJS/", From f16aa9ae6c8aeead5e57afff24081691f9818dd7 Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Mon, 12 Aug 2013 14:06:46 -0500 Subject: [PATCH 13/14] Fix to handle synchronization of asynx XDomainRequest * Adds handling of HTTP status via fake status since XDR doesn't provide the real status --- src/LRS.js | 80 +++++++++++++++++++++++++++++------------------ test/js/TinCan.js | 2 +- yuidoc.json | 2 +- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/LRS.js b/src/LRS.js index 3b71ed0..f77e5fa 100644 --- a/src/LRS.js +++ b/src/LRS.js @@ -286,6 +286,7 @@ TinCan client library headers = {}, data, requestCompleteResult, + syncFakeStatus, until, prop, pairs = [], @@ -293,17 +294,26 @@ TinCan client library ; // Setup request callback - function requestComplete () { + function requestComplete (fakeStatus) { self.log("requestComplete: " + finished + ", xhr.status: " + xhr.status); var notFoundOk, httpStatus; // - // older versions of IE don't properly handle 204 status codes - // so correct when receiving a 1223 to be 204 locally - // http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request + // XDomainRequest doesn't give us a way to get the status, + // so allow passing in a forged one // - httpStatus = (xhr.status === 1223) ? 204 : xhr.status; + if (typeof xhr.status === "undefined") { + httpStatus = fakeStatus; + } + else { + // + // older versions of IE don't properly handle 204 status codes + // so correct when receiving a 1223 to be 204 locally + // http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request + // + httpStatus = (xhr.status === 1223) ? 204 : xhr.status; + } if (! finished) { // may be in sync or async mode, using XMLHttpRequest or IE XDomainRequest, onreadystatechange or @@ -312,7 +322,7 @@ TinCan client library finished = true; notFoundOk = (cfg.ignore404 && httpStatus === 404); - if (httpStatus === undefined || (httpStatus >= 200 && httpStatus < 400) || notFoundOk) { + if ((httpStatus >= 200 && httpStatus < 400) || notFoundOk) { if (cfg.callback) { cfg.callback(null, xhr); } @@ -456,12 +466,28 @@ TinCan client library xhr = new XDomainRequest (); xhr.open("POST", fullUrl); - xhr.onload = function () { - requestComplete(); - }; - xhr.onerror = function () { - requestComplete(); - }; + if (! cfg.callback) { + xhr.onload = function () { + syncFakeStatus = 200; + }; + xhr.onerror = function () { + syncFakeStatus = 400; + }; + xhr.ontimeout = function () { + syncFakeStatus = 0; + }; + } + else { + xhr.onload = function () { + requestComplete(200); + }; + xhr.onerror = function () { + requestComplete(400); + }; + xhr.ontimeout = function () { + requestComplete(0); + }; + } // IE likes to randomly abort requests when some handlers // aren't defined, so define them with no-ops, see: @@ -469,7 +495,6 @@ TinCan client library // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ // http://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified // - xhr.ontimeout = function () {}; xhr.onprogress = function () {}; xhr.timeout = 0; } @@ -483,22 +508,8 @@ TinCan client library // including jQuery (https://github.com/jquery/jquery/blob/1.10.2/src/ajax.js#L549 // https://github.com/jquery/jquery/blob/1.10.2/src/ajax/xhr.js#L97) // - // I'm wondering if the setTimeout wrapper suggested in the links - // above for XDR solves the random exception issue, but don't know - // for sure - // try { - if (this._requestMode === XDR) { - setTimeout( - function () { - xhr.send(data); - }, - 0 - ); - } - else { - xhr.send(data); - } + xhr.send(data); } catch (ex) { this.log("sendRequest caught send exception: " + ex); @@ -508,13 +519,14 @@ TinCan client library // synchronous if (this._requestMode === XDR) { // synchronous call in IE, with no synchronous mode available - until = 1000 + Date.now(); + until = 10000 + Date.now(); this.log("sendRequest - until: " + until + ", finished: " + finished); - while (Date.now() < until && ! finished) { + while (Date.now() < until && typeof syncFakeStatus === "undefined") { //this.log("calling __delay"); this.__delay(); } + return requestComplete(syncFakeStatus); } return requestComplete(); } @@ -1135,7 +1147,8 @@ TinCan client library } if (typeof xhr.contentType !== "undefined") { - // most likely an XDomainRequest which has .contentType + // most likely an XDomainRequest which has .contentType, + // for the ones that it supports result.contentType = xhr.contentType; } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { result.contentType = xhr.getResponseHeader("Content-Type"); @@ -1178,6 +1191,7 @@ TinCan client library } if (typeof requestResult.xhr.contentType !== "undefined") { // most likely an XDomainRequest which has .contentType + // for the ones that it supports requestResult.state.contentType = requestResult.xhr.contentType; } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.state.contentType = requestResult.xhr.getResponseHeader("Content-Type"); @@ -1390,6 +1404,7 @@ TinCan client library } if (typeof xhr.contentType !== "undefined") { // most likely an XDomainRequest which has .contentType + // for the ones that it supports result.contentType = xhr.contentType; } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { result.contentType = xhr.getResponseHeader("Content-Type"); @@ -1432,6 +1447,7 @@ TinCan client library } if (typeof requestResult.xhr.contentType !== "undefined") { // most likely an XDomainRequest which has .contentType + // for the ones that it supports requestResult.profile.contentType = requestResult.xhr.contentType; } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); @@ -1613,6 +1629,7 @@ TinCan client library } if (typeof xhr.contentType !== "undefined") { // most likely an XDomainRequest which has .contentType + // for the ones that it supports result.contentType = xhr.contentType; } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { result.contentType = xhr.getResponseHeader("Content-Type"); @@ -1655,6 +1672,7 @@ TinCan client library } if (typeof requestResult.xhr.contentType !== "undefined") { // most likely an XDomainRequest which has .contentType + // for the ones that it supports requestResult.profile.contentType = requestResult.xhr.contentType; } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); diff --git a/test/js/TinCan.js b/test/js/TinCan.js index 0179959..16a480f 100644 --- a/test/js/TinCan.js +++ b/test/js/TinCan.js @@ -677,7 +677,7 @@ options = { activity: new TinCan.Activity( { - id: "http://tincanapi.com/TinCanJS/Test/TinCan_setActivityProfile/sync/" + v + id: "http://tincanapi.com/TinCanJS/Test/TinCan_setActivityProfile/syncJSON/" + v } ), contentType: "application/json" diff --git a/yuidoc.json b/yuidoc.json index d39aa3a..eb21c48 100644 --- a/yuidoc.json +++ b/yuidoc.json @@ -1,5 +1,5 @@ { - "version": "0.8.1", + "version": "0.8.2", "name": "TinCanJS", "description": "Library for working with Tin Can API in JavaScript", "url": "http://rusticisoftware.github.com/TinCanJS/", From 1cf874a273e6ce37b076c734a07f1a281273e55c Mon Sep 17 00:00:00 2001 From: "Brian J. Miller" Date: Mon, 12 Aug 2013 14:08:02 -0500 Subject: [PATCH 14/14] Build 0.8.2 --- build/tincan-min.js | 2 +- build/tincan.js | 80 +++++++++++++++++++++++++++------------------ 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/build/tincan-min.js b/build/tincan-min.js index 18375d6..6513915 100644 --- a/build/tincan-min.js +++ b/build/tincan-min.js @@ -14,4 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. */ -;var TinCan;(function(){"use strict";var _environment=null,_reservedQSParams={statementId:!0,voidedStatementId:!0,verb:!0,object:!0,registration:!0,context:!0,actor:!0,since:!0,until:!0,limit:!0,authoritative:!0,sparse:!0,instructor:!0,ascending:!0,continueToken:!0,agent:!0,activityId:!0,stateId:!0,profileId:!0,activity_platform:!0,grouping:!0,"Accept-Language":!0};TinCan=function(a){this.log("constructor"),this.environment=null,this.recordStores=[],this.actor=null,this.activity=null,this.registration=null,this.context=null,this.init(a)},TinCan.prototype={LOG_SRC:"TinCan",log:function(a,b){TinCan.DEBUG&&typeof console!="undefined"&&console.log&&(b=b||this.LOG_SRC||"TinCan",console.log("TinCan."+b+": "+a))},init:function(a){this.log("init");var b;a=a||{},a.hasOwnProperty("url")&&a.url!==""&&this._initFromQueryString(a.url);if(a.hasOwnProperty("recordStores")&&a.recordStores!==undefined)for(b=0;b0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatement - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatement - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],c.retrieveStatement(a,{callback:b});d="[warning] getStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},voidStatement:function(a,b,c){this.log("voidStatement");var d=this,e,f,g,h=this.recordStores.length,i,j,k=[],l,m=[];a instanceof TinCan.Statement&&(a=a.id),typeof c.actor!="undefined"?f=c.actor:this.actor!==null&&(f=this.actor),g=new TinCan.Statement({actor:f,verb:{id:"http://adlnet.gov/expapi/verbs/voided"},target:{objectType:"StatementRef",id:a}});if(h>0){typeof b=="function"&&(l=function(a,c){var e;d.log("voidStatement - callbackWrapper: "+h),h>1?(h-=1,m.push({err:a,xhr:c})):h===1?(m.push({err:a,xhr:c}),e=[m,g],b.apply(this,e)):d.log("voidStatement - unexpected record store count: "+h)});for(i=0;i0)return c=this.recordStores[0],c.retrieveVoidedStatement(a,{callback:b});d="[warning] getVoidedStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},sendStatements:function(a,b){this.log("sendStatements");var c=this,d,e=[],f=this.recordStores.length,g,h,i=[],j,k=[];if(a.length===0)typeof b=="function"&&b.apply(this,[null,e]);else{for(g=0;g0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatements - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatements - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],a=a||{},d=a.params||{},a.sendActor&&this.actor!==null&&(c.version==="0.9"||c.version==="0.95"?d.actor=this.actor:d.agent=this.actor),a.sendActivity&&this.activity!==null&&(c.version==="0.9"||c.version==="0.95"?d.target=this.activity:d.activity=this.activity),typeof d.registration=="undefined"&&this.registration!==null&&(d.registration=this.registration),b={params:d},typeof a.callback!="undefined"&&(b.callback=a.callback),c.queryStatements(b);e="[warning] getStatements: No LRSs added yet (statements not read)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getState:function(a,b){this.log("getState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveState(a,c);e="[warning] getState: No LRSs added yet (state not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setState:function(a,b,c){this.log("setState");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor,activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.registration!="undefined"?d.registration=c.registration:this.registration!==null&&(d.registration=this.registration),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),typeof c.callback!="undefined"&&(d.callback=c.callback),e.saveState(a,b,d);f="[warning] setState: No LRSs added yet (state not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteState:function(a,b){this.log("deleteState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropState(a,c);e="[warning] deleteState: No LRSs added yet (state not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getActivityProfile:function(a,b){this.log("getActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveActivityProfile(a,c);e="[warning] getActivityProfile: No LRSs added yet (activity profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setActivityProfile:function(a,b,c){this.log("setActivityProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveActivityProfile(a,b,d);f="[warning] setActivityProfile: No LRSs added yet (activity profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteActivityProfile:function(a,b){this.log("deleteActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropActivityProfile(a,c);e="[warning] deleteActivityProfile: No LRSs added yet (activity profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getAgentProfile:function(a,b){this.log("getAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveAgentProfile(a,c);e="[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setAgentProfile:function(a,b,c){this.log("setAgentProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveAgentProfile(a,b,d);f="[warning] setAgentProfile: No LRSs added yet (agent profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteAgentProfile:function(a,b){this.log("deleteAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropAgentProfile(a,c);e="[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)}},TinCan.DEBUG=!1,TinCan.enableDebug=function(){TinCan.DEBUG=!0},TinCan.disableDebug=function(){TinCan.DEBUG=!1},TinCan.versions=function(){return["1.0.0","0.95","0.9"]},TinCan.environment=function(){return _environment===null&&(_environment={},typeof window!="undefined"?(_environment.isBrowser=!0,_environment.hasCORS=!1,_environment.useXDR=!1,typeof XMLHttpRequest!="undefined"&&typeof (new XMLHttpRequest).withCredentials!="undefined"?_environment.hasCORS=!0:typeof XDomainRequest!="undefined"&&(_environment.hasCORS=!0,_environment.useXDR=!0)):_environment.isBrowser=!1),_environment},TinCan.environment().isBrowser&&(window.JSON||(window.JSON={parse:function(sJSON){return eval("("+sJSON+")")},stringify:function(a){var b="",c,d;if(a instanceof Object){if(a.constructor===Array){for(c=0;c1)d="0"+d,c/=10;return d}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())+"."+b(a.getUTCMilliseconds(),3)+"Z"},getSHA1String:function(a){return CryptoJS.SHA1(a).toString(CryptoJS.enc.Hex)},getBase64String:function(a){return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(a))},getLangDictionaryValue:function(a,b){var c=this[a],d;if(typeof b!="undefined"&&typeof c[b]!="undefined")return c[b];if(typeof c.und!="undefined")return c.und;if(typeof c["en-US"]!="undefined")return c["en-US"];for(d in c)if(c.hasOwnProperty(d))return c[d];return""},parseURL:function(a){var b=String(a).split("?"),c,d,e,f={};if(b.length===2){c=b[1].split("&");for(e=0;e=200&&b<400||a))return j={err:b,xhr:d},n.alertOnRequestFailure&&(b===0?alert("[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint ("+b+")"):alert("[warning] There was a problem communicating with the Learning Record Store. ("+b+" | "+d.responseText+")")),c.callback&&c.callback(b,d),j;if(!c.callback)return j={err:null,xhr:d},j;c.callback(null,d)}this.log("sendRequest");var d,e=!1,f=window.location,g=this.endpoint+c.url,h={},i,j,k,l,m=[],n=this;c.url.indexOf("http")===0&&(g=c.url);if(this.extended!==null){c.params=c.params||{};for(l in this.extended)this.extended.hasOwnProperty(l)&&(c.params.hasOwnProperty(l)||this.extended[l]!==null&&(c.params[l]=this.extended[l]))}h.Authorization=this.auth,this.version!=="0.9"&&(h["X-Experience-API-Version"]=this.version);for(l in c.headers)c.headers.hasOwnProperty(l)&&(h[l]=c.headers[l]);if(this._requestMode===b){this.log("sendRequest using XMLHttpRequest");for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));m.length>0&&(g+="?"+m.join("&")),this.log("sendRequest using XMLHttpRequest - async: "+(typeof c.callback!="undefined")),typeof XMLHttpRequest!="undefined"?d=new XMLHttpRequest:d=new ActiveXObject("Microsoft.XMLHTTP"),d.open(c.method,g,typeof c.callback!="undefined");for(l in h)h.hasOwnProperty(l)&&d.setRequestHeader(l,h[l]);typeof c.data!="undefined"&&(c.data+=""),i=c.data,d.onreadystatechange=function(){n.log("xhr.onreadystatechange - xhr.readyState: "+d.readyState),d.readyState===4&&o()}}else if(this._requestMode===a){this.log("sendRequest using XDomainRequest"),g+="?method="+c.method;for(l in c.params)c.params.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(c.params[l]));for(l in h)h.hasOwnProperty(l)&&m.push(l+"="+encodeURIComponent(h[l]));c.data!==null&&m.push("content="+encodeURIComponent(c.data)),i=m.join("&"),d=new XDomainRequest,d.open("POST",g),d.onload=function(){o()},d.onerror=function(){o()},d.ontimeout=function(){},d.onprogress=function(){},d.timeout=0}else this.log("sendRequest unrecognized _requestMode: "+this._requestMode);try{this._requestMode===a?setTimeout(function(){d.send(i)},0):d.send(i)}catch(p){this.log("sendRequest caught send exception: "+p)}if(!c.callback){if(this._requestMode===a){k=1e3+Date.now(),this.log("sendRequest - until: "+k+", finished: "+e);while(Date.now()0&&(a.name=a.firstName[0],a.firstName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.lastName!="undefined"&&a.lastName.length>0&&(a.name+=a.lastName[0],a.lastName.length>1&&(this.degraded=!0));else if(typeof a.familyName!="undefined"||typeof a.givenName!="undefined")a.name="",typeof a.givenName!="undefined"&&a.givenName.length>0&&(a.name=a.givenName[0],a.givenName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.familyName!="undefined"&&a.familyName.length>0&&(a.name+=a.familyName[0],a.familyName.length>1&&(this.degraded=!0));typeof a.name=="object"&&a.name!==null&&(a.name.length>1&&(this.degraded=!0),a.name=a.name[0]),typeof a.mbox=="object"&&a.mbox!==null&&(a.mbox.length>1&&(this.degraded=!0),a.mbox=a.mbox[0]),typeof a.mbox_sha1sum=="object"&&a.mbox_sha1sum!==null&&(a.mbox_sha1sum.length>1&&(this.degraded=!0),a.mbox_sha1sum=a.mbox_sha1sum[0]),typeof a.openid=="object"&&a.openid!==null&&(a.openid.length>1&&(this.degraded=!0),a.openid=a.openid[0]),typeof a.account=="object"&&a.account!==null&&typeof a.account.homePage=="undefined"&&typeof a.account.name=="undefined"&&(a.account.length===0?delete a.account:(a.account.length>1&&(this.degraded=!0),a.account=a.account[0])),a.hasOwnProperty("account")&&(a.account instanceof TinCan.AgentAccount?this.account=a.account:this.account=new TinCan.AgentAccount(a.account));for(b=0;b0){b.member=[];for(c=0;c0)for(c=0;c0)if(a==="0.9"||a==="0.95")this[c[d]].length>1&&this.log("[WARNING] version does not support multiple values in: "+c[d]),b[c[d]]=this[c[d]][0].asVersion(a);else{b[c[d]]=[];for(e=0;e>>2]|=(c[e>>>2]>>>24-8*(e%4)&255)<<24-8*((d+e)%4);else if(65535>>2]=c[e>>>2];else b.push.apply(b,c);return this.sigBytes+=a,this},clamp:function(){var b=this.words,c=this.sigBytes;b[c>>>2]&=4294967295<<32-8*(c%4),b.length=a.ceil(c/4)},clone:function(){var a=e.clone.call(this);return a.words=this.words.slice(0),a},random:function(b){for(var c=[],d=0;d>>2]>>>24-8*(d%4)&255;c.push((e>>>4).toString(16)),c.push((e&15).toString(16))}return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>3]|=parseInt(a.substr(d,2),16)<<24-4*(d%8);return f.create(c,b/2)}},i=g.Latin1={stringify:function(a){for(var b=a.words,a=a.sigBytes,c=[],d=0;d>>2]>>>24-8*(d%4)&255));return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>2]|=(a.charCodeAt(d)&255)<<24-8*(d%4);return f.create(c,b)}},j=g.Utf8={stringify:function(a){try{return decodeURIComponent(escape(i.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data")}},parse:function(a){return i.parse(unescape(encodeURIComponent(a)))}},k=d.BufferedBlockAlgorithm=e.extend({reset:function(){this._data=f.create(),this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a)),this._data.concat(a),this._nDataBytes+=a.sigBytes},_process:function(b){var c=this._data,d=c.words,e=c.sigBytes,g=this.blockSize,h=e/(4*g),h=b?a.ceil(h):a.max((h|0)-this._minBufferSize,0),b=h*g,e=a.min(4*b,e);if(b){for(var i=0;ik;k++){if(16>k)d[k]=a[b+k]|0;else{var l=d[k-3]^d[k-8]^d[k-14]^d[k-16];d[k]=l<<1|l>>>31}l=(e<<5|e>>>27)+j+d[k],l=20>k?l+((f&g|~f&i)+1518500249):40>k?l+((f^g^i)+1859775393):60>k?l+((f&g|f&i|g&i)-1894007588):l+((f^g^i)-899497514),j=i,i=g,g=f<<30|f>>>2,f=e,e=l}c[0]=c[0]+e|0,c[1]=c[1]+f|0,c[2]=c[2]+g|0,c[3]=c[3]+i|0,c[4]=c[4]+j|0},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,d=8*a.sigBytes;b[d>>>5]|=128<<24-d%32,b[(d+64>>>9<<4)+15]=c,a.sigBytes=4*b.length,this._process()}});a.SHA1=b._createHelper(e),a.HmacSHA1=b._createHmacHelper(e)})(),function(){var a=CryptoJS,b=a.lib,c=b.WordArray,d=a.enc,e=d.Base64={stringify:function(a){var b=a.words,c=a.sigBytes,d=this._map;a.clamp();var e=[];for(var f=0;f>>2]>>>24-f%4*8&255,h=b[f+1>>>2]>>>24-(f+1)%4*8&255,i=b[f+2>>>2]>>>24-(f+2)%4*8&255,j=g<<16|h<<8|i;for(var k=0;k<4&&f+k*.75>>6*(3-k)&63))}var l=d.charAt(64);if(l)while(e.length%4)e.push(l);return e.join("")},parse:function(a){a=a.replace(/\s/g,"");var b=a.length,d=this._map,e=d.charAt(64);if(e){var f=a.indexOf(e);f!=-1&&(b=f)}var g=[],h=0;for(var i=0;i>>6-i%4*2;g[h>>>2]|=(j|k)<<24-h%4*8,h++}return c.create(g,h)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(); +;var TinCan;(function(){"use strict";var _environment=null,_reservedQSParams={statementId:!0,voidedStatementId:!0,verb:!0,object:!0,registration:!0,context:!0,actor:!0,since:!0,until:!0,limit:!0,authoritative:!0,sparse:!0,instructor:!0,ascending:!0,continueToken:!0,agent:!0,activityId:!0,stateId:!0,profileId:!0,activity_platform:!0,grouping:!0,"Accept-Language":!0};TinCan=function(a){this.log("constructor"),this.environment=null,this.recordStores=[],this.actor=null,this.activity=null,this.registration=null,this.context=null,this.init(a)},TinCan.prototype={LOG_SRC:"TinCan",log:function(a,b){TinCan.DEBUG&&typeof console!="undefined"&&console.log&&(b=b||this.LOG_SRC||"TinCan",console.log("TinCan."+b+": "+a))},init:function(a){this.log("init");var b;a=a||{},a.hasOwnProperty("url")&&a.url!==""&&this._initFromQueryString(a.url);if(a.hasOwnProperty("recordStores")&&a.recordStores!==undefined)for(b=0;b0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatement - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatement - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],c.retrieveStatement(a,{callback:b});d="[warning] getStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},voidStatement:function(a,b,c){this.log("voidStatement");var d=this,e,f,g,h=this.recordStores.length,i,j,k=[],l,m=[];a instanceof TinCan.Statement&&(a=a.id),typeof c.actor!="undefined"?f=c.actor:this.actor!==null&&(f=this.actor),g=new TinCan.Statement({actor:f,verb:{id:"http://adlnet.gov/expapi/verbs/voided"},target:{objectType:"StatementRef",id:a}});if(h>0){typeof b=="function"&&(l=function(a,c){var e;d.log("voidStatement - callbackWrapper: "+h),h>1?(h-=1,m.push({err:a,xhr:c})):h===1?(m.push({err:a,xhr:c}),e=[m,g],b.apply(this,e)):d.log("voidStatement - unexpected record store count: "+h)});for(i=0;i0)return c=this.recordStores[0],c.retrieveVoidedStatement(a,{callback:b});d="[warning] getVoidedStatement: No LRSs added yet (statement not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+d):this.log(d)},sendStatements:function(a,b){this.log("sendStatements");var c=this,d,e=[],f=this.recordStores.length,g,h,i=[],j,k=[];if(a.length===0)typeof b=="function"&&b.apply(this,[null,e]);else{for(g=0;g0){typeof b=="function"&&(j=function(a,d){var g;c.log("sendStatements - callbackWrapper: "+f),f>1?(f-=1,k.push({err:a,xhr:d})):f===1?(k.push({err:a,xhr:d}),g=[k,e],b.apply(this,g)):c.log("sendStatements - unexpected record store count: "+f)});for(g=0;g0)return c=this.recordStores[0],a=a||{},d=a.params||{},a.sendActor&&this.actor!==null&&(c.version==="0.9"||c.version==="0.95"?d.actor=this.actor:d.agent=this.actor),a.sendActivity&&this.activity!==null&&(c.version==="0.9"||c.version==="0.95"?d.target=this.activity:d.activity=this.activity),typeof d.registration=="undefined"&&this.registration!==null&&(d.registration=this.registration),b={params:d},typeof a.callback!="undefined"&&(b.callback=a.callback),c.queryStatements(b);e="[warning] getStatements: No LRSs added yet (statements not read)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getState:function(a,b){this.log("getState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveState(a,c);e="[warning] getState: No LRSs added yet (state not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setState:function(a,b,c){this.log("setState");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor,activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.registration!="undefined"?d.registration=c.registration:this.registration!==null&&(d.registration=this.registration),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),typeof c.callback!="undefined"&&(d.callback=c.callback),e.saveState(a,b,d);f="[warning] setState: No LRSs added yet (state not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteState:function(a,b){this.log("deleteState");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor,activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.registration!="undefined"?c.registration=b.registration:this.registration!==null&&(c.registration=this.registration),typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropState(a,c);e="[warning] deleteState: No LRSs added yet (state not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getActivityProfile:function(a,b){this.log("getActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveActivityProfile(a,c);e="[warning] getActivityProfile: No LRSs added yet (activity profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setActivityProfile:function(a,b,c){this.log("setActivityProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={activity:typeof c.activity!="undefined"?c.activity:this.activity},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveActivityProfile(a,b,d);f="[warning] setActivityProfile: No LRSs added yet (activity profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteActivityProfile:function(a,b){this.log("deleteActivityProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={activity:typeof b.activity!="undefined"?b.activity:this.activity},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropActivityProfile(a,c);e="[warning] deleteActivityProfile: No LRSs added yet (activity profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},getAgentProfile:function(a,b){this.log("getAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.retrieveAgentProfile(a,c);e="[warning] getAgentProfile: No LRSs added yet (agent profile not retrieved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)},setAgentProfile:function(a,b,c){this.log("setAgentProfile");var d,e,f;if(this.recordStores.length>0)return e=this.recordStores[0],c=c||{},d={agent:typeof c.agent!="undefined"?c.agent:this.actor},typeof c.callback!="undefined"&&(d.callback=c.callback),typeof c.lastSHA1!="undefined"&&(d.lastSHA1=c.lastSHA1),typeof c.contentType!="undefined"&&(d.contentType=c.contentType),e.saveAgentProfile(a,b,d);f="[warning] setAgentProfile: No LRSs added yet (agent profile not saved)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+f):this.log(f)},deleteAgentProfile:function(a,b){this.log("deleteAgentProfile");var c,d,e;if(this.recordStores.length>0)return d=this.recordStores[0],b=b||{},c={agent:typeof b.agent!="undefined"?b.agent:this.actor},typeof b.callback!="undefined"&&(c.callback=b.callback),d.dropAgentProfile(a,c);e="[warning] deleteAgentProfile: No LRSs added yet (agent profile not deleted)",TinCan.environment().isBrowser?alert(this.LOG_SRC+": "+e):this.log(e)}},TinCan.DEBUG=!1,TinCan.enableDebug=function(){TinCan.DEBUG=!0},TinCan.disableDebug=function(){TinCan.DEBUG=!1},TinCan.versions=function(){return["1.0.0","0.95","0.9"]},TinCan.environment=function(){return _environment===null&&(_environment={},typeof window!="undefined"?(_environment.isBrowser=!0,_environment.hasCORS=!1,_environment.useXDR=!1,typeof XMLHttpRequest!="undefined"&&typeof (new XMLHttpRequest).withCredentials!="undefined"?_environment.hasCORS=!0:typeof XDomainRequest!="undefined"&&(_environment.hasCORS=!0,_environment.useXDR=!0)):_environment.isBrowser=!1),_environment},TinCan.environment().isBrowser&&(window.JSON||(window.JSON={parse:function(sJSON){return eval("("+sJSON+")")},stringify:function(a){var b="",c,d;if(a instanceof Object){if(a.constructor===Array){for(c=0;c1)d="0"+d,c/=10;return d}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())+"."+b(a.getUTCMilliseconds(),3)+"Z"},getSHA1String:function(a){return CryptoJS.SHA1(a).toString(CryptoJS.enc.Hex)},getBase64String:function(a){return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Latin1.parse(a))},getLangDictionaryValue:function(a,b){var c=this[a],d;if(typeof b!="undefined"&&typeof c[b]!="undefined")return c[b];if(typeof c.und!="undefined")return c.und;if(typeof c["en-US"]!="undefined")return c["en-US"];for(d in c)if(c.hasOwnProperty(d))return c[d];return""},parseURL:function(a){var b=String(a).split("?"),c,d,e,f={};if(b.length===2){c=b[1].split("&");for(e=0;e=200&&f<400||b))return j={err:f,xhr:d},o.alertOnRequestFailure&&(f===0?alert("[warning] There was a problem communicating with the Learning Record Store. Aborted, offline, or invalid CORS endpoint ("+f+")"):alert("[warning] There was a problem communicating with the Learning Record Store. ("+f+" | "+d.responseText+")")),c.callback&&c.callback(f,d),j;if(!c.callback)return j={err:null,xhr:d},j;c.callback(null,d)}this.log("sendRequest");var d,e=!1,f=window.location,g=this.endpoint+c.url,h={},i,j,k,l,m,n=[],o=this;c.url.indexOf("http")===0&&(g=c.url);if(this.extended!==null){c.params=c.params||{};for(m in this.extended)this.extended.hasOwnProperty(m)&&(c.params.hasOwnProperty(m)||this.extended[m]!==null&&(c.params[m]=this.extended[m]))}h.Authorization=this.auth,this.version!=="0.9"&&(h["X-Experience-API-Version"]=this.version);for(m in c.headers)c.headers.hasOwnProperty(m)&&(h[m]=c.headers[m]);if(this._requestMode===b){this.log("sendRequest using XMLHttpRequest");for(m in c.params)c.params.hasOwnProperty(m)&&n.push(m+"="+encodeURIComponent(c.params[m]));n.length>0&&(g+="?"+n.join("&")),this.log("sendRequest using XMLHttpRequest - async: "+(typeof c.callback!="undefined")),typeof XMLHttpRequest!="undefined"?d=new XMLHttpRequest:d=new ActiveXObject("Microsoft.XMLHTTP"),d.open(c.method,g,typeof c.callback!="undefined");for(m in h)h.hasOwnProperty(m)&&d.setRequestHeader(m,h[m]);typeof c.data!="undefined"&&(c.data+=""),i=c.data,d.onreadystatechange=function(){o.log("xhr.onreadystatechange - xhr.readyState: "+d.readyState),d.readyState===4&&p()}}else if(this._requestMode===a){this.log("sendRequest using XDomainRequest"),g+="?method="+c.method;for(m in c.params)c.params.hasOwnProperty(m)&&n.push(m+"="+encodeURIComponent(c.params[m]));for(m in h)h.hasOwnProperty(m)&&n.push(m+"="+encodeURIComponent(h[m]));c.data!==null&&n.push("content="+encodeURIComponent(c.data)),i=n.join("&"),d=new XDomainRequest,d.open("POST",g),c.callback?(d.onload=function(){p(200)},d.onerror=function(){p(400)},d.ontimeout=function(){p(0)}):(d.onload=function(){k=200},d.onerror=function(){k=400},d.ontimeout=function(){k=0}),d.onprogress=function(){},d.timeout=0}else this.log("sendRequest unrecognized _requestMode: "+this._requestMode);try{d.send(i)}catch(q){this.log("sendRequest caught send exception: "+q)}if(!c.callback){if(this._requestMode===a){l=1e4+Date.now(),this.log("sendRequest - until: "+l+", finished: "+e);while(Date.now()0&&(a.name=a.firstName[0],a.firstName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.lastName!="undefined"&&a.lastName.length>0&&(a.name+=a.lastName[0],a.lastName.length>1&&(this.degraded=!0));else if(typeof a.familyName!="undefined"||typeof a.givenName!="undefined")a.name="",typeof a.givenName!="undefined"&&a.givenName.length>0&&(a.name=a.givenName[0],a.givenName.length>1&&(this.degraded=!0)),a.name!==""&&(a.name+=" "),typeof a.familyName!="undefined"&&a.familyName.length>0&&(a.name+=a.familyName[0],a.familyName.length>1&&(this.degraded=!0));typeof a.name=="object"&&a.name!==null&&(a.name.length>1&&(this.degraded=!0),a.name=a.name[0]),typeof a.mbox=="object"&&a.mbox!==null&&(a.mbox.length>1&&(this.degraded=!0),a.mbox=a.mbox[0]),typeof a.mbox_sha1sum=="object"&&a.mbox_sha1sum!==null&&(a.mbox_sha1sum.length>1&&(this.degraded=!0),a.mbox_sha1sum=a.mbox_sha1sum[0]),typeof a.openid=="object"&&a.openid!==null&&(a.openid.length>1&&(this.degraded=!0),a.openid=a.openid[0]),typeof a.account=="object"&&a.account!==null&&typeof a.account.homePage=="undefined"&&typeof a.account.name=="undefined"&&(a.account.length===0?delete a.account:(a.account.length>1&&(this.degraded=!0),a.account=a.account[0])),a.hasOwnProperty("account")&&(a.account instanceof TinCan.AgentAccount?this.account=a.account:this.account=new TinCan.AgentAccount(a.account));for(b=0;b0){b.member=[];for(c=0;c0)for(c=0;c0)if(a==="0.9"||a==="0.95")this[c[d]].length>1&&this.log("[WARNING] version does not support multiple values in: "+c[d]),b[c[d]]=this[c[d]][0].asVersion(a);else{b[c[d]]=[];for(e=0;e>>2]|=(c[e>>>2]>>>24-8*(e%4)&255)<<24-8*((d+e)%4);else if(65535>>2]=c[e>>>2];else b.push.apply(b,c);return this.sigBytes+=a,this},clamp:function(){var b=this.words,c=this.sigBytes;b[c>>>2]&=4294967295<<32-8*(c%4),b.length=a.ceil(c/4)},clone:function(){var a=e.clone.call(this);return a.words=this.words.slice(0),a},random:function(b){for(var c=[],d=0;d>>2]>>>24-8*(d%4)&255;c.push((e>>>4).toString(16)),c.push((e&15).toString(16))}return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>3]|=parseInt(a.substr(d,2),16)<<24-4*(d%8);return f.create(c,b/2)}},i=g.Latin1={stringify:function(a){for(var b=a.words,a=a.sigBytes,c=[],d=0;d>>2]>>>24-8*(d%4)&255));return c.join("")},parse:function(a){for(var b=a.length,c=[],d=0;d>>2]|=(a.charCodeAt(d)&255)<<24-8*(d%4);return f.create(c,b)}},j=g.Utf8={stringify:function(a){try{return decodeURIComponent(escape(i.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data")}},parse:function(a){return i.parse(unescape(encodeURIComponent(a)))}},k=d.BufferedBlockAlgorithm=e.extend({reset:function(){this._data=f.create(),this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a)),this._data.concat(a),this._nDataBytes+=a.sigBytes},_process:function(b){var c=this._data,d=c.words,e=c.sigBytes,g=this.blockSize,h=e/(4*g),h=b?a.ceil(h):a.max((h|0)-this._minBufferSize,0),b=h*g,e=a.min(4*b,e);if(b){for(var i=0;ik;k++){if(16>k)d[k]=a[b+k]|0;else{var l=d[k-3]^d[k-8]^d[k-14]^d[k-16];d[k]=l<<1|l>>>31}l=(e<<5|e>>>27)+j+d[k],l=20>k?l+((f&g|~f&i)+1518500249):40>k?l+((f^g^i)+1859775393):60>k?l+((f&g|f&i|g&i)-1894007588):l+((f^g^i)-899497514),j=i,i=g,g=f<<30|f>>>2,f=e,e=l}c[0]=c[0]+e|0,c[1]=c[1]+f|0,c[2]=c[2]+g|0,c[3]=c[3]+i|0,c[4]=c[4]+j|0},_doFinalize:function(){var a=this._data,b=a.words,c=8*this._nDataBytes,d=8*a.sigBytes;b[d>>>5]|=128<<24-d%32,b[(d+64>>>9<<4)+15]=c,a.sigBytes=4*b.length,this._process()}});a.SHA1=b._createHelper(e),a.HmacSHA1=b._createHmacHelper(e)})(),function(){var a=CryptoJS,b=a.lib,c=b.WordArray,d=a.enc,e=d.Base64={stringify:function(a){var b=a.words,c=a.sigBytes,d=this._map;a.clamp();var e=[];for(var f=0;f>>2]>>>24-f%4*8&255,h=b[f+1>>>2]>>>24-(f+1)%4*8&255,i=b[f+2>>>2]>>>24-(f+2)%4*8&255,j=g<<16|h<<8|i;for(var k=0;k<4&&f+k*.75>>6*(3-k)&63))}var l=d.charAt(64);if(l)while(e.length%4)e.push(l);return e.join("")},parse:function(a){a=a.replace(/\s/g,"");var b=a.length,d=this._map,e=d.charAt(64);if(e){var f=a.indexOf(e);f!=-1&&(b=f)}var g=[],h=0;for(var i=0;i>>6-i%4*2;g[h>>>2]|=(j|k)<<24-h%4*8,h++}return c.create(g,h)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(); diff --git a/build/tincan.js b/build/tincan.js index 8ba3abf..d84c09f 100644 --- a/build/tincan.js +++ b/build/tincan.js @@ -1936,6 +1936,7 @@ TinCan client library headers = {}, data, requestCompleteResult, + syncFakeStatus, until, prop, pairs = [], @@ -1943,17 +1944,26 @@ TinCan client library ; // Setup request callback - function requestComplete () { + function requestComplete (fakeStatus) { self.log("requestComplete: " + finished + ", xhr.status: " + xhr.status); var notFoundOk, httpStatus; // - // older versions of IE don't properly handle 204 status codes - // so correct when receiving a 1223 to be 204 locally - // http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request + // XDomainRequest doesn't give us a way to get the status, + // so allow passing in a forged one // - httpStatus = (xhr.status === 1223) ? 204 : xhr.status; + if (typeof xhr.status === "undefined") { + httpStatus = fakeStatus; + } + else { + // + // older versions of IE don't properly handle 204 status codes + // so correct when receiving a 1223 to be 204 locally + // http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request + // + httpStatus = (xhr.status === 1223) ? 204 : xhr.status; + } if (! finished) { // may be in sync or async mode, using XMLHttpRequest or IE XDomainRequest, onreadystatechange or @@ -1962,7 +1972,7 @@ TinCan client library finished = true; notFoundOk = (cfg.ignore404 && httpStatus === 404); - if (httpStatus === undefined || (httpStatus >= 200 && httpStatus < 400) || notFoundOk) { + if ((httpStatus >= 200 && httpStatus < 400) || notFoundOk) { if (cfg.callback) { cfg.callback(null, xhr); } @@ -2106,12 +2116,28 @@ TinCan client library xhr = new XDomainRequest (); xhr.open("POST", fullUrl); - xhr.onload = function () { - requestComplete(); - }; - xhr.onerror = function () { - requestComplete(); - }; + if (! cfg.callback) { + xhr.onload = function () { + syncFakeStatus = 200; + }; + xhr.onerror = function () { + syncFakeStatus = 400; + }; + xhr.ontimeout = function () { + syncFakeStatus = 0; + }; + } + else { + xhr.onload = function () { + requestComplete(200); + }; + xhr.onerror = function () { + requestComplete(400); + }; + xhr.ontimeout = function () { + requestComplete(0); + }; + } // IE likes to randomly abort requests when some handlers // aren't defined, so define them with no-ops, see: @@ -2119,7 +2145,6 @@ TinCan client library // http://cypressnorth.com/programming/internet-explorer-aborting-ajax-requests-fixed/ // http://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified // - xhr.ontimeout = function () {}; xhr.onprogress = function () {}; xhr.timeout = 0; } @@ -2133,22 +2158,8 @@ TinCan client library // including jQuery (https://github.com/jquery/jquery/blob/1.10.2/src/ajax.js#L549 // https://github.com/jquery/jquery/blob/1.10.2/src/ajax/xhr.js#L97) // - // I'm wondering if the setTimeout wrapper suggested in the links - // above for XDR solves the random exception issue, but don't know - // for sure - // try { - if (this._requestMode === XDR) { - setTimeout( - function () { - xhr.send(data); - }, - 0 - ); - } - else { - xhr.send(data); - } + xhr.send(data); } catch (ex) { this.log("sendRequest caught send exception: " + ex); @@ -2158,13 +2169,14 @@ TinCan client library // synchronous if (this._requestMode === XDR) { // synchronous call in IE, with no synchronous mode available - until = 1000 + Date.now(); + until = 10000 + Date.now(); this.log("sendRequest - until: " + until + ", finished: " + finished); - while (Date.now() < until && ! finished) { + while (Date.now() < until && typeof syncFakeStatus === "undefined") { //this.log("calling __delay"); this.__delay(); } + return requestComplete(syncFakeStatus); } return requestComplete(); } @@ -2785,7 +2797,8 @@ TinCan client library } if (typeof xhr.contentType !== "undefined") { - // most likely an XDomainRequest which has .contentType + // most likely an XDomainRequest which has .contentType, + // for the ones that it supports result.contentType = xhr.contentType; } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { result.contentType = xhr.getResponseHeader("Content-Type"); @@ -2828,6 +2841,7 @@ TinCan client library } if (typeof requestResult.xhr.contentType !== "undefined") { // most likely an XDomainRequest which has .contentType + // for the ones that it supports requestResult.state.contentType = requestResult.xhr.contentType; } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.state.contentType = requestResult.xhr.getResponseHeader("Content-Type"); @@ -3040,6 +3054,7 @@ TinCan client library } if (typeof xhr.contentType !== "undefined") { // most likely an XDomainRequest which has .contentType + // for the ones that it supports result.contentType = xhr.contentType; } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { result.contentType = xhr.getResponseHeader("Content-Type"); @@ -3082,6 +3097,7 @@ TinCan client library } if (typeof requestResult.xhr.contentType !== "undefined") { // most likely an XDomainRequest which has .contentType + // for the ones that it supports requestResult.profile.contentType = requestResult.xhr.contentType; } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type"); @@ -3263,6 +3279,7 @@ TinCan client library } if (typeof xhr.contentType !== "undefined") { // most likely an XDomainRequest which has .contentType + // for the ones that it supports result.contentType = xhr.contentType; } else if (typeof xhr.getResponseHeader !== "undefined" && xhr.getResponseHeader("Content-Type") !== null && xhr.getResponseHeader("Content-Type") !== "") { result.contentType = xhr.getResponseHeader("Content-Type"); @@ -3305,6 +3322,7 @@ TinCan client library } if (typeof requestResult.xhr.contentType !== "undefined") { // most likely an XDomainRequest which has .contentType + // for the ones that it supports requestResult.profile.contentType = requestResult.xhr.contentType; } else if (typeof requestResult.xhr.getResponseHeader !== "undefined" && requestResult.xhr.getResponseHeader("Content-Type") !== null && requestResult.xhr.getResponseHeader("Content-Type") !== "") { requestResult.profile.contentType = requestResult.xhr.getResponseHeader("Content-Type");