src/core/Tsugi.js
var Q = require("q");
var TsugiUtils = require("../util/TsugiUtils");
var Crypto = require("../util/Crypto");
/**
* The Tsugi class/namespace/Utilities
*
* Calling sequence in a NodeJS app:
*
* var CFG = require('./src/config/Config');
* var Tsugi = require('./src/core/Tsugi');
*
* launch = Tsugi.requireData(CFG, req, res);
* if ( launch.complete ) return;
*/
class Tsugi {
// http://stackoverflow.com/questions/32647215/declaring-static-constants-in-es6-classes
constructor() {
/**
*
*/
this.CONTEXT = "context_id";
this.USER = "user_id";
this.LINK = "link_id";
this.ALL = "all";
this.NONE = "none";
/*
* Way to signal to the class that it is being exercised in a unit test.
*/
this.unit_testing = false;
}
/**
* Optionally handle launch and/or set up the LTI session variables
*
* This will set up as much of the user, context, link,
* and result data as it can including leaving them all null
* if this is called on a request with no LTI launch and no LTI
* data in the session. This expects req.session to be properly
* set up as it may read and / or write session data. If this
* encounters a LTI launch (POST) it might redirect and indicate
* that this request is now complete.
*
* Calling sequence:
*
* launch = tsugi.setup(CFG, req, res, session);
* if (launch.complete) return;
*
* @param {ConfigSample} CFG A Tsugi Configuration object
* @param {http.ClientRequest} req
* @param {http.ServerResponse} res
* @param {*} A session object
* @param {*} needed Indicates which of
* the data structures are needed. If this is omitted,
* this assumes that CONTEXT, LINK, and USER data are required.
* If NONE is present, then none of the three are rquired.
* If some combination of the three are needed, this accepts
* an array of the CONTEXT, LINK, and USER
* can be passed in.
*
* @return Launch A Tsugi Launch object.
*/
setup(CFG, req, res, session) {
return this.requireData(CFG, this.NONE);
}
/**
* Handle launch and/or set up the LTI session and global variables
*
* Make sure we have the values we need in the LTI session
* This routine will not start a session if none exists. It will
* die is there if no session_name() (PHPSESSID) cookie or
* parameter. No need to create any fresh sessions here.
*
* @param {ConfigSample} CFG A Tsugi Configuration object
* @param {http.ClientRequest} req
* @param {http.ServerResponse} res
* @param {*} A session object
* @param {*} The body (i.e. POST data)
* @param {*} needed Indicates which of
* the data structures are needed. If this is omitted,
* this assumes that CONTEXT, LINK, and USER data are required.
* If NONE is present, then none of the three are rquired.
* If some combination of the three are needed, this accepts
* an array of the CONTEXT, LINK, and USER
* can be passed in.
*
* @return Launch A Tsugi Launch object.
*/
requireData(CFG, req, res, body=null, session=null, needed=this.ALL) {
let reqData = Q.defer();
if ( ! ( needed instanceof Array ) ) {
needed = [ needed ];
}
console.log(needed);
let ns = new Set(needed);
console.log(ns.has(this.ALL));
body = body || req.body || req.payload;
session = session || req.session ;
let Launch = require('./Launch.js');
/**
* @type {Launch}
*/
let launch = new Launch(CFG, req, res, session);
if (!this.isRequest(body)) {
if (session == null) {
reqData.reject ('This tool must be launched using LTI')
} else {
let sess_row = session.lti_row;
if (sess_row == null) {
reqData.reject ('This tool must be launched using LTI or have LTI data in session');
} else {
launch.success = true;
launch.fill(sess_row);
reqData.resolve(launch);
}
}
} else { //This already is a valid request
// Start fresh in the session
if ( session != null ) delete session.lti_row;
// Pull in the POST data
let post = this.extractPost(body, needed);
if ( post == null ) {
reqData.reject('Missing essential POST data');
} else {
// Pull in whatever old data we have (including secret)
let topclass = this;
this.loadAllData(CFG, post).then(function(rows) {
console.log("Data Rows: ", rows.length);
if ( rows.length < 1 ) {
console.log("Key not found "+post.key_key);
reqData.reject ('Key not found');
} else {
let row = rows[0];
//console.log('ROW',row);
let x = false;
let validated = false;
if ( req != null ) {
let key = row.key_key;
let secret = row.secret;
let new_secret = row.new_secret;
console.log("OAuth",key,secret,new_secret);
// checkOAuthSignature Returns three item array:
// [0] An error with member ".message" containing a textual message
// [1] true/false if it was validated
// [2] The base string or null
if ( new_secret != null ) {
x = topclass.checkOAuthSignature(launch, key, new_secret);
validated = x[1];
}
if ( !validated && secret != null ) {
x = topclass.checkOAuthSignature(launch, key, secret);
validated = x[1];
}
if ( !validated ) {
launch.message = x[0].message;
launch.error = true;
launch.base = x[2];
console.log("OAuth error: "+launch.message);
console.log("Base string: "+launch.base);
let returnUrl = req.body.launch_presentation_return_url
if ( returnUrl ) {
if ( returnUrl.indexOf('?') > 0 ) {
returnUrl += '&';
} else {
returnUrl += '?';
}
returnUrl += 'lti_errormsg=' + encodeURIComponent(x[0].message);
returnUrl += '&base=' + encodeURIComponent(x.base);
console.log(returnUrl);
res.redirect(returnUrl);
launch.complete = true;
//Although is an error lets return a valid result to be processd as complete response (redirect)
reqData.resolve (launch);
}
}
} else if ( this.unit_testing ) {
console.log("HttpServletRequest is null - test only");
} else {
throw new Error("HttpServletRequest is required unless Tsugi.unit_testing = true");
}
launch.success = true;
let adjust = topclass.adjustData(CFG, row, post);
adjust.then( function () {
launch.fill(row);
if ( session != null ) {
session.lti_row = row;
let redirect = TsugiUtils.requestUrl(req);
console.log("Redirecting back to "+redirect);
res.redirect(redirect);
launch.complete = true;
reqData.resolve (launch);
} else {
reqdata.reject('No session found');
}
}).catch (function (adjustError){
reqData.reject (adjustError);
});
}
})
.catch (function (error){ //loadData went wrong
reqData.reject (error);
});
}
}
return reqData.promise;
}
/**
* Extract the data from POST
*
* @param {object} i The input post data
* @param {*} needed Indicates which of
* the data structures are needed. If this is omitted,
* this assumes that CONTEXT, LINK, and USER data are required.
* If NONE is present, then none of the three are rquired.
* If some combination of the three are needed, this accepts
* an array of the CONTEXT, LINK, and USER
* can be passed in.
* @type {object}
*/
extractPost(i, needed=this.ALL) {
let o = {};
TsugiUtils.copy(o,"key_key",i,"oauth_consumer_key");
TsugiUtils.copy(o,"nonce",i,"oauth_nonce");
if ( o.key_key == null || o.nonce == null ) return null;
TsugiUtils.copy(o,"link_key",i,"resource_link_id");
TsugiUtils.copy(o,"context_key",i,"context_id", "custom_courseoffering_sourcedid");
TsugiUtils.copy(o,"user_key",i,"user_id","custom_person_sourcedid");
// Test for the required parameters.
let ns = this.patchNeeded(needed);
if ( o.user_key == null && (ns.has(this.ALL) || ns.has(this.USER)) ) return null;
if ( o.context_key == null && (ns.has(this.ALL) || ns.has(this.CONTEXT)) ) return null;
if ( o.link_key == null && (ns.has(this.ALL) || ns.has(this.LIN)) ) return null;
// LTI 1.x settings and Outcomes
TsugiUtils.copy(o,"service",i,"lis_outcome_service_url");
TsugiUtils.copy(o,"sourcedid",i,"lis_result_sourcedid");
// LTI 2.x settings and Outcomes
TsugiUtils.copy(o,"result_url",i,"custom_result_url");
TsugiUtils.copy(o,"link_settings_url",i,"custom_link_settings_url");
TsugiUtils.copy(o,"context_settings_url",i,"custom_context_settings_url");
// LTI 1.x / 2.x Service endpoints
TsugiUtils.copy(o,"ext_memberships_id",i,"ext_memberships_id");
TsugiUtils.copy(o,"ext_memberships_url",i,"ext_memberships_url");
TsugiUtils.copy(o,"lineitems_url",i,"lineitems_url","custom_lineitems_url");
TsugiUtils.copy(o,"memberships_url",i,"memberships_url", "custom_memberships_url");
TsugiUtils.copy(o,"context_title",i,"context_title");
TsugiUtils.copy(o,"link_title",i,"resource_link_title");
// Getting email from LTI 1.x and LTI 2.x
let email = TsugiUtils.toNull(i["lis_person_contact_email_primary"]);
if ( email == null ) email = TsugiUtils.toNull(i["custom_person_email_primary"]);
if ( email == null ) email = TsugiUtils.toNull(i["custom_person_contact_email_primary"]);
if ( email != null ) o["user_email"] = email;
TsugiUtils.copy(o,"user_image",i,"user_image", "custom_user_image");
// Displayname from LTI 2.x
if ( i["person_name_full"] != null ) {
TsugiUtils.copy(o,"user_displayname",i,"custom_person_name_full");
} else if ( i["custom_person_name_given"] != null && i["custom_person_name_family"] != null ) {
o["user_displayname"] = i["custom_person_name_given"]+" "+i["custom_person_name_family"];
} else if ( i["custom_person_name_given"] != null ) {
TsugiUtils.copy(o,"user_displayname",i,"custom_person_name_given");
} else if ( i["custom_person_name_family"] != null ) {
TsugiUtils.copy(o,"user_displayname",i,"custom_person_name_family");
// Displayname from LTI 1.x
} else if ( i["lis_person_name_full"] != null ) {
TsugiUtils.copy(o,"user_displayname",i,"lis_person_name_full");
} else if ( i["lis_person_name_given"] != null && i["lis_person_name_family"] != null ) {
o["user_displayname"] = i["lis_person_name_given"]+" "+i["lis_person_name_family"];
} else if ( i["lis_person_name_given"] != null ) {
TsugiUtils.copy(o,"user_displayname",i,"lis_person_name_given");
} else if ( i["lis_person_name_family"] != null ) {
TsugiUtils.copy(o,"user_displayname",i,"lis_person_name_family");
}
let LEARNER_ROLE = "0";
let INSTRUCTOR_ROLE = "1000";
let TENANT_ADMIN_ROLE = "5000";
let ROOT_ADMIN_ROLE = "10000";
// Get the role
o["role"] = LEARNER_ROLE;
let roles = "";
if ( i["custom_membership_role"] != null ) { // From LTI 2.x
roles = i["custom_membership_role"];
} else if ( i["roles"] != null ) { // From LTI 1.x
roles = i["roles"];
}
if ( roles.length > 0 ) {
roles = roles.toLowerCase();
if ( roles.indexOf("instructor") >= 0 ) o["role"] = INSTRUCTOR_ROLE;
if ( roles.indexOf("administrator") >= 0 ) o["role"] = TENANT_ADMIN_ROLE;
}
return o;
}
// TODO: Make sure to do nonce cleanup
/**
* Load the data from our lti_ tables using one long LEFT JOIN
*
* This data may or may not exist - hence the use of the long
* LEFT JOIN.
*
* @param {Config} CFG A Tsugi Configuration object
* @param {object} post The post data
*/
loadAllData(CFG, post)
{
let sql =
`SELECT k.key_id, k.key_key, k.secret, k.new_secret, c.settings_url AS key_settings_url,
n.nonce,
c.context_id, c.title AS context_title, context_sha256, c.settings AS context_settings,
c.settings_url AS context_settings_url,
c.ext_memberships_id AS ext_memberships_id, c.ext_memberships_url AS ext_memberships_url,
c.lineitems_url AS lineitems_url, c.memberships_url AS memberships_url,
l.link_id, l.title AS link_title, l.settings AS link_settings, l.settings_url AS link_settings_url,
u.user_id, u.displayname AS user_displayname, u.email AS user_email, user_key, u.image AS user_image,
u.subscribe AS subscribe, u.user_sha256 AS user_sha256,
m.membership_id, m.role, m.role_override,
r.result_id, r.grade, r.note AS result_comment, r.result_url, r.sourcedid
`;
if ( post["service"] != null ) {
sql += `, s.service_id, s.service_key AS service
`;
}
sql +=
`FROM {p}lti_key AS k
LEFT JOIN {p}lti_nonce AS n ON k.key_id = n.key_id AND n.nonce = :nonce
LEFT JOIN {p}lti_context AS c ON k.key_id = c.key_id AND c.context_sha256 = :context
LEFT JOIN {p}lti_link AS l ON c.context_id = l.context_id AND l.link_sha256 = :link
LEFT JOIN {p}lti_user AS u ON k.key_id = u.key_id AND u.user_sha256 = :user
LEFT JOIN {p}lti_membership AS m ON u.user_id = m.user_id AND c.context_id = m.context_id
LEFT JOIN {p}lti_result AS r ON u.user_id = r.user_id AND l.link_id = r.link_id
`; // :nonce 1 :context 2 :link 3 :user 4
// Note that extractPost() insures these fields all exist in post
let data = {nonce: post.nonce,
context: Crypto.sha256(post.context_key),
link: Crypto.sha256(post.link_key),
user: Crypto.sha256(post.user_key),
key: Crypto.sha256(post.key_key) };
if ( post["service"] != null ) {
sql += `LEFT JOIN {p}lti_service AS s ON k.key_id = s.key_id AND s.service_sha256 = :service
`; // :service 5
data.service = Crypto.sha256(post.service);
}
sql += `WHERE k.key_sha256 = :key
`; // :key 6 or 5
// Handle deleted
sql += `AND COALESCE(k.deleted,0) = 0
AND COALESCE(c.deleted,0) = 0
AND COALESCE(l.deleted,0) = 0
AND COALESCE(u.deleted,0) = 0
AND COALESCE(m.deleted,0) = 0
AND COALESCE(r.deleted,0) = 0
`;
if ( post["service"] != null ) {
sql += `
AND COALESCE(s.deleted,0) = 0
`;
}
sql += ` LIMIT 1`;
// console.log(sql);
// console.log(data);
// Return a promise of a query.
return CFG.pdox.allRowsDie(sql, data);
}
/**
* Make sure that the data in our lti_ tables matches the POST data
*
* This routine compares the POST data with the data pulled from the
* lti_ tables and goes through carefully INSERTing or UPDATING
* all the nexessary data in the lti_ tables to make sure that
* the lti_ table correctly match all the data from the incoming post.
*
* While this looks like a lot of INSERT and UPDATE statements,
* the INSERT statements only run when we see a new user/course/link
* for the first time and after that, we only update is something
* changes. So in a high percentage of launches we are not seeing
* any new or updated data and so this code just falls through and
* does absolutely no SQL.
*
* @param {Config} CFG A Tsugi Configuration object
* @param {object} row The row data (from loadAllData)
* @param {object} post The post data
*/
adjustData(CFG, row, post)
{
// console.log("adjustData starting");
// console.log(row);
// console.log(post);
// Here we go with some forced straight line async code NodeJS Style
let actions = [];
let oldserviceid = row.service_id;
function contextHandling () {
console.log("CONTEXT HANDLING");
if ( row.context_id == null) {
let sql = `INSERT INTO {p}lti_context
( context_key, context_sha256, settings_url, title, key_id, created_at, updated_at ) VALUES
( :context_key, :context_sha256, :settings_url, :title, :key_id, NOW(), NOW() )`;
let data = {
context_key: post.context_key,
context_sha256: Crypto.sha256(post.context_key),
settings_url: post.context_settings_url,
title: post.context_title,
key_id: row.key_id
};
return CFG.pdox.insertKey(sql, data).then( function(insertId) {
row.context_id = insertId;
row.context_title = post.context_title;
row.context_settings_url = post.context_settings_url;
actions.push("=== Inserted context id="+row.context_id+" "+row.context_title);
});
}
};
function linkHandling () {
console.log("LINK HANDLING");
if ( row.link_id == null) {
let sql = `INSERT INTO {p}lti_link
( link_key, link_sha256, settings_url, title, context_id, created_at, updated_at ) VALUES
( :link_key, :link_sha256, :settings_url, :title, :context_id, NOW(), NOW() )`;
let data = {
link_key: post.link_key,
link_sha256: Crypto.sha256(post.link_key),
settings_url: post.link_settings_url,
title: post.link_title,
context_id: row.context_id,
key_id: row.key_id
};
return CFG.pdox.insertKey(sql, data).then( function(insertId) {
row.link_id = insertId;
row.link_title = post.link_title;
row.link_settings_url = post.link_settings_url;
actions.push("=== Inserted link id="+row.link_id+" "+row.link_title);
});
}
};
function userHandling () {
console.log("USER HANDLING");
if ( row.user_id == null) {
let sql = `INSERT INTO {p}lti_user
( user_key, user_sha256, displayname, email, key_id, created_at, updated_at ) VALUES
( :user_key, :user_sha256, :displayname, :email, :key_id, NOW(), NOW() )`;
let data = {
user_key: post.user_key,
user_sha256: Crypto.sha256(post.user_key),
displayname: post.user_displayname,
email: post.user_email,
key_id: row.key_id
};
return CFG.pdox.insertKey(sql, data).then( function(insertId) {
row.user_id = insertId;
row.user_displayname = post.user_displayname;
row.email = post.email;
actions.push("=== Inserted user id="+row.user_id+" "+row.user_displayname);
});
}
};
function membershipHandling () {
console.log("MEMBERSHIP HANDLING");
if ( row.membership_id == null && row.context_id != null && row.user_id != null ) {
let sql = `INSERT INTO {p}lti_membership
( context_id, user_id, role, created_at, updated_at ) VALUES
( :context_id, :user_id, :role, NOW(), NOW() )`;
let data = {
context_id: row.context_id,
user_id: row.user_id,
role: post.role
};
return CFG.pdox.insertKey(sql, data).then( function(insertId) {
row.membership_id = insertId;
row.role = post.role;
actions.push("=== Inserted membership id="+row.membership_id+" role="+row.role+
" user="+row.user_id+" context="+row.context_id);
});
}
};
function serviceHandling (){
// We need to handle the case where the service URL changes but we already have a sourcedid
// This is for LTI 1.x only as service is not used for LTI 2.x
console.log("SERVICE HANDLING");
if ( TsugiUtils.isset(post.service)) {
if ( row.service_id === null && post.service ) {
let sql = `INSERT INTO {p}lti_service
( service_key, service_sha256, key_id, created_at, updated_at ) VALUES
( :service_key, :service_sha256, :key_id, NOW(), NOW() )`;
let data = {
service_key: post.service,
service_sha256: Crypto.sha256(post.service),
key_id: row.key_id
};
return CFG.pdox.insertKey(sql, data).then( function(insertId) {
row.service_id = insertId;
row.service = post.service;
actions.push( "=== Inserted service id="+row.service_id+" "+post.service);
});
}
}
};
function resultToServiceHandling () {
console.log("RESULT TO SERVICE HANDLING");
if ( TsugiUtils.isset(post.service)) {
if ( oldserviceid === null && row.result_id !== null && row.service_id !== null && post.service ) {
let sql = "UPDATE {p}lti_result SET service_id = :service_id WHERE result_id = :result_id";
let data = {
service_id: row.service_id,
result_id: row.result_id
};
return CFG.pdox.query(sql, data).then( function() {
actions.push( "=== Updated result id="+row.result_id+" service="+row.service_id);
});
}
}
};
function resultHandling () {
// console.log("RESULT HANDLING");
// We always insert a result row if we have a link - we will store
// grades locally in this row - even if we cannot send grades
if ( row.result_id == null && row.link_id != null && row.user_id != null ) {
let sql = `INSERT INTO {p}lti_result
( link_id, user_id, created_at, updated_at ) VALUES
( :link_id, :user_id, NOW(), NOW() )`;
let data = {
link_id: row.link_id,
user_id: row.user_id
};
return CFG.pdox.insertKey(sql, data).then( function(insertId) {
row.result_id = insertId;
actions.push( "=== Inserted result id="+row.result_id);
});
}
};
function postTweeks () {
console.log("POST TWEAKS");
// Set these values to null if they were not in the post
if ( ! TsugiUtils.isset(post.sourcedid) ) post.sourcedid = null;
if ( ! TsugiUtils.isset(post.service) ) post.service = null;
if ( ! TsugiUtils.isset(post.result_url) ) post.result_url = null;
if ( ! TsugiUtils.isset(row.service) ) {
row.service = null;
row.service_id = null;
}
}
function resultUpdate () {
console.log("RESULT UPDATE");
// Here we handle updates to sourcedid or result_url including if we
// just inserted the result row
if ( row.result_id != null &&
(post.sourcedid != row.sourcedid || post.result_url != row.result_url ||
post.service != row.service )
) {
let sql = `UPDATE {p}lti_result
SET sourcedid = :sourcedid, result_url = :result_url, service_id = :service_id
WHERE result_id = :result_id`;
let data = {
result_url: post.result_url,
sourcedid: post.sourcedid,
service_id: row.service_id,
result_id: row.result_id
};
return CFG.pdox.query(sql, data).then( function() {
row.sourcedid = post.sourcedid;
row.service = post.service;
row.result_url = post.result_url;
actions.push( "=== Updated result id="+row.result_id+" result_url="+row.result_url+
" sourcedid="+row.sourcedid+" service_id="+row.service_id);
});
}
};
function updateContext () {
console.log("UPDATING CONTEXT");
if ( row.context_id != null &&
TsugiUtils.isset(post.context_title) && post.context_title != row.context_title ) {
let sql = `UPDATE {p}lti_context SET title = :title WHERE context_id = :context_id`;
let data = {
title: post.context_title,
context_id: row.context_id
};
return CFG.pdox.query(sql, data).then( function() {
row.context_title = post.context_title;
actions.push( "=== Updated context="+row.context_id+" title="+post.context_title);
});
}
};
function updateContextColumn (column) {
console.log("UPDATING CONTEXT "+column);
if ( row.context_id != null &&
TsugiUtils.isset(post[column]) && post[column] != row[column] ) {
let sql = `UPDATE {p}lti_context SET `+column+` = :value WHERE context_id = :context_id`;
let data = {
value: post[column],
context_id: row.context_id
};
return CFG.pdox.query(sql, data).then( function() {
row[column] = post[column];
actions.push( "=== Updated context="+row.context_id+" "+column+"="+post[column]);
});
}
};
function updateLink () {
console.log("UPDATING LINK");
if ( row.link_id != null &&
TsugiUtils.isset(post.link_title) && post.link_title != row.link_title ) {
let sql = "UPDATE {p}lti_link SET title = :title WHERE link_id = :link_id";
let data = {
title: post.link_title,
link_id: row.link_id
};
return CFG.pdox.query(sql, data).then( function() {
row.link_title = post.link_title;
actions.push( "=== Updated link="+row.link_id+" title="+post.link_title);
});
}
};
function updateUser(fieldname) {
console.log("UPDATING USER "+fieldname);
var post_name = "user_"+fieldname;
if ( row.user_id != null &&
TsugiUtils.isset(post[post_name]) && post[post_name] != row[fieldname] && post[post_name].length > 0 ) {
let sql = "UPDATE {p}lti_user SET "+fieldname+" = :value WHERE user_id = :user_id";
let data = {
value: post[post_name],
user_id: row.user_id
};
return CFG.pdox.query(sql, data).then( function() {
row[fieldname] = post[post_name];
actions.push( "=== Updated user="+row.user_id+" "+fieldname+"="+post[post_name]);
});
}
};
function updateEmail () {
console.log("UPDATING EMAIL");
if ( TsugiUtils.isset(post.user_email) && post.user_email != row.user_email && post.user_email.length > 0 ) {
let sql = "UPDATE {p}lti_user SET email = :email WHERE user_id = :user_id";
let data = {
email: post.user_email,
user_id: row.user_id
};
return CFG.pdox.query(sql, data).then( function() {
row.user_email = post.user_email;
actions.push( "=== Updated user="+row.user_id+" email="+post.user_email);
});
}
};
function updateRole (){
console.log("UPDATING ROLE");
if ( TsugiUtils.isset(post.role) && post.role != row.role ) {
let sql = "UPDATE {p}lti_membership SET role = :role WHERE membership_id = :membership_id";
let data = {
role: post.role,
membership_id: row.membership_id
};
return CFG.pdox.query(sql, data).then( function() {
row.role = post.role;
actions.push( "=== Updated membership="+row.membership_id+" role="+post.role);
});
}
};
var adjustSequence = Q.defer();
TsugiUtils.emptyPromise()
.then(contextHandling)
.then(linkHandling)
.then(userHandling)
.then(membershipHandling)
.then(serviceHandling)
.then(resultToServiceHandling)
.then(resultHandling)
.then(postTweeks)
.then(resultUpdate)
.then(updateContext)
.then(updateContextColumn('ext_memberships_id'))
.then(updateContextColumn('ext_memberships_url'))
.then(updateContextColumn('lineitems_url'))
.then(updateContextColumn('memberships_url'))
.then(updateLink)
.then(updateUser('displayname'))
.then(updateUser('image'))
.then(updateEmail)
.then(updateRole)
.then(function (){
// De-undefine the altered row data
TsugiUtils.toNullAll(row);
// console.log(row);
if ( actions.length > 0 ) console.log("Actions", actions);
adjustSequence.resolve(row);
})
.catch (function (error){
console.log (error);
adjustSequence.reject();
});
return adjustSequence.promise;
}
/*
** Returns true if this is an LTI message with minimum values to meet the protocol
*
* @param {*} needed Indicates which of
*
*/
isRequest(props) {
let vers = props.lti_version;
let mtype = props.lti_message_type;
if ( vers == null || mtype == null ) return false;
let good_message_type = mtype == ("basic-lti-launch-request" ||
mtype == "ToolProxyReregistrationRequest" ||
mtype == "ContentItemSelectionRequest");
let good_lti_version = (vers == "LTI-1p0" || vers == "LTI-2p0");
if (good_message_type && good_lti_version ) return true;
return false;
}
/*
** Check the OAuth Signature
*/
checkOAuthSignature(launch, key, secret) {
// var lti = require('tsugi-node-lti/lib/ims-lti.js');
let lti = launch.CFG.LTI;
// provider = new lti.Provider '12345', 'secret', [nonce_store=MemoryStore], [signature_method=HMAC_SHA1]
let provider = new lti.Provider (key, secret);
// Returns three item array:
// [0] An error with member ".message" containing a textual message
// [1] true/false if it was validated
// [2] The base string or null
let x = provider.valid_request(launch.req, launch.req.body, function(x,y,z) { return [x,y,z];} );
// console.log('YAYAYAYAYAYAYAYAY',x);
return x;
}
/**
* Patch the value for the list of needed features and return a Set
*
* ns = this.patchNeeded(Tsugi.ALL)
* console.log(ns.has(this.ALL));
*
* or if you don't need link..
*
* ns = this.patchNeeded([Tsugi.USER,Tsugi.CONTEXT]))
* console.log(ns.has(this.ALL));
*
*
* Note - causes no harm if called more than once.
*
* @param {*} needed Indicates which of
* the data structures are needed. If this is omitted,
* this assumes that CONTEXT, LINK, and USER data are required.
* If NONE is present, then none of the three are rquired.
* If some combination of the three are needed, this accepts
* an array of the CONTEXT, LINK, and USER
* can be passed in.
*
* @return {Set} A set of the needed values
*/
patchNeeded(needed) {
if ( needed instanceof Set ) return needed;
if ( needed == this.NONE ) return new Set();
if ( needed == this.ALL ) {
let ns = new Set([this.CONTEXT, this.USER, this.LINK]);
return ns;
}
if ( ! ( needed instanceof Array ) ) {
needed = [ needed ];
}
// console.log(needed);
let ns = new Set(needed);
return ns;
}
}
module.exports = new Tsugi();