Skip to content

Commit

Permalink
LTI Authentication (#53)
Browse files Browse the repository at this point in the history
* adds LTI authenthication to filter

* checkers fixes

* fix ci branch issue

* fix stylelint checker

* fix unit test

* fix php warning: non-component has no effect
  • Loading branch information
ferishili authored Dec 13, 2024
1 parent 6db0017 commit 2c12724
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/moodle-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ jobs:
uses: Opencast-Moodle/moodle-workflows-opencast/.github/workflows/moodle-ci.yml@main
with:
requires-tool-plugin: true
branch-tool-plugin: master
branch-tool-plugin: main
191 changes: 191 additions & 0 deletions classes/local/lti_helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* LTI helper class for filter opencast.
* @package filter_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace filter_opencast\local;

use oauth_helper;
use tool_opencast\local\settings_api;

/**
* LTI helper class for filter opencast.
* @package filter_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lti_helper {

/** */
const LTI_LAUNCH_PATH = '/filter/opencast/ltilaunch.php';

/**
* Create necessary lti parameters.
* @param string $consumerkey LTI consumer key.
* @param string $consumersecret LTI consumer secret.
* @param string $endpoint of the opencast instance.
* @param string $customtool the custom tool
* @param int $courseid the course id to add into the parameters
*
* @return array lti parameters
*/
public static function create_lti_parameters($consumerkey, $consumersecret, $endpoint, $customtool, $courseid) {
global $CFG, $USER;

$course = get_course($courseid);

require_once($CFG->dirroot . '/mod/lti/locallib.php');
require_once($CFG->dirroot . '/lib/oauthlib.php');

$helper = new oauth_helper(['oauth_consumer_key' => $consumerkey,
'oauth_consumer_secret' => $consumersecret, ]);

// Set all necessary parameters.
$params = [];
$params['oauth_version'] = '1.0';
$params['oauth_nonce'] = $helper->get_nonce();
$params['oauth_timestamp'] = $helper->get_timestamp();
$params['oauth_consumer_key'] = $consumerkey;

$params['context_id'] = $course->id;
$params['context_label'] = trim($course->shortname);
$params['context_title'] = trim($course->fullname);
$params['resource_link_id'] = 'o' . random_int(1000, 9999) . '-' . random_int(1000, 9999);
$params['resource_link_title'] = 'Opencast';
$params['context_type'] = ($course->format == 'site') ? 'Group' : 'CourseSection';
$params['launch_presentation_locale'] = current_language();
$params['ext_lms'] = 'moodle-2';
$params['tool_consumer_info_product_family_code'] = 'moodle';
$params['tool_consumer_info_version'] = strval($CFG->version);
$params['oauth_callback'] = 'about:blank';
$params['lti_version'] = 'LTI-1p0';
$params['lti_message_type'] = 'basic-lti-launch-request';
$urlparts = parse_url($CFG->wwwroot);
$params['tool_consumer_instance_guid'] = $urlparts['host'];
$params['custom_tool'] = urlencode($customtool);

// User data.
$params['user_id'] = $USER->id;
$params['lis_person_name_given'] = $USER->firstname;
$params['lis_person_name_family'] = $USER->lastname;
$params['lis_person_name_full'] = $USER->firstname . ' ' . $USER->lastname;
$params['ext_user_username'] = $USER->username;
$params['lis_person_contact_email_primary'] = $USER->email;
$params['roles'] = lti_get_ims_role($USER, null, $course->id, false);

if (!empty($CFG->mod_lti_institution_name)) {
$params['tool_consumer_instance_name'] = trim(html_to_text($CFG->mod_lti_institution_name, 0));
} else {
$params['tool_consumer_instance_name'] = get_site()->shortname;
}

$params['launch_presentation_document_target'] = 'iframe';
$params['oauth_signature_method'] = 'HMAC-SHA1';
$params['oauth_signature'] = $helper->sign("POST", $endpoint, $params, $consumersecret . '&');
return $params;
}

/**
* Retrieves the LTI consumer key and consumer secret for a given Opencast instance ID.
*
* @param int $ocinstanceid The ID of the Opencast instance.
*
* @return array An associative array containing the 'consumerkey' and 'consumersecret' for the given Opencast instance.
* If the credentials are not found, an empty array is returned.
*/
public static function get_lti_credentials(int $ocinstanceid) {
$lticonsumerkey = settings_api::get_lticonsumerkey($ocinstanceid);
$lticonsumersecret = settings_api::get_lticonsumersecret($ocinstanceid);
return ['consumerkey' => $lticonsumerkey, 'consumersecret' => $lticonsumersecret];
}

/**
* Checks if LTI credentials are configured for a given Opencast instance.
*
* This function verifies whether both the LTI consumer key and consumer secret
* are set for the specified Opencast instance.
*
* @param int $ocinstanceid The ID of the Opencast instance to check.
*
* @return bool Returns true if both LTI consumer key and secret are configured,
* false otherwise.
*/
public static function is_lti_credentials_configured(int $ocinstanceid) {
$lticredentials = self::get_lti_credentials($ocinstanceid);
return !empty($lticredentials['consumerkey']) && !empty($lticredentials['consumersecret']);
}

/**
* Retrieves an object containing LTI settings for a given Opencast instance.
*
* This function gathers the LTI credentials and API URL for the specified Opencast instance
* and returns them as a structured object.
*
* @param int $ocinstanceid The ID of the Opencast instance for which to retrieve LTI settings.
*
* @return object An object containing the following properties:
* - ocinstanceid: The ID of the Opencast instance.
* - consumerkey: The LTI consumer key for the instance.
* - consumersecret: The LTI consumer secret for the instance.
* - baseurl: The API URL for the Opencast instance.
*/
public static function get_lti_set_object(int $ocinstanceid) {
$lticredentials = self::get_lti_credentials($ocinstanceid);
$baseurl = settings_api::get_apiurl($ocinstanceid);

return (object) [
'ocinstanceid' => $ocinstanceid,
'consumerkey' => $lticredentials['consumerkey'],
'consumersecret' => $lticredentials['consumersecret'],
'baseurl' => $baseurl,
];
}

/**
* Generates the LTI launch URL for the Opencast filter.
*
* This function creates a URL for launching LTI content specific to the Opencast filter,
* incorporating necessary parameters such as course ID, Opencast instance ID, and episode ID.
*
* @param int $ocinstanceid The ID of the Opencast instance.
* @param int $courseid The ID of the course.
* @param string $episodeid The ID of the Opencast episode.
* @param bool $output Optional. If true, returns the URL as a string. If false, returns a moodle_url object. Default is true.
*
* @return string|moodle_url If $output is true, returns the LTI launch URL as a string.
* If $output is false, returns a moodle_url object representing the LTI launch URL.
*/
public static function get_filter_lti_launch_url(int $ocinstanceid, int $courseid, string $episodeid, bool $output = true) {
$params = [
'courseid' => $courseid,
'ocinstanceid' => $ocinstanceid,
'episodeid' => $episodeid,
'sesskey' => sesskey(),
];
$ltilaunchurl = new \moodle_url(self::LTI_LAUNCH_PATH, $params);
if ($output) {
return $ltilaunchurl->out(false);
}
return $ltilaunchurl;
}
}
33 changes: 24 additions & 9 deletions classes/text_filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@

