Skip to content

Commit

Permalink
Add the LTI 1.x roster extension support
Browse files Browse the repository at this point in the history
  • Loading branch information
csev committed Sep 5, 2022
1 parent e381fc5 commit 4e2787b
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 8 deletions.
183 changes: 183 additions & 0 deletions api/ltiextroster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php
require_once "../config.php";

use \Tsugi\OAuth\OAuthUtil;
use \Tsugi\Util\U;
use \Tsugi\Util\LTI;
use \Tsugi\Util\Net;
use \Tsugi\Core\LTIX;
use \Tsugi\Core\Result;

// This was something added by Moodle and Sakai and at one point was documented at IMS
// IMS seems to have deleted it just because. So we look to the Sakai documents:
//
// https://github.com/sakaiproject/sakai/blob/master/basiclti/docs/sakai_basiclti_api.md

$membership_id = U::get($_POST,'id');
// Parse the sourcedid
$pieces = explode('::', $membership_id);
if ( count($pieces) != 4 ) {
Net::send400('Invalid sourcedid format');
return;
}
if ( is_numeric($pieces[0]) && is_numeric($pieces[1]) && is_numeric($pieces[2]) ) {
$key_id = $pieces[0];
$context_id = $pieces[1];
$link_id = $pieces[2];
$incoming_sig = $pieces[3];
} else {
Net::send400('sourcedid requires 4 numeric parameters');
return;
}

$sql = "SELECT K.secret, K.key_key, C.context_key, L.settings, L.placementsecret
FROM {$CFG->dbprefix}lti_key AS K
JOIN {$CFG->dbprefix}lti_context AS C ON K.key_id = C.key_id
JOIN {$CFG->dbprefix}lti_link AS L ON C.context_id = L.context_id
WHERE K.key_id = :KID AND C.context_id = :CID AND
L.link_id = :LID
LIMIT 1";

$PDOX = LTIX::getConnection();

$row = $PDOX->rowDie($sql, array(
":KID" => $key_id,
":CID" => $context_id,
":LID" => $link_id)
);

if ( ! $row ) {
Net::send403('Could not locate sourcedid row');
return;
}

$oauth_consumer_key_up = $row['key_key'];
$oauth_consumer_secret_up = LTIX::decrypt_secret($row['secret']);
$placementsecret = $row['placementsecret'];
$settingsstr = $row['settings'];
try {
$settings = json_decode($settingsstr);
$oauth_consumer_key = $settings->key;
$oauth_consumer_secret = LTIX::decrypt_secret($settings->secret);
} catch(Exception $e) {
Net::send403('Could not parse link settings');
return;

}

$allowRoster = isset($settings->allowRoster) && $settings->allowRoster;
if ( ! $allowRoster ) {
Net::send403('Roster sharing not enabled for this context');
return;
}

$sendName = isset($settings->sendName) && $settings->sendName;
$sendEmail = isset($settings->sendEmail) && $settings->sendEmail;

$sourcebase = $key_id . '::' . $context_id . '::' . $link_id . '::';
$plain = $sourcebase . $placementsecret;
$sig = U::lti_sha256($plain);

if ( $incoming_sig != $sig ) {
Net::send403('Invalid sourcedid signature');
return;
}

if ( strlen($oauth_consumer_key) < 1 || strlen($oauth_consumer_secret) < 1 ) {
Net::send403("Missing key/secret key=$oauth_consumer_key");
return;
}

$post_key = U::get($_POST, 'oauth_consumer_key');
if ( $post_key != $oauth_consumer_key ) {
Net::send403("Mismatch oauth_consumer_key does not match $post_key");
return;
}

$cur_page_url = LTIX::curPageUrl();
$row_secret = $row['secret'];

$valid = LTI::verifyKeyAndSecret($post_key,$row_secret,$cur_page_url, $_POST);
if ( is_array($valid) ) {
Net::send403($valid[0], $valid[1]);
return;
}

$sql = "SELECT M.role, M.user_id, U.displayname, U.email
FROM {$CFG->dbprefix}lti_membership AS M
JOIN {$CFG->dbprefix}lti_user AS U ON M.user_id = U.user_id
WHERE M.context_id = :CID AND M.deleted = 0";

$rows = $PDOX->allRowsDie($sql, array(":CID" => $context_id));

