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:
+
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/build/tincan-min.js b/build/tincan-min.js
index 114b254..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.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&&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 bca4628..d84c09f 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);
+ }
}
};
@@ -1445,11 +1625,31 @@ 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;
}
};
}());
/*
- 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 +1913,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
@@ -1736,6 +1936,7 @@ TinCan client library
headers = {},
data,
requestCompleteResult,
+ syncFakeStatus,
until,
prop,
pairs = [],
@@ -1743,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
@@ -1762,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);
}
@@ -1775,19 +1985,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 +2030,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;
@@ -1905,12 +2116,37 @@ 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:
+ //
+ // 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.onprogress = function () {};
+ xhr.timeout = 0;
}
else {
this.log("sendRequest unrecognized _requestMode: " + this._requestMode);
@@ -1933,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();
}
@@ -1978,7 +2215,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 +2396,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 +2795,22 @@ TinCan client library
//
result.etag = TinCan.Utils.getSHA1String(xhr.responseText);
}
+
+ 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");
+ }
+
+ if (TinCan.Utils.isApplicationJSON(result.contentType)) {
+ try {
+ result.contents = JSON.parse(result.contents);
+ } catch (ex) {
+ this.log("retrieveState - failed to deserialize JSON: " + ex);
+ }
+ }
}
}
@@ -2580,6 +2839,20 @@ 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
+ // 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");
+ }
+ if (TinCan.Utils.isApplicationJSON(requestResult.state.contentType)) {
+ try {
+ requestResult.state.contents = JSON.parse(requestResult.state.contents);
+ } catch (ex) {
+ this.log("retrieveState - failed to deserialize JSON: " + ex);
+ }
+ }
}
}
@@ -2597,6 +2870,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 +2888,11 @@ TinCan client library
return;
}
- if (typeof val === "object") {
+ if (typeof cfg.contentType === "undefined") {
+ cfg.contentType = "application/octet-stream";
+ }
+
+ if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) {
val = JSON.stringify(val);
}
@@ -2641,15 +2919,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 +2947,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 +3052,20 @@ TinCan client library
//
result.etag = TinCan.Utils.getSHA1String(xhr.responseText);
}
+ 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");
+ }
+ if (TinCan.Utils.isApplicationJSON(result.contentType)) {
+ try {
+ result.contents = JSON.parse(result.contents);
+ } catch (ex) {
+ this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex);
+ }
+ }
}
}
@@ -2802,6 +3095,20 @@ 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
+ // 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");
+ }
+ if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) {
+ try {
+ requestResult.profile.contents = JSON.parse(requestResult.profile.contents);
+ } catch (ex) {
+ this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex);
+ }
+ }
}
}
@@ -2816,6 +3123,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 +3138,11 @@ TinCan client library
return;
}
- if (typeof val === "object") {
+ if (typeof cfg.contentType === "undefined") {
+ cfg.contentType = "application/octet-stream";
+ }
+
+ if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) {
val = JSON.stringify(val);
}
@@ -2841,20 +3153,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 +3182,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 +3195,9 @@ TinCan client library
}
requestParams = {
+ profileId: key,
activityId: cfg.activity.id
};
- if (key !== null) {
- requestParams.profileId = key;
- }
requestCfg = {
url: "activities/profile",
@@ -2902,6 +3211,242 @@ 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
+ // 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");
+ }
+ if (TinCan.Utils.isApplicationJSON(result.contentType)) {
+ 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
+ // 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");
+ }
+ if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) {
+ 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" && TinCan.Utils.isApplicationJSON(cfg.contentType)) {
+ 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 +6499,12 @@ TinCan client library
*/
this.etag = null;
+ /**
+ @property contentType
+ @type String
+ */
+ this.contentType = null;
+
this.init(cfg);
};
State.prototype = {
@@ -5976,7 +6527,9 @@ TinCan client library
var i,
directProps = [
"id",
- "contents"
+ "contents",
+ "etag",
+ "contentType"
],
val
;
@@ -6070,6 +6623,12 @@ TinCan client library
*/
this.etag = null;
+ /**
+ @property contentType
+ @type String
+ */
+ this.contentType = null;
+
this.init(cfg);
};
ActivityProfile.prototype = {
@@ -6093,7 +6652,8 @@ TinCan client library
directProps = [
"id",
"contents",
- "etag"
+ "etag",
+ "contentType"
],
val
;
@@ -6131,6 +6691,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/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/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 ed2e24f..f77e5fa 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.
@@ -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
@@ -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);
}
@@ -325,19 +335,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;
}
}
@@ -368,7 +380,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;
@@ -455,12 +466,37 @@ 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:
+ //
+ // 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.onprogress = function () {};
+ xhr.timeout = 0;
}
else {
this.log("sendRequest unrecognized _requestMode: " + this._requestMode);
@@ -483,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();
}
@@ -528,7 +565,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";
@@ -706,7 +746,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;
@@ -1102,6 +1145,22 @@ TinCan client library
//
result.etag = TinCan.Utils.getSHA1String(xhr.responseText);
}
+
+ 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");
+ }
+
+ if (TinCan.Utils.isApplicationJSON(result.contentType)) {
+ try {
+ result.contents = JSON.parse(result.contents);
+ } catch (ex) {
+ this.log("retrieveState - failed to deserialize JSON: " + ex);
+ }
+ }
}
}
@@ -1130,6 +1189,20 @@ 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
+ // 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");
+ }
+ if (TinCan.Utils.isApplicationJSON(requestResult.state.contentType)) {
+ try {
+ requestResult.state.contents = JSON.parse(requestResult.state.contents);
+ } catch (ex) {
+ this.log("retrieveState - failed to deserialize JSON: " + ex);
+ }
+ }
}
}
@@ -1147,6 +1220,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) {
@@ -1164,7 +1238,11 @@ TinCan client library
return;
}
- if (typeof val === "object") {
+ if (typeof cfg.contentType === "undefined") {
+ cfg.contentType = "application/octet-stream";
+ }
+
+ if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) {
val = JSON.stringify(val);
}
@@ -1191,15 +1269,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);
@@ -1218,8 +1297,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
@@ -1323,6 +1402,20 @@ TinCan client library
//
result.etag = TinCan.Utils.getSHA1String(xhr.responseText);
}
+ 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");
+ }
+ if (TinCan.Utils.isApplicationJSON(result.contentType)) {
+ try {
+ result.contents = JSON.parse(result.contents);
+ } catch (ex) {
+ this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex);
+ }
+ }
}
}
@@ -1352,6 +1445,20 @@ 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
+ // 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");
+ }
+ if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) {
+ try {
+ requestResult.profile.contents = JSON.parse(requestResult.profile.contents);
+ } catch (ex) {
+ this.log("retrieveActivityProfile - failed to deserialize JSON: " + ex);
+ }
+ }
}
}
@@ -1366,6 +1473,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) {
@@ -1380,7 +1488,11 @@ TinCan client library
return;
}
- if (typeof val === "object") {
+ if (typeof cfg.contentType === "undefined") {
+ cfg.contentType = "application/octet-stream";
+ }
+
+ if (typeof val === "object" && TinCan.Utils.isApplicationJSON(cfg.contentType)) {
val = JSON.stringify(val);
}
@@ -1391,20 +1503,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);
@@ -1421,8 +1532,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
@@ -1434,11 +1545,9 @@ TinCan client library
}
requestParams = {
+ profileId: key,
activityId: cfg.activity.id
};
- if (key !== null) {
- requestParams.profileId = key;
- }
requestCfg = {
url: "activities/profile",
@@ -1452,6 +1561,242 @@ 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
+ // 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");
+ }
+ if (TinCan.Utils.isApplicationJSON(result.contentType)) {
+ 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
+ // 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");
+ }
+ if (TinCan.Utils.isApplicationJSON(requestResult.profile.contentType)) {
+ 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" && TinCan.Utils.isApplicationJSON(cfg.contentType)) {
+ 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/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..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;
+ }
},
/**
@@ -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);
+ }
}
};
diff --git a/src/Utils.js b/src/Utils.js
index 29ea883..8cba5aa 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -182,6 +182,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;
}
};
}());
diff --git a/test/complete.html b/test/complete.html
index 034230e..a28ab3f 100644
--- a/test/complete.html
+++ b/test/complete.html
@@ -49,6 +49,7 @@
+
-->
@@ -60,9 +61,9 @@
-
-
+
+
@@ -78,5 +79,7 @@
+
+