namespace filter_opencast;

use filter_opencast\local\lti_helper;
use mod_opencast\local\paella_transform;
use tool_opencast\local\settings_api;
use stdClass;
use moodle_url;


/**
* Automatic opencast videos filter class.
Expand Down Expand Up @@ -57,9 +62,9 @@ private static function get_attribute(string $tag, string $attributename, string
* @return array|null [ocinstanceid, episodeid] or null if there are no matches.
*/
private static function test_url(string $url, array $episodeurls) {
foreach ($episodeurls as [$ocinstanceid, $episoderegex, $baseurl]) {
foreach ($episodeurls as [$ocinstanceid, $episoderegex, $baseurl, $shoulduselti]) {
if (preg_match_all($episoderegex, $url, $matches)) {
return [$ocinstanceid, $matches[1][0]];
return [$ocinstanceid, $matches[1][0], $shoulduselti];
}
}
return null;
Expand All @@ -81,7 +86,7 @@ public function filter($text, array $options = []) {

// First section: (Relatively) quick check if there are episode urls in the text, and only look for these later.
// Improvable by combining all episode urls into one big regex if needed.
$ocinstances = \tool_opencast\local\settings_api::get_ocinstances();
$ocinstances = settings_api::get_ocinstances();
$occurrences = [];
foreach ($ocinstances as $ocinstance) {
$episodeurls = get_config('filter_opencast', 'episodeurl_' . $ocinstance->id);
Expand All @@ -90,6 +95,11 @@ public function filter($text, array $options = []) {
continue;
}

$uselticonfig = get_config('filter_opencast', 'uselti_' . $ocinstance->id);
// Double check.
$hasconfiguredlti = lti_helper::is_lti_credentials_configured($ocinstance->id);
$shoulduselti = $uselticonfig && $hasconfiguredlti;

foreach (explode("\n", $episodeurls) as $episodeurl) {
$episodeurl = trim($episodeurl);

Expand All @@ -105,7 +115,7 @@ public function filter($text, array $options = []) {
if (self::str_contains($text, $baseurl)) {
$episoderegex = "/" . preg_quote($episodeurl, "/") . "/";
$episoderegex = preg_replace('/\\\\\[EPISODEID\\\]/', '([0-9a-zA-Z\-]+)', $episoderegex);
$occurrences[] = [$ocinstance->id, $episoderegex, $baseurl];
$occurrences[] = [$ocinstance->id, $episoderegex, $baseurl, $shoulduselti];
}
}
}
Expand Down Expand Up @@ -139,7 +149,7 @@ public function filter($text, array $options = []) {
if (self::str_starts_with($match, "</$currenttag")) {
$replacement = null;
if ($episode) {
$replacement = $this->render_player($episode[0], $episode[1], $i++, $width, $height);
$replacement = $this->render_player($episode[0], $episode[1], $episode[2], $i++, $width, $height);
}
if ($replacement) {
$newtext .= $replacement;
Expand Down Expand Up @@ -192,14 +202,15 @@ public function filter($text, array $options = []) {
* Render HTML for embedding video player.
* @param int $ocinstanceid Id of ocinstance.
* @param string $episodeid Id opencast episode.
* @param bool $shoulduselti Flag to determine whether to use LTI launch for this video or not.
* @param int $playerid Unique id to assign to player element.
* @param int|null $width Optionally width for player.
* @param int|null $height Optionally height for player.
* @return string|null
*/
protected function render_player(int $ocinstanceid, string $episodeid, int $playerid,
$width = null, $height = null) {
global $OUTPUT, $PAGE;
protected function render_player(int $ocinstanceid, string $episodeid, bool $shoulduselti,
int $playerid, $width = null, $height = null) {
global $OUTPUT, $PAGE, $COURSE;

$data = paella_transform::get_paella_data_json($ocinstanceid, $episodeid);

Expand All @@ -218,7 +229,11 @@ protected function render_player(int $ocinstanceid, string $episodeid, int $play
$mustachedata->data = json_encode($data);
$mustachedata->width = $width;
$mustachedata->height = $height;
$mustachedata->modplayerpath = (new moodle_url('/mod/opencast/player.html'))->out(false);
if ($shoulduselti) {
$mustachedata->ltiplayerpath = lti_helper::get_filter_lti_launch_url($ocinstanceid, $COURSE->id, $episodeid);
} else {
$mustachedata->modplayerpath = (new moodle_url('/mod/opencast/player.html'))->out(false);
}

if (isset($data['streams'])) {
if (count($data['streams']) === 1) {
Expand Down
5 changes: 5 additions & 0 deletions lang/en/filter_opencast.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@

defined('MOODLE_INTERNAL') || die();
$string['filtername'] = 'Opencast';
$string['ltilaunch_failed'] = 'Performing LTI authentication failed, please try again!';
$string['pluginname'] = 'Opencast Filter';
$string['privacy:metadata'] = 'The Opencast filter plugin does not store any personal data.';
$string['setting_configurl'] = 'URL to Paella config.json';
$string['setting_configurl_desc'] = 'URL of the config.json used by Paella Player. Can either be a absolute URL or a URL relative to the wwwroot.';
$string['setting_episodeurl'] = 'URL templates for filtering';
$string['setting_episodeurl_desc'] = 'URLs matching this template are replaced with the Opencast player. You must use the placeholder [EPISODEID] to indicate where the episode ID is contained in the URL e.g. http://stable.opencast.de/play/[EPISODEID]. If you want to filter for multiple URLs, enter each URL in a new line.';
$string['setting_uselti'] = 'Enable LTI authentication';
$string['setting_uselti_desc'] = 'When enabled, Opencast videos are delivered through LTI authentication using the <strong>default Opencast video player</strong>. This is typically used alongside Secure Static Files in Opencast for enhanced security.';
$string['setting_uselti_nolti_desc'] = 'To enable LTI Authentication for Opencast, you must configure the required credentials (Consumer Key and Consumer Secret) for this instance. Please do so via this link: {$a}';
$string['setting_uselti_ocinstance_name'] = 'Opencast API {$a} Instance';
81 changes: 81 additions & 0 deletions ltilaunch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* LTI Launch page.
* Designed to be called as a link in an iframe to prepare the lti launch data and perform the launch.
*
* @package filter_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

use filter_opencast\local\lti_helper;

require(__DIR__ . '/../../config.php');

global $PAGE;

$courseid = required_param('courseid', PARAM_INT);
$ocinstanceid = required_param('ocinstanceid', PARAM_INT);
$episodeid = required_param('episodeid', PARAM_ALPHANUMEXT);

$baseurl = lti_helper::get_filter_lti_launch_url($ocinstanceid, $courseid, $episodeid, false);

$PAGE->set_pagelayout('embedded');
$PAGE->set_url($baseurl);

require_login($courseid, false);

if (confirm_sesskey()) {
$ltisetobject = lti_helper::get_lti_set_object($ocinstanceid);
$customtool = "/play/{$episodeid}";
$endpoint = rtrim($ltisetobject->baseurl, '/') . '/lti';
$ltiparams = lti_helper::create_lti_parameters(
$ltisetobject->consumerkey,
$ltisetobject->consumersecret,
$endpoint,
$customtool,
$courseid
);
$formid = "ltiLaunchForm-{$episodeid}";
$formattributed = [
'action' => $endpoint,
'method' => 'post',
'id' => $formid,
'name' => $formid,
'encType' => 'application/x-www-form-urlencoded',
];
echo html_writer::start_tag('form', $formattributed);

foreach ($ltiparams as $name => $value) {
$attributes = ['type' => 'hidden', 'name' => htmlspecialchars($name), 'value' => htmlspecialchars($value)];
echo html_writer::empty_tag('input', $attributes) . "\n";
}

echo html_writer::end_tag('form');

echo html_writer::script(
"window.onload = function() {
document.getElementById('{$formid}').submit();
};"
);

exit();
}

throw new \moodle_exception('ltilaunch_failed', 'filter_opencast', $baseurl);
Loading

0 comments on commit 2c12724

Please sign in to comment.