@dinoboff/ims-lti
Version:
Module for building an LTI Tool Provider and accept LTI launch requests
346 lines (296 loc) • 11.4 kB
JavaScript
// Generated by CoffeeScript 1.12.7
(function() {
var HMAC_SHA1, OutcomeDocument, OutcomeService, REQUEST_DELETE, REQUEST_READ, REQUEST_REPLACE, crypto, errors, http, https, navigateXml, parseResponse, url, utils, uuid, xml2js, xml_builder;
crypto = require('crypto');
http = require('http');
https = require('https');
url = require('url');
uuid = require('node-uuid');
xml2js = require('xml2js');
xml_builder = require('xmlbuilder');
errors = require('../errors');
HMAC_SHA1 = require('../hmac-sha1');
utils = require('../utils');
REQUEST_REPLACE = 'replaceResult';
REQUEST_READ = 'readResult';
REQUEST_DELETE = 'deleteResult';
navigateXml = function(xmlObject, path) {
var i, len, part, ref, ref1;
ref = path.split('.');
for (i = 0, len = ref.length; i < len; i++) {
part = ref[i];
xmlObject = xmlObject != null ? (ref1 = xmlObject[part]) != null ? ref1[0] : void 0 : void 0;
}
return xmlObject;
};
parseResponse = function(body, callback) {
return xml2js.parseString(body, {
trim: true
}, (function(_this) {
return function(err, result) {
var code, msg, response;
if (err != null) {
callback(new errors.OutcomeResponseError('The server responsed with an invalid XML document'), false);
return;
}
response = result != null ? result.imsx_POXEnvelopeResponse : void 0;
code = navigateXml(response, 'imsx_POXHeader.imsx_POXResponseHeaderInfo.imsx_statusInfo.imsx_codeMajor');
if (code !== 'success') {
msg = navigateXml(response, 'imsx_POXHeader.imsx_POXResponseHeaderInfo.imsx_statusInfo.imsx_description');
return callback(new errors.OutcomeResponseError(msg), false);
} else {
return callback(null, true, response);
}
};
})(this));
};
OutcomeDocument = (function() {
OutcomeDocument.replace = function(source_did, payload, options) {
var doc;
doc = new OutcomeDocument(REQUEST_REPLACE, source_did, options);
if (payload == null) {
return doc;
}
if (payload.score != null) {
doc.add_score(payload.score);
}
if (payload.text != null) {
doc.add_text(payload.text);
}
if (payload.url != null) {
doc.add_url(payload.url);
}
return doc;
};
OutcomeDocument.read = function(source_did, options) {
var doc;
return doc = new OutcomeDocument(REQUEST_READ, source_did, options);
};
OutcomeDocument["delete"] = function(source_did, options) {
var doc;
return doc = new OutcomeDocument(REQUEST_DELETE, source_did, options);
};
function OutcomeDocument(type, source_did, options) {
var xmldec;
this.result_data_types = (options != null ? options.result_data_types : void 0) || true;
this.language = (options != null ? options.language : void 0) || 'en';
xmldec = {
version: '1.0',
encoding: 'UTF-8'
};
this.doc = xml_builder.create('imsx_POXEnvelopeRequest', xmldec);
this.doc.attribute('xmlns', 'http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0');
this.head = this.doc.ele('imsx_POXHeader').ele('imsx_POXRequestHeaderInfo');
this.body = this.doc.ele('imsx_POXBody').ele(type + 'Request').ele('resultRecord');
this.head.ele('imsx_version', 'V1.0');
this.head.ele('imsx_messageIdentifier', uuid.v1());
this.body.ele('sourcedGUID').ele('sourcedId', source_did);
}
OutcomeDocument.prototype.add_score = function(score) {
var eScore;
if (typeof score !== 'number' || score < 0 || score > 1.0) {
throw new errors.ParameterError('Score must be a floating point number >= 0 and <= 1');
}
eScore = this._result_ele().ele('resultScore');
eScore.ele('language', this.language);
return eScore.ele('textString', score);
};
OutcomeDocument.prototype.add_text = function(text) {
return this._add_payload('text', text);
};
OutcomeDocument.prototype.add_url = function(url) {
return this._add_payload('url', url);
};
OutcomeDocument.prototype.finalize = function() {
return this.doc.end({
pretty: true
});
};
OutcomeDocument.prototype._result_ele = function() {
return this.result || (this.result = this.body.ele('result'));
};
OutcomeDocument.prototype._add_payload = function(type, value) {
if (this.has_payload) {
throw new errors.ExtensionError('Result data payload has already been set');
}
if (!this._supports_result_data(type)) {
throw new errors.ExtensionError('Result data type is not supported');
}
this._result_ele().ele('resultData').ele(type, value);
return this.has_payload = true;
};
OutcomeDocument.prototype._supports_result_data = function(type) {
if (!this.result_data_types || this.result_data_types.length === 0) {
return false;
}
if (this.result_data_types === true) {
return true;
}
if (type == null) {
return true;
}
return this.result_data_types.indexOf(type) !== -1;
};
return OutcomeDocument;
})();
OutcomeService = (function() {
OutcomeService.prototype.REQUEST_REPLACE = REQUEST_REPLACE;
OutcomeService.prototype.REQUEST_READ = REQUEST_READ;
OutcomeService.prototype.REQUEST_DELETE = REQUEST_DELETE;
function OutcomeService(options) {
var parts;
if (options == null) {
options = {};
}
this.consumer_key = options.consumer_key;
this.consumer_secret = options.consumer_secret;
this.service_url = options.service_url;
this.source_did = options.source_did;
this.result_data_types = options.result_data_types || [];
this.signer = options.signer || (new HMAC_SHA1());
this.cert_authority = options.cert_authority || null;
this.language = options.language || 'en';
parts = this.service_url_parts = url.parse(this.service_url, true);
this.service_url_oauth = parts.protocol + '//' + parts.host + parts.pathname;
}
OutcomeService.prototype.send_replace_result = function(score, callback) {
return this._send_replace_result({
score: score
}, callback);
};
OutcomeService.prototype.send_replace_result_with_text = function(score, text, callback) {
return this._send_replace_result({
score: score,
text: text
}, callback);
};
OutcomeService.prototype.send_replace_result_with_url = function(score, url, callback) {
return this._send_replace_result({
score: score,
url: url
}, callback);
};
OutcomeService.prototype._send_replace_result = function(payload, callback) {
var doc, err;
try {
doc = OutcomeDocument.replace(this.source_did, payload, this);
return this._send_request(doc, callback);
} catch (error) {
err = error;
return callback(err, false);
}
};
OutcomeService.prototype.send_read_result = function(callback) {
var doc;
doc = OutcomeDocument.read(this.source_did, this);
return this._send_request(doc, (function(_this) {
return function(err, result, xml) {
var score;
if (err) {
return callback(err, result);
}
score = parseFloat(navigateXml(xml, 'imsx_POXBody.readResultResponse.result.resultScore.textString'), 10);
if (isNaN(score)) {
return callback(new errors.OutcomeResponseError('Invalid score response'), false);
} else {
return callback(null, score);
}
};
})(this));
};
OutcomeService.prototype.send_delete_result = function(callback) {
var doc;
doc = OutcomeDocument["delete"](this.source_did, this);
return this._send_request(doc, callback);
};
OutcomeService.prototype.supports_result_data = function(type) {
return this.result_data_types.length && (!type || this.result_data_types.indexOf(type) !== -1);
};
OutcomeService.prototype._send_request = function(doc, callback) {
var body, is_ssl, options, req, xml;
xml = doc.finalize();
body = '';
is_ssl = this.service_url_parts.protocol === 'https:';
options = {
hostname: this.service_url_parts.hostname,
path: this.service_url_parts.path,
method: 'POST',
headers: this._build_headers(xml)
};
if (this.cert_authority && is_ssl) {
options.ca = this.cert_authority;
} else {
options.agent = is_ssl ? https.globalAgent : http.globalAgent;
}
if (this.service_url_parts.port) {
options.port = this.service_url_parts.port;
}
req = (is_ssl ? https : http).request(options, (function(_this) {
return function(res) {
res.setEncoding('utf8');
res.on('data', function(chunk) {
return body += chunk;
});
return res.on('end', function() {
return _this._process_response(body, callback);
});
};
})(this));
req.on('error', (function(_this) {
return function(err) {
return callback(err, false);
};
})(this));
req.write(xml);
return req.end();
};
OutcomeService.prototype._build_headers = function(body) {
var headers, key, val;
headers = {
oauth_version: '1.0',
oauth_nonce: uuid.v4(),
oauth_timestamp: Math.round(Date.now() / 1000),
oauth_consumer_key: this.consumer_key,
oauth_body_hash: crypto.createHash('sha1').update(body).digest('base64'),
oauth_signature_method: 'HMAC-SHA1'
};
headers.oauth_signature = this.signer.build_signature_raw(this.service_url_oauth, this.service_url_parts, 'POST', headers, this.consumer_secret);
return {
Authorization: 'OAuth realm="",' + ((function() {
var results;
results = [];
for (key in headers) {
val = headers[key];
results.push(key + "=\"" + (utils.special_encode(val)) + "\"");
}
return results;
})()).join(','),
'Content-Type': 'application/xml',
'Content-Length': body.length
};
};
OutcomeService.prototype._process_response = function(body, callback) {
return parseResponse(body, callback);
};
return OutcomeService;
})();
exports.init = function(provider) {
var accepted_vals;
if (provider.body.lis_outcome_service_url && provider.body.lis_result_sourcedid) {
accepted_vals = provider.body.ext_outcome_data_values_accepted;
return provider.outcome_service = new OutcomeService({
consumer_key: provider.consumer_key,
consumer_secret: provider.consumer_secret,
service_url: provider.body.lis_outcome_service_url,
source_did: provider.body.lis_result_sourcedid,
result_data_types: accepted_vals && accepted_vals.split(',') || [],
signer: provider.signer
});
} else {
return provider.outcome_service = false;
}
};
exports.OutcomeDocument = OutcomeDocument;
exports.OutcomeService = OutcomeService;
exports.parseResponse = parseResponse;
}).call(this);