/*
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<message_response>
<lti_message_type>basic-lis-readmembershipsforcontext</lti_message_type>
<members>
<member>
<lis_result_sourcedid>7d69999997</lis_result_sourcedid>
<person_contact_email_primary>[email protected]</person_contact_email_primary>
<person_name_family>Sakai</person_name_family>
<person_name_full>Hirouki Sakai</person_name_full>
<person_name_given>Hirouki</person_name_given>
<person_sourcedid>hirouki</person_sourcedid>
<role>Instructor</role>
<user_id>422e099999-45dc-a4e5-196d3f749782</user_id>
</member>
<member>
<lis_result_sourcedid>7d65a1b397</lis_result_sourcedid>
<person_contact_email_primary>[email protected]</person_contact_email_primary>
<person_name_family>Severance</person_name_family>
<person_name_full>Charles Severance</person_name_full>
<person_name_given>Charles</person_name_given>
<person_sourcedid>csev</person_sourcedid>
<role>Learner</role>
<user_id>422e09b8-b53a-45dc-a4e5-196d3f749782</user_id>
</member>
</members>
<statusinfo>
<codemajor>Success</codemajor>
<codeminor>fullsuccess</codeminor>
<severity>Status</severity>
</statusinfo>
</message_response>
*/

header('Content-Type: application/xml; charset=utf-8');

?><?xml version="1.0" encoding="UTF-8" standalone="no"?>
<message_response>
<lti_message_type>basic-lis-readmembershipsforcontext</lti_message_type>
<members>
<?php
foreach($rows as $row) {
$email = $row['email'];
$displayname = $row['displayname'];
echo(" <member>\n");
echo(" <user_id>".$row['user_id']."</user_id>\n");
if ( $row['role'] >= 1000 ) {
echo(" <role>Instructor</role>\n");
} else {
echo(" <role>Learner</role>\n");
}
if ( $sendEmail && is_string($email) && strlen($email) > 0 ) {
echo(" <person_contact_email_primary>");
echo(htmlspecialchars($email, ENT_XML1, 'UTF-8'));
echo("</person_contact_email_primary>\n");
}
if ( $sendName && is_string($displayname) && strlen($displayname) > 0 ) {
echo(" <person_name_full>");
echo(htmlspecialchars($displayname, ENT_XML1, 'UTF-8'));
echo("</person_name_full>\n");
}
echo(" </member>\n");
}
?>
</members>
<statusinfo>
<codemajor>Success</codemajor>
<codeminor>fullsuccess</codeminor>
<severity>Status</severity>
</statusinfo>
</message_response>
6 changes: 3 additions & 3 deletions api/poxresult.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
$request_headers = OAuthUtil::get_headers();
$hct = U::get($request_headers,'Content-Type', U::get($_SERVER, 'CONTENT_TYPE'));

// Get skeleton response
$response = LTI::getPOXResponse();

if (strpos($hct,'application/xml') === false ) {
header('Content-Type: text/plain');

Expand Down Expand Up @@ -108,9 +111,6 @@
return;
}

// Get skeleton response
$response = LTI::getPOXResponse();

if ( strlen($oauth_consumer_key) < 1 || strlen($oauth_consumer_secret) < 1 ) {
echo(sprintf($response,uniqid(),'failure', "Missing key/secret key=$oauth_consumer_key",$message_ref,"",""));
return;
Expand Down
26 changes: 21 additions & 5 deletions vendor/tsugi/lib/src/Util/Net.php
Original file line number Diff line number Diff line change
Expand Up @@ -433,23 +433,39 @@ public static function bodyCurl($url, $method, $body, $header) {
/**
* Send a 403 header
*/
public static function send403() {
header("HTTP/1.1 403 Forbidden");
public static function send403($msg=null, $detail=null) {
if ( headers_sent() ) {
echo("Headers sent - they would be:\n");
echo("HTTP/1.1 403 Forbidden"."\n");
if ( is_string($msg) ) echo("X-Error-Message: ".$msg."\n");
if ( is_string($detail) ) echo("X-Error-Detail: ".$detail."\n");
} else {
header("HTTP/1.1 403 Forbidden");
if ( is_string($msg) ) header("X-Error-Message: ".$msg);
if ( is_string($detail) ) header("X-Error-Detail: ".$detail);
}
}

/**
* Send a 400 (Malformed request) header
*/
public static function send400($msg='Malformed request') {
header("HTTP/1.1 400 ".$msg);
public static function send400($msg='Malformed request', $detail=null) {
if ( headers_sent() ) {
echo("Headers sent - they would be:\n");
echo("HTTP/1.1 400 ".$msg."\n");
if ( is_string($detail) ) echo("X-Error-Detail: ".$detail."\n");
} else {
header("HTTP/1.1 400 ".$msg);
if ( is_string($detail) ) header("X-Error-Detail: ".$detail);
}
}

/**
* Get the actual IP address of the incoming request.
*
* Handle being behind a load balancer or a proxy like Cloudflare.
* This will often return NULL when talking to localhost to make sure
* to test code using this ona real IP address.
* to test code using this on a real IP address.
*
* Adapted from: https://www.chriswiegman.com/2014/05/getting-correct-ip-address-php/
* With some additional explode goodness via: http://stackoverflow.com/a/25193833/1994792
Expand Down

0 comments on commit 4e2787b

Please sign in to comment.