From 4e2787b52bfd69be32b38331916d282192ec77a7 Mon Sep 17 00:00:00 2001 From: Charles Severance Date: Mon, 5 Sep 2022 10:19:33 -0400 Subject: [PATCH] Add the LTI 1.x roster extension support --- api/ltiextroster.php | 183 ++++++++++++++++++++++++++++++ api/poxresult.php | 6 +- vendor/tsugi/lib/src/Util/Net.php | 26 ++++- 3 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 api/ltiextroster.php diff --git a/api/ltiextroster.php b/api/ltiextroster.php new file mode 100644 index 0000000000..72cc211d3d --- /dev/null +++ b/api/ltiextroster.php @@ -0,0 +1,183 @@ +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)); + +/* + + + basic-lis-readmembershipsforcontext + + + 7d69999997 + hir@ppp.com + Sakai + Hirouki Sakai + Hirouki + hirouki + Instructor + 422e099999-45dc-a4e5-196d3f749782 + + + 7d65a1b397 + csev@ppp.co + Severance + Charles Severance + Charles + csev + Learner + 422e09b8-b53a-45dc-a4e5-196d3f749782 + + + + Success + fullsuccess + Status + + + */ + +header('Content-Type: application/xml; charset=utf-8'); + +?> + + basic-lis-readmembershipsforcontext + +\n"); + echo(" ".$row['user_id']."\n"); + if ( $row['role'] >= 1000 ) { + echo(" Instructor\n"); + } else { + echo(" Learner\n"); + } + if ( $sendEmail && is_string($email) && strlen($email) > 0 ) { + echo(" "); + echo(htmlspecialchars($email, ENT_XML1, 'UTF-8')); + echo("\n"); + } + if ( $sendName && is_string($displayname) && strlen($displayname) > 0 ) { + echo(" "); + echo(htmlspecialchars($displayname, ENT_XML1, 'UTF-8')); + echo("\n"); + } + echo(" \n"); +} +?> + + + Success + fullsuccess + Status + + diff --git a/api/poxresult.php b/api/poxresult.php index 7dbd45dc54..7d1f4a279d 100644 --- a/api/poxresult.php +++ b/api/poxresult.php @@ -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'); @@ -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; diff --git a/vendor/tsugi/lib/src/Util/Net.php b/vendor/tsugi/lib/src/Util/Net.php index a59a8b9a65..870474555f 100644 --- a/vendor/tsugi/lib/src/Util/Net.php +++ b/vendor/tsugi/lib/src/Util/Net.php @@ -433,15 +433,31 @@ 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); + } } /** @@ -449,7 +465,7 @@ public static function send400($msg='Malformed 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