mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-03-09 06:27:21 -04:00
all
This commit is contained in:
@@ -8,57 +8,3 @@
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Find the first LTI activity for the MediaCMS tool in a course, or create a
|
||||
* visible dummy one if none exists. Repairs any existing stealth/hidden activity.
|
||||
*
|
||||
* @param int $courseid
|
||||
* @param int $typeid LTI tool type ID
|
||||
* @return int course-module ID
|
||||
*/
|
||||
function filter_mediacms_get_dummy_activity($courseid, $typeid) {
|
||||
global $DB;
|
||||
|
||||
$sql = "SELECT cm.id
|
||||
FROM {course_modules} cm
|
||||
JOIN {modules} m ON m.id = cm.module
|
||||
JOIN {lti} lti ON lti.id = cm.instance
|
||||
WHERE cm.course = :courseid
|
||||
AND m.name = 'lti'
|
||||
AND lti.typeid = :typeid
|
||||
AND cm.deletioninprogress = 0
|
||||
ORDER BY cm.visible DESC, cm.visibleoncoursepage DESC
|
||||
LIMIT 1";
|
||||
|
||||
$existing = $DB->get_record_sql($sql, ['courseid' => $courseid, 'typeid' => $typeid]);
|
||||
if ($existing) {
|
||||
return $existing->id;
|
||||
}
|
||||
|
||||
// Create the dummy activity then immediately force visibleoncoursepage=0,
|
||||
// because add_moduleinfo() always defaults it to 1.
|
||||
$moduleinfo = new stdClass();
|
||||
$moduleinfo->course = $courseid;
|
||||
$moduleinfo->module = $DB->get_field('modules', 'id', ['name' => 'lti']);
|
||||
$moduleinfo->modulename = 'lti';
|
||||
$moduleinfo->section = 0;
|
||||
$moduleinfo->visible = 1;
|
||||
$moduleinfo->visibleoncoursepage = 1;
|
||||
$moduleinfo->availability = null;
|
||||
$moduleinfo->showdescription = 0;
|
||||
$moduleinfo->name = 'My Media';
|
||||
$moduleinfo->intro = '';
|
||||
$moduleinfo->introformat = FORMAT_HTML;
|
||||
$moduleinfo->typeid = $typeid;
|
||||
$moduleinfo->instructorchoiceacceptgrades = 0;
|
||||
$moduleinfo->grade = 0;
|
||||
$moduleinfo->instructorchoicesendname = 1;
|
||||
$moduleinfo->instructorchoicesendemailaddr = 1;
|
||||
$moduleinfo->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
|
||||
$moduleinfo->instructorcustomparameters = '';
|
||||
|
||||
$result = add_moduleinfo($moduleinfo, get_course($courseid));
|
||||
|
||||
return $result->coursemodule;
|
||||
}
|
||||
|
||||
190
lms-plugins/mediacms-moodle/filter/mediacms/lti_auth.php
Normal file
190
lms-plugins/mediacms-moodle/filter/mediacms/lti_auth.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
/**
|
||||
* MediaCMS custom LTI 1.3 auth endpoint.
|
||||
*
|
||||
* Functionally identical to /mod/lti/auth.php except the no-activity (id=0)
|
||||
* branch only requires the user to be logged in — not moodle/course:manageactivities.
|
||||
*
|
||||
* Custom params (publishdata, redirect_path) are read from the PHP session
|
||||
* where lti_launch.php / select_media_picker.php store them before starting
|
||||
* the OIDC flow.
|
||||
*
|
||||
* Setup required:
|
||||
* 1. MediaCMS admin → LTI Platforms → edit Moodle record:
|
||||
* set "Auth login url" to https://YOUR_MOODLE/filter/mediacms/lti_auth.php
|
||||
* 2. Moodle admin → Site admin → Plugins → Activity modules → External tool
|
||||
* → Manage tools → MediaCMS tool → edit → add to "Redirection URIs":
|
||||
* https://YOUR_MOODLE/filter/mediacms/lti_auth.php
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||
global $_POST, $_SERVER, $SESSION;
|
||||
|
||||
if (!isloggedin() && empty($_POST['repost'])) {
|
||||
header_remove("Set-Cookie");
|
||||
$PAGE->set_pagelayout('popup');
|
||||
$PAGE->set_context(context_system::instance());
|
||||
$output = $PAGE->get_renderer('mod_lti');
|
||||
$page = new \mod_lti\output\repost_crosssite_page($_SERVER['REQUEST_URI'], $_POST);
|
||||
echo $output->header();
|
||||
echo $output->render($page);
|
||||
echo $output->footer();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope = optional_param('scope', '', PARAM_TEXT);
|
||||
$responsetype = optional_param('response_type', '', PARAM_TEXT);
|
||||
$clientid = optional_param('client_id', '', PARAM_TEXT);
|
||||
$redirecturi = optional_param('redirect_uri', '', PARAM_URL);
|
||||
$loginhint = optional_param('login_hint', '', PARAM_TEXT);
|
||||
$ltimessagehintenc = optional_param('lti_message_hint', '', PARAM_TEXT);
|
||||
$state = optional_param('state', '', PARAM_TEXT);
|
||||
$responsemode = optional_param('response_mode', '', PARAM_TEXT);
|
||||
$nonce = optional_param('nonce', '', PARAM_TEXT);
|
||||
$prompt = optional_param('prompt', '', PARAM_TEXT);
|
||||
|
||||
$ok = !empty($scope) && !empty($responsetype) && !empty($clientid) &&
|
||||
!empty($redirecturi) && !empty($loginhint) && !empty($nonce);
|
||||
|
||||
if (!$ok) {
|
||||
$error = 'invalid_request';
|
||||
}
|
||||
$ltimessagehint = json_decode($ltimessagehintenc);
|
||||
$ok = $ok && isset($ltimessagehint->launchid);
|
||||
if (!$ok) {
|
||||
$error = 'invalid_request';
|
||||
$desc = 'No launch id in LTI hint';
|
||||
}
|
||||
if ($ok && ($scope !== 'openid')) {
|
||||
$ok = false;
|
||||
$error = 'invalid_scope';
|
||||
}
|
||||
if ($ok && ($responsetype !== 'id_token')) {
|
||||
$ok = false;
|
||||
$error = 'unsupported_response_type';
|
||||
}
|
||||
if ($ok) {
|
||||
$launchid = $ltimessagehint->launchid;
|
||||
list($courseid, $typeid, $id, $messagetype, $foruserid, $titleb64, $textb64) =
|
||||
explode(',', $SESSION->$launchid, 7);
|
||||
unset($SESSION->$launchid);
|
||||
$config = lti_get_type_type_config($typeid);
|
||||
$ok = ($clientid === $config->lti_clientid);
|
||||
if (!$ok) {
|
||||
$error = 'unauthorized_client';
|
||||
}
|
||||
}
|
||||
if ($ok && ($loginhint !== $USER->id)) {
|
||||
$ok = false;
|
||||
$error = 'access_denied';
|
||||
}
|
||||
|
||||
if (empty($config)) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
} else {
|
||||
$uris = array_map('trim', explode("\n", $config->lti_redirectionuris));
|
||||
if (!in_array($redirecturi, $uris)) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
}
|
||||
}
|
||||
if ($ok) {
|
||||
if (isset($responsemode)) {
|
||||
$ok = ($responsemode === 'form_post');
|
||||
if (!$ok) {
|
||||
$error = 'invalid_request';
|
||||
$desc = 'Invalid response_mode';
|
||||
}
|
||||
} else {
|
||||
$ok = false;
|
||||
$error = 'invalid_request';
|
||||
$desc = 'Missing response_mode';
|
||||
}
|
||||
}
|
||||
if ($ok && !empty($prompt) && ($prompt !== 'none')) {
|
||||
$ok = false;
|
||||
$error = 'invalid_request';
|
||||
$desc = 'Invalid prompt';
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
$course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST);
|
||||
|
||||
if ($id) {
|
||||
// Activity-based launch — identical to auth.php's if ($id) branch.
|
||||
$cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST);
|
||||
$context = context_module::instance($cm->id);
|
||||
require_login($course, true, $cm);
|
||||
require_capability('mod/lti:view', $context);
|
||||
$lti = $DB->get_record('lti', ['id' => $cm->instance], '*', MUST_EXIST);
|
||||
$lti->cmid = $cm->id;
|
||||
list($endpoint, $params) = lti_get_launch_data($lti, $nonce, $messagetype, $foruserid);
|
||||
} else {
|
||||
// No-activity launch — student-accessible.
|
||||
// Custom params (publishdata / redirect_path) were stored in the session
|
||||
// by lti_launch.php or select_media_picker.php before initiating the OIDC flow.
|
||||
require_login($course);
|
||||
|
||||
$customparams = '';
|
||||
if (!empty($SESSION->mediacms_launch_customparams)) {
|
||||
$customparams = $SESSION->mediacms_launch_customparams;
|
||||
unset($SESSION->mediacms_launch_customparams);
|
||||
}
|
||||
|
||||
// Minimal LTI instance object — enough for lti_get_launch_data to sign the JWT.
|
||||
$lti = new stdClass();
|
||||
$lti->id = 0;
|
||||
$lti->typeid = (int) $typeid;
|
||||
$lti->course = (int) $courseid;
|
||||
$lti->cmid = 0;
|
||||
$lti->name = 'MediaCMS';
|
||||
$lti->toolurl = '';
|
||||
$lti->securetoolurl = '';
|
||||
$lti->instructorcustomparameters = $customparams;
|
||||
$lti->instructorchoicesendname = LTI_SETTING_ALWAYS;
|
||||
$lti->instructorchoicesendemailaddr = LTI_SETTING_ALWAYS;
|
||||
$lti->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
|
||||
$lti->instructorchoiceallowroster = null;
|
||||
$lti->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
|
||||
$lti->resourcekey = '';
|
||||
$lti->password = '';
|
||||
$lti->servicesalt = '';
|
||||
$lti->resource_link_id = '';
|
||||
|
||||
list($endpoint, $params) = lti_get_launch_data(
|
||||
$lti,
|
||||
$nonce,
|
||||
$messagetype ?: 'basic-lti-launch-request',
|
||||
$foruserid
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$params['error'] = $error;
|
||||
if (!empty($desc)) {
|
||||
$params['error_description'] = $desc;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($state)) {
|
||||
$params['state'] = $state;
|
||||
}
|
||||
unset($SESSION->lti_message_hint);
|
||||
|
||||
$r = '<form action="' . $redirecturi . "\" name=\"ltiAuthForm\" id=\"ltiAuthForm\" " .
|
||||
"method=\"post\" enctype=\"application/x-www-form-urlencoded\">\n";
|
||||
foreach ($params as $key => $value) {
|
||||
$key = htmlspecialchars($key, ENT_COMPAT);
|
||||
$value = htmlspecialchars($value, ENT_COMPAT);
|
||||
$r .= " <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
|
||||
}
|
||||
$r .= "</form>\n";
|
||||
$r .= "<script type=\"text/javascript\">\n" .
|
||||
"//<![CDATA[\n" .
|
||||
"document.ltiAuthForm.submit();\n" .
|
||||
"//]]>\n" .
|
||||
"</script>\n";
|
||||
echo $r;
|
||||
@@ -5,6 +5,13 @@
|
||||
* Builds custom_publishdata (enrolled courses + roles) and initiates
|
||||
* the LTI 1.3 OIDC login flow, outputting the auto-submit form directly.
|
||||
*
|
||||
* No dummy LTI activity is created. The publishdata is stored in the PHP
|
||||
* session and picked up by lti_auth.php during the OIDC callback.
|
||||
*
|
||||
* Edge case: if the user is not enrolled in any course the launch still
|
||||
* proceeds using the site course (SITEID). MediaCMS will receive an empty
|
||||
* publishdata array and can decide how to handle it (e.g. show a message).
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
@@ -13,10 +20,8 @@
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/lib.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||
require_once($CFG->dirroot . '/course/modlib.php');
|
||||
require_once(__DIR__ . '/locallib.php');
|
||||
|
||||
global $SITE, $DB, $CFG, $USER;
|
||||
global $SITE, $DB, $CFG, $USER, $SESSION;
|
||||
|
||||
require_login();
|
||||
|
||||
@@ -32,7 +37,7 @@ if (!$type) {
|
||||
throw new moodle_exception('ltitoolnotfound', 'filter_mediacms');
|
||||
}
|
||||
|
||||
// Build custom_publishdata: all courses the user is enrolled in + role.
|
||||
// Build publishdata: all courses the user is enrolled in + role.
|
||||
$enrolled_courses = enrol_get_users_courses($USER->id, true, ['id', 'shortname', 'fullname']);
|
||||
|
||||
$publish_data = [];
|
||||
@@ -60,8 +65,8 @@ foreach ($enrolled_courses as $enrolled_course) {
|
||||
|
||||
$publishdata_b64 = base64_encode(json_encode($publish_data));
|
||||
|
||||
// Use a course the user is actually enrolled in so they have mod/lti:view during
|
||||
// the OIDC flow. Fall back to SITEID only for admins with no course enrolments.
|
||||
// Use a course the user is actually enrolled in so they pass require_login
|
||||
// in lti_auth.php. Fall back to SITEID for admins with no course enrolments.
|
||||
$launch_courseid = SITEID;
|
||||
$launch_course = $SITE;
|
||||
foreach ($enrolled_courses as $ec) {
|
||||
@@ -72,26 +77,10 @@ foreach ($enrolled_courses as $ec) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get or create the dummy activity (visible, non-stealth).
|
||||
try {
|
||||
$dummy_cmid = filter_mediacms_get_dummy_activity($launch_courseid, $type->id);
|
||||
} catch (Exception $e) {
|
||||
throw new moodle_exception('cannotcreatedummyactivity', 'filter_mediacms');
|
||||
}
|
||||
|
||||
$cm = get_coursemodule_from_id('lti', $dummy_cmid, 0, false, MUST_EXIST);
|
||||
$instance = $DB->get_record('lti', ['id' => $cm->instance], '*', MUST_EXIST);
|
||||
|
||||
// DEBUG: log enrolled courses retrieved.
|
||||
error_log('MediaCMS My Media publishdata courses (' . count($publish_data) . '): ' . json_encode($publish_data));
|
||||
|
||||
// Write publishdata to DB — Moodle's auth.php re-reads the instance from DB
|
||||
// when building the LTI launch JWT, so in-memory changes are ignored.
|
||||
$DB->set_field('lti', 'instructorcustomparameters', 'publishdata=' . $publishdata_b64, ['id' => $cm->instance]);
|
||||
$instance->instructorcustomparameters = 'publishdata=' . $publishdata_b64;
|
||||
$instance->name = 'MediaCMS My Media';
|
||||
// Store publishdata in session — lti_auth.php picks it up after the OIDC roundtrip.
|
||||
$SESSION->mediacms_launch_customparams = 'publishdata=' . $publishdata_b64;
|
||||
|
||||
$typeconfig = lti_get_type_type_config($type->id);
|
||||
$content = lti_initiate_login($launch_course->id, $dummy_cmid, $instance, $typeconfig, null, $instance->name);
|
||||
$content = lti_initiate_login($launch_courseid, 0, null, $typeconfig, null, 'MediaCMS My Media');
|
||||
|
||||
echo $content;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* Student-accessible MediaCMS media picker launcher.
|
||||
*
|
||||
* Initiates a no-activity LTI 1.3 OIDC login that routes to MediaCMS's
|
||||
* /lti/select-media/ UI. No LTI activity is created in the course.
|
||||
*
|
||||
* The redirect_path custom param is stored in the PHP session and injected
|
||||
* by lti_auth.php during the OIDC callback, so MediaCMS routes to the
|
||||
* media-picker rather than the default My Media page.
|
||||
*
|
||||
* Flow:
|
||||
* 1. TinyMCE plugin opens this URL in an iframe (contentItemUrl).
|
||||
* 2. We store redirect_path in session and start the OIDC flow.
|
||||
* 3. lti_auth.php processes the OIDC callback (no manageactivities check).
|
||||
* 4. MediaCMS receives redirect_path=/lti/select-media/?mode=lms_embed_mode.
|
||||
* 5. User picks a video; MediaCMS sends postMessage({type:'videoSelected',...})
|
||||
* which iframeembed.js already handles.
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/lib.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||
|
||||
global $DB, $PAGE, $OUTPUT, $SITE, $USER, $SESSION;
|
||||
|
||||
require_login();
|
||||
|
||||
$courseid = required_param('courseid', PARAM_INT);
|
||||
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
|
||||
|
||||
if (empty($ltitoolid)) {
|
||||
die('MediaCMS LTI tool not configured.');
|
||||
}
|
||||
|
||||
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
|
||||
if (!$type) {
|
||||
die('LTI tool not found.');
|
||||
}
|
||||
|
||||
// Resolve course — fall back to the user's first enrolled course if needed.
|
||||
if ($courseid && $courseid != SITEID) {
|
||||
$course = get_course($courseid);
|
||||
$context = context_course::instance($courseid);
|
||||
} else {
|
||||
$course = $SITE;
|
||||
$context = context_system::instance();
|
||||
foreach (enrol_get_users_courses($USER->id, true, ['id']) as $ec) {
|
||||
if ((int)$ec->id !== SITEID) {
|
||||
$course = get_course($ec->id);
|
||||
$context = context_course::instance($ec->id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require_login($course);
|
||||
|
||||
$PAGE->set_url(new moodle_url('/filter/mediacms/select_media_picker.php', ['courseid' => $course->id]));
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_pagelayout('embedded');
|
||||
$PAGE->set_title('MediaCMS Select Media');
|
||||
|
||||
$typeconfig = lti_get_type_type_config($type->id);
|
||||
|
||||
// Store redirect_path in session — lti_auth.php picks it up after the OIDC roundtrip.
|
||||
$SESSION->mediacms_launch_customparams = 'redirect_path=/lti/select-media/?mode=lms_embed_mode';
|
||||
|
||||
$content = lti_initiate_login($course->id, 0, null, $typeconfig, null, 'MediaCMS Select Media');
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo $content;
|
||||
echo $OUTPUT->footer();
|
||||
@@ -161,16 +161,14 @@ class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menu
|
||||
$courseid = $COURSE->id;
|
||||
}
|
||||
|
||||
// Build the content item URL for LTI Deep Linking.
|
||||
// This URL initiates the LTI Deep Linking flow which allows users
|
||||
// to select content (like videos) from the tool provider.
|
||||
// Build the URL for the student-accessible media picker.
|
||||
// Uses /filter/mediacms/select_media_picker.php instead of the standard
|
||||
// /mod/lti/contentitem.php, which requires moodle/course:manageactivities
|
||||
// and therefore fails for students.
|
||||
$contentitemurl = '';
|
||||
if (!empty($ltitoolid) && $courseid > 0) {
|
||||
$contentitemurl = (new moodle_url('/mod/lti/contentitem.php', [
|
||||
'id' => $ltitoolid,
|
||||
'course' => $courseid,
|
||||
'title' => 'MediaCMS Library',
|
||||
'return_types' => 1 // LTI_DEEPLINKING_RETURN_TYPE_LTI_LINK
|
||||
$contentitemurl = (new moodle_url('/filter/mediacms/select_media_picker.php', [
|
||||
'courseid' => $courseid,
|
||||
]))->out(false);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user