Skip to content

Commit

Permalink
Merge pull request #82 from adlnet/attachments
Browse files Browse the repository at this point in the history
Attachments
  • Loading branch information
ljwolford authored Feb 27, 2017
2 parents f21315f + 1e490e6 commit 8477dcf
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 15 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ var stmt = ADL.XAPIStatement(myactor, ADL.verbs.launched, myactivity);
```

##### Send Statement
`function sendStatement(statement, callback)`
`function sendStatement(statement, callback, [attachments])`
Sends a single Statement to the LRS using a PUT request. This
method will automatically create the Statement ID. Providing a
function to call after the send Statement request will make
Expand Down Expand Up @@ -407,6 +407,29 @@ ADL.XAPIWrapper.sendStatement(stmt, function(resp, obj){
>> [4edfe763-8b84-41f1-a355-78b7601a6fe8]: 200 - OK
```

###### Send Statement with Attachments
The wrapper can construct a `multipart/mixed` POST for a single statement that includes attachments. Attachments should be
supplied as an array in the 3rd parameter to `sendStatement`. Attachments are optional. The attachments array should consist of
objects that have a `type` and a `value` field. `Type` should be the metadata description of the attachment as described by the spec, and `value`
should be a string containing the data to post. The type field does not need to include the SHA2 or the length. These will be computed
for you. The type may optionally be the string 'signature'. In this case, the wrapper will construct the proper metadata block.

```JavaScript
var attachment = {};
attachment.type = {
"usageType": "http://adlnet.gov/expapi/attachments/signature",
"display": {
"en-US": "A JWT signature"
},
"description": {
"en-US": "A signature proving the statement was not modified"
},
"contentType": "application/octet-stream"
};
attachment.value = "somehugestring";
ADL.XAPIWrapper.sendStatement(stmt,callback,[attachment]);
```

###### Send Statement with URL query string values
The wrapper looks for URL query string values to include in
its internal configuration. If certain keys
Expand Down
5 changes: 3 additions & 2 deletions dist/xapiwrapper.min.js

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions lib/cryptojs_v3.1.2.js
Original file line number Diff line number Diff line change
Expand Up @@ -927,3 +927,25 @@ var CryptoJS = CryptoJS || (function (Math, undefined) {

return C;
}(Math));


//add the sha256 functions

/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(h,s){var f={},g=f.lib={},q=function(){},m=g.Base={extend:function(a){q.prototype=this;var c=new q;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
r=g.WordArray=m.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||k).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e<a;e++)c[b+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)c[b+e>>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d<a;d+=4)c.push(4294967296*h.random()|0);return new r.init(c,a)}}),l=f.enc={},k=l.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++){var e=c[b>>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b+=2)d[b>>>3]|=parseInt(a.substr(b,
2),16)<<24-4*(b%8);return new r.init(d,c/2)}},n=l.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++)d.push(String.fromCharCode(c[b>>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b++)d[b>>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new r.init(d,c)}},j=l.Utf8={stringify:function(a){try{return decodeURIComponent(escape(n.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return n.parse(unescape(encodeURIComponent(a)))}},
u=g.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var g=0;g<a;g+=e)this._doProcessBlock(d,g);g=d.splice(0,a);c.sigBytes-=b}return new r.init(g,b)},clone:function(){var a=m.clone.call(this);
a._data=this._data.clone();return a},_minBufferSize:0});g.Hasher=u.extend({cfg:m.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){u.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(c,d){return(new a.init(d)).finalize(c)}},_createHmacHelper:function(a){return function(c,d){return(new t.HMAC.init(a,
d)).finalize(c)}}});var t=f.algo={};return f}(Math);
(function(h){for(var s=CryptoJS,f=s.lib,g=f.WordArray,q=f.Hasher,f=s.algo,m=[],r=[],l=function(a){return 4294967296*(a-(a|0))|0},k=2,n=0;64>n;){var j;a:{j=k;for(var u=h.sqrt(j),t=2;t<=u;t++)if(!(j%t)){j=!1;break a}j=!0}j&&(8>n&&(m[n]=l(h.pow(k,0.5))),r[n]=l(h.pow(k,1/3)),n++);k++}var a=[],f=f.SHA256=q.extend({_doReset:function(){this._hash=new g.init(m.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],g=b[2],j=b[3],h=b[4],m=b[5],n=b[6],q=b[7],p=0;64>p;p++){if(16>p)a[p]=
c[d+p]|0;else{var k=a[p-15],l=a[p-2];a[p]=((k<<25|k>>>7)^(k<<14|k>>>18)^k>>>3)+a[p-7]+((l<<15|l>>>17)^(l<<13|l>>>19)^l>>>10)+a[p-16]}k=q+((h<<26|h>>>6)^(h<<21|h>>>11)^(h<<7|h>>>25))+(h&m^~h&n)+r[p]+a[p];l=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&g^f&g);q=n;n=m;m=h;h=j+k|0;j=g;g=f;f=e;e=k+l|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+g|0;b[3]=b[3]+j|0;b[4]=b[4]+h|0;b[5]=b[5]+m|0;b[6]=b[6]+n|0;b[7]=b[7]+q|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes;
d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=q.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=q._createHelper(f);s.HmacSHA256=q._createHmacHelper(f)})(Math);
(function(){var h=CryptoJS,s=h.enc.Utf8;h.algo.HMAC=h.lib.Base.extend({init:function(f,g){f=this._hasher=new f.init;"string"==typeof g&&(g=s.parse(g));var h=f.blockSize,m=4*h;g.sigBytes>m&&(g=f.finalize(g));g.clamp();for(var r=this._oKey=g.clone(),l=this._iKey=g.clone(),k=r.words,n=l.words,j=0;j<h;j++)k[j]^=1549556828,n[j]^=909522486;r.sigBytes=l.sigBytes=m;this.reset()},reset:function(){var f=this._hasher;f.reset();f.update(this._iKey)},update:function(f){this._hasher.update(f);return this},finalize:function(f){var g=
this._hasher;f=g.finalize(f);g.reset();return g.finalize(this._oKey.clone().concat(f))}})})();
27 changes: 17 additions & 10 deletions src/xapi-launch.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,25 @@ function xAPILaunch(cb, terminate_on_unload)
var launchToken = getQueryVariable("xAPILaunchKey");
var launchEndpoint = getQueryVariable("xAPILaunchService");
var encrypted = getQueryVariable("encrypted");
if (!encrypted)
if (encrypted)
{
//here, we'd have to implement decryption for the data. This makes little sense in a client side only course
}

xAPILaunch.terminate = function(message)
{
var launch = new URL(launchEndpoint);
launch.pathname += "launch/" + launchToken + "/terminate";
var xhr2 = new XMLHttpRequest();
xhr2.withCredentials = true;
xhr2.crossDomain = true;

xhr2.open('POST', launch.toString(), false);
xhr2.setRequestHeader("Content-type" , "application/json");
xhr2.send(JSON.stringify({"code":0,"description": message ||"User closed content"}));

}

if (!launchToken || !launchEndpoint)
return cb("invalid launch parameters");
var launch = new URL(launchEndpoint);
Expand Down Expand Up @@ -138,15 +153,7 @@ function xAPILaunch(cb, terminate_on_unload)
{
if (!terminate_on_unload)
return;
var launch = new URL(launchEndpoint);
launch.pathname += "launch/" + launchToken + "/terminate";
var xhr2 = new XMLHttpRequest();
xhr2.withCredentials = true;
xhr2.crossDomain = true;

xhr2.open('POST', launch.toString(), false);
xhr2.setRequestHeader("Content-type" , "application/json");
xhr2.send('{"code":0,"description":"User closed content"}');
xAPILaunch.terminate("User closed content")
}
var wrapper = new ADL.XAPIWrapper.constructor();
wrapper.changeConfig(conf);
Expand Down
100 changes: 98 additions & 2 deletions src/xapiwrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ function toSHA1(text){
else
return Crypto.util.bytesToHex( Crypto.SHA1(text,{asBytes:true}) );
}
function toSHA256(text){
if(CryptoJS && CryptoJS.SHA256)
return CryptoJS.SHA256(text).toString();

}

// check if string or object is date, if it is, return date object
// feburary 31st == march 3rd in this solution
Expand Down Expand Up @@ -69,6 +74,24 @@ function isDate(date) {

(function(ADL){
log.debug = false;

function getByteLen(normal_val) {
// Force string type
normal_val = String(normal_val);

var byteLen = 0;
for (var i = 0; i < normal_val.length; i++) {
var c = normal_val.charCodeAt(i);
byteLen += c < (1 << 7) ? 1 :
c < (1 << 11) ? 2 :
c < (1 << 16) ? 3 :
c < (1 << 21) ? 4 :
c < (1 << 26) ? 5 :
c < (1 << 31) ? 6 : Number.NaN;
}
return byteLen;
}

/*
* Config object used w/ url params to configure the lrs object
* change these to match your lrs
Expand Down Expand Up @@ -108,6 +131,9 @@ function isDate(date) {
*/
XAPIWrapper = function(config, verifyxapiversion)
{



this.lrs = getLRSObject(config || {});
if (this.lrs.user && this.lrs.password)
updateAuth(this.lrs, this.lrs.user, this.lrs.password);
Expand Down Expand Up @@ -278,7 +304,7 @@ function isDate(date) {
* ADL.XAPIWrapper.log("[" + obj.id + "]: " + resp.status + " - " + resp.statusText);});
* >> [4edfe763-8b84-41f1-a355-78b7601a6fe8]: 204 - NO CONTENT
*/
XAPIWrapper.prototype.sendStatement = function(stmt, callback)
XAPIWrapper.prototype.sendStatement = function(stmt, callback, attachments)
{
if (this.testConfig())
{
Expand All @@ -293,14 +319,84 @@ function isDate(date) {
id = ADL.ruuid();
stmt['id'] = id;
}

var payload = JSON.stringify(stmt);
var extraHeaders = null;
if(attachments && attachments.length > 0)
{
extraHeaders = {}
payload = this.buildMultipartPost(stmt,attachments,extraHeaders)
}
var resp = ADL.XHR_request(this.lrs, this.lrs.endpoint+"statements",
"POST", JSON.stringify(stmt), this.lrs.auth, callback, {"id":id},null,false,this.withCredentials);
"POST", payload, this.lrs.auth, callback, {"id":id},null,extraHeaders,this.withCredentials);
if (!callback)
return {"xhr":resp,
"id" :id};
}
};
/*
* Build the post body to include the multipart boundries, edit the statement to include the attachment types
* extraHeaders should be an object. It will have the multipart boundary value set
* attachments should be an array of objects of the type
* {
type:"signature" || {
usageType : URI,
display: Language-map
description: Language-map
},
value : a UTF8 string containing the binary data of the attachment. For string values, this can just be the JS string.
}
*/
XAPIWrapper.prototype.buildMultipartPost = function(statement,attachments,extraHeaders)
{
statement.attachments = [];
for(var i =0; i < attachments.length; i++)
{
//replace the term 'signature' with the hard coded definition for a signature attachment
if(attachments[i].type == "signature")
{
attachments[i].type = {
"usageType": "http://adlnet.gov/expapi/attachments/signature",
"display": {
"en-US": "A JWT signature"
},
"description": {
"en-US": "A signature proving the statement was not modified"
},
"contentType": "application/octet-stream"
}
}

//compute the length and the sha2 of the attachment
attachments[i].type.length = attachments[i].value.length;
attachments[i].type.sha2 = toSHA256(attachments[i].value);

//attach the attachment metadata to the statement
statement.attachments.push(attachments[i].type)
}

var body = "";
var CRLF = "\r\n";
var boundary = (Math.random()+' ').substring(2,10)+(Math.random()+' ').substring(2,10);

extraHeaders["Content-Type"] = "multipart/mixed; boundary=" + boundary;

body += CRLF + '--' + boundary + CRLF + 'Content-Type:application/json' + CRLF + "Content-Disposition: form-data; name=\"statement\"" + CRLF + CRLF;
body += JSON.stringify(statement);

for(var i in attachments)
{

body += CRLF + '--' + boundary + CRLF + 'X-Experience-API-Hash:' + attachments[i].type.sha2 + CRLF + "Content-Type:application/octet-stream" + CRLF + "Content-Transfer-Encoding: binary" + CRLF + CRLF
body += attachments[i].value;
}
body += CRLF + "--" + boundary + "--" + CRLF




return body;
}
/*
* Send a list of statements to the LRS.
* @param {array} stmtArray the list of statement objects to send
Expand Down
60 changes: 60 additions & 0 deletions test/testAttachments.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<html>

<head>
<meta charset="utf-8">
<title>Mocha Tests</title>
<link href="./libs/mocha/2.2.5/mocha.css" rel="stylesheet" />
<script src="./libs/jquery/2.1.4/jquery.min.js"></script>
<script>
localStorage.clear();
</script>
</head>

<body>


<!-- <script src="../dist/xapiwrapper.min.js"></script>-->
<script type="text/javascript" src="../lib/cryptojs_v3.1.2.js"></script>
<script type="text/javascript" src="../src/verbs.js"></script>
<script type="text/javascript" src="../src/xapistatement.js"></script>
<script type="text/javascript" src="../src/xapiwrapper.js"></script>

<script type="text/javascript" src="../examples/stmtBank.js"></script>

<script src="../src/xapi-util.js" data-cover></script>

<script>
var conf = {
"endpoint" : "https://lrs.adlnet.gov/xapi/",
"user" : "tom",
"password" : "1234",
};
ADL.XAPIWrapper.changeConfig(conf);

var statement = {"actor":{"mbox":"mailto:tom@tom.com"}, "verb":{"id":"http://verb.com/do1"}, "object":{"id":"http://from.tom/act1", "objectType":"Activity", "definition":{"name":{"en-US": "soccer", "fr": "football", "de": "foossball"}}}};


var attachmentMetadata = {
"usageType": "http://adlnet.gov/expapi/attachments/asdf",
"display":
{
"en-US": "asdfasdf"
},
"description":
{
"en-US": "asdfasdfasd"
},
"contentType": "application/octet-stream"
}
var attachment = {
value: "this is a simple string attachment",
type:attachmentMetadata
}


ADL.XAPIWrapper.sendStatement(statement,null,[attachment]);

</script>
</body>

</html>

0 comments on commit 8477dcf

Please sign in to comment.