This commit is contained in:
Markos Gogoulos
2026-02-19 13:00:41 +02:00
parent 96755af3b2
commit 21ddd04165
11 changed files with 486 additions and 58 deletions

View File

@@ -0,0 +1,49 @@
<?php
/**
* Hook listener for filter_mediacms navigation hooks (Moodle 4.3+)
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace filter_mediacms;
/**
* Extends the primary (top) navigation bar with a My Media link.
*/
class hook_listener {
/**
* Called by the \core\hook\navigation\primary_extend hook.
* Adds the My Media link to the primary nav bar when placement = 'top'.
*/
public static function extend_primary_navigation(
\core\hook\navigation\primary_extend $hook
): void {
$placement = get_config('filter_mediacms', 'mymedia_placement');
if ($placement !== 'top') {
return;
}
if (!isloggedin() || isguestuser()) {
return;
}
$url = new \moodle_url('/filter/mediacms/my_media.php');
$node = \navigation_node::create(
get_string('mymedia', 'filter_mediacms'),
$url,
\navigation_node::TYPE_CUSTOM,
null,
'mediacms_mymedia',
new \pix_icon('i/media', '')
);
$primarynav = $hook->get_primarynav();
if ($primarynav === null) {
return;
}
$primarynav->add_node($node);
}
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* Hook registrations for filter_mediacms.
* Primary navigation is handled via extend_navigation() in lib.php instead.
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$callbacks = [];

View File

@@ -17,3 +17,13 @@ $string['iframeheight_desc'] = 'Default height for embedded videos (pixels).';
$string['enableautoconvert'] = 'Auto-convert URLs';
$string['enableautoconvert_desc'] = 'Automatically convert MediaCMS URLs (e.g., /view?m=xyz) in text to embedded players.';
$string['privacy:metadata'] = 'The MediaCMS filter does not store any personal data.';
// My Media page.
$string['mymedia'] = 'My Media';
$string['mymedia_placement'] = 'My Media link placement';
$string['mymedia_placement_desc'] = 'Where to display the My Media link in the Moodle interface.';
$string['mymedia_placement_top'] = 'Top navigation';
$string['mymedia_placement_user'] = 'User navigation';
$string['notconfigured'] = 'MediaCMS is not fully configured. Please set the MediaCMS URL and LTI Tool in Site Administration → Plugins → Filters → MediaCMS.';
$string['ltitoolnotfound'] = 'The configured LTI tool could not be found. Please check the MediaCMS filter settings.';
$string['cannotcreatedummyactivity'] = 'Could not create the MediaCMS launcher activity. Please check course permissions.';

View File

@@ -11,65 +11,10 @@ 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, $PAGE, $OUTPUT, $CFG;
/**
* Find first LTI activity for the MediaCMS tool, or create dummy if none exists
*/
function filter_mediacms_get_dummy_activity($courseid, $typeid) {
global $DB;
// Find any existing LTI activity with this tool
$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
LIMIT 1";
$existing = $DB->get_record_sql($sql, ['courseid' => $courseid, 'typeid' => $typeid]);
if ($existing) {
// Ensure it's accessible (fix if created with visible=0)
$cm = get_coursemodule_from_id('lti', $existing->id, 0, false, IGNORE_MISSING);
if ($cm && !$cm->visible) {
set_coursemodule_visible($existing->id, 1);
}
return $existing->id;
}
// No existing activity - create dummy in stealth mode (accessible but completely hidden)
$moduleinfo = new stdClass();
$moduleinfo->course = $courseid;
$moduleinfo->module = $DB->get_field('modules', 'id', ['name' => 'lti']);
$moduleinfo->modulename = 'lti';
$moduleinfo->section = 0;
$moduleinfo->visible = 1; // Accessible to all
$moduleinfo->visibleoncoursepage = 0; // Hidden from course page
$moduleinfo->availability = null; // No restrictions
$moduleinfo->showdescription = 0; // Don't show description
$moduleinfo->name = 'MediaCMS Filter Launcher';
$moduleinfo->intro = ''; // Empty 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));
// Additionally hide from activity navigation by setting it as orphaned
set_coursemodule_visible($result->coursemodule, 1, 0); // visible but not on course page
return $result->coursemodule;
}
require_login();
$mediatoken = required_param('token', PARAM_ALPHANUMEXT);

View File

@@ -0,0 +1,72 @@
<?php
/**
* Navigation callbacks for filter_mediacms
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Add My Media to the global / flat navigation (nav drawer).
* Fires on every page when placement is set to 'top'.
* In Moodle 4.x Boost the nav drawer is opened via the hamburger icon
* and the node appears as a top-level item alongside Home / My courses.
*/
function filter_mediacms_extend_navigation(global_navigation $navigation): void {
$placement = get_config('filter_mediacms', 'mymedia_placement');
if ($placement !== 'top') {
return;
}
if (!isloggedin() || isguestuser()) {
return;
}
$url = new moodle_url('/filter/mediacms/my_media.php');
$node = navigation_node::create(
get_string('mymedia', 'filter_mediacms'),
$url,
navigation_node::TYPE_CUSTOM,
null,
'mediacms_mymedia',
new pix_icon('i/media', '')
);
// showinflatnavigation = true makes it visible in the Boost nav drawer.
$node->showinflatnavigation = true;
$navigation->add_node($node);
}
/**
* Add My Media to the user account / settings navigation.
* Fires when placement is set to 'user'.
* In Moodle 4.x Boost this section is reachable via avatar → Preferences.
*/
function filter_mediacms_extend_navigation_user_settings(
navigation_node $navigation,
stdClass $user,
context_user $usercontext,
stdClass $course,
context_course $coursecontext
): void {
$placement = get_config('filter_mediacms', 'mymedia_placement');
if ($placement !== 'user') {
return;
}
if (!isloggedin() || isguestuser()) {
return;
}
$url = new moodle_url('/filter/mediacms/my_media.php');
$navigation->add(
get_string('mymedia', 'filter_mediacms'),
$url,
navigation_node::TYPE_SETTING,
null,
'mediacms_mymedia',
new pix_icon('i/media', '')
);
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Local helper functions for filter_mediacms
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Find the first LTI activity for the MediaCMS tool in a course, or create a
* hidden dummy one if none exists.
*
* @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
LIMIT 1";
$existing = $DB->get_record_sql($sql, ['courseid' => $courseid, 'typeid' => $typeid]);
if ($existing) {
$cm = get_coursemodule_from_id('lti', $existing->id, 0, false, IGNORE_MISSING);
if ($cm && !$cm->visible) {
set_coursemodule_visible($existing->id, 1);
}
return $existing->id;
}
// Create a stealth dummy activity (accessible but hidden from the course page).
$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 = 0;
$moduleinfo->availability = null;
$moduleinfo->showdescription = 0;
$moduleinfo->name = 'MediaCMS Filter Launcher';
$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));
set_coursemodule_visible($result->coursemodule, 1, 0);
return $result->coursemodule;
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* My Media LTI launch page — runs inside the iframe from my_media.php.
*
* Builds custom_publishdata (enrolled courses + roles) and initiates
* the LTI 1.3 OIDC login flow, outputting the auto-submit form directly.
*
* @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');
require_once($CFG->dirroot . '/course/modlib.php');
require_once(__DIR__ . '/locallib.php');
global $SITE, $DB, $CFG, $USER;
require_login();
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
if (empty($mediacmsurl) || empty($ltitoolid)) {
throw new moodle_exception('notconfigured', 'filter_mediacms');
}
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
if (!$type) {
throw new moodle_exception('ltitoolnotfound', 'filter_mediacms');
}
// Build custom_publishdata: all courses the user is enrolled in + role.
$enrolled_courses = enrol_get_users_courses($USER->id, true, ['id', 'shortname', 'fullname']);
$publish_data = [];
foreach ($enrolled_courses as $enrolled_course) {
if ((int)$enrolled_course->id === SITEID) {
continue;
}
$course_context = context_course::instance($enrolled_course->id);
$roles = get_user_roles($course_context, $USER->id, false);
$role_shortname = 'student';
if (!empty($roles)) {
$role = reset($roles);
$role_shortname = $role->shortname;
}
$publish_data[] = [
'id' => (int)$enrolled_course->id,
'shortname' => $enrolled_course->shortname,
'fullname' => $enrolled_course->fullname,
'role' => $role_shortname,
];
}
$publishdata_b64 = base64_encode(json_encode($publish_data));
try {
$dummy_cmid = filter_mediacms_get_dummy_activity(SITEID, $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);
$instance->instructorcustomparameters = 'publishdata=' . $publishdata_b64;
$instance->name = 'MediaCMS My Media';
$typeconfig = lti_get_type_type_config($type->id);
$content = lti_initiate_login($SITE->id, $dummy_cmid, $instance, $typeconfig, null, $instance->name);
echo $content;

View File

@@ -0,0 +1,38 @@
<?php
/**
* My Media page — renders the Moodle shell with an LTI iframe.
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
global $SITE, $PAGE, $OUTPUT, $USER;
require_login();
$context = context_system::instance();
$PAGE->set_context($context);
$PAGE->set_url(new moodle_url('/filter/mediacms/my_media.php'));
$PAGE->set_course($SITE);
$PAGE->set_pagelayout('mydashboard');
$PAGE->set_title(get_string('mymedia', 'filter_mediacms'));
$PAGE->set_heading(get_string('mymedia', 'filter_mediacms'));
echo $OUTPUT->header();
$attr = [
'id' => 'contentframe',
'src' => (new moodle_url('/filter/mediacms/lti_launch.php'))->out(false),
'width' => '100%',
'height' => '600px',
'allowfullscreen' => 'true',
'allow' => 'autoplay *; fullscreen *; encrypted-media *; camera *; microphone *;',
'style' => 'border:none;display:block;',
];
echo html_writer::tag('iframe', '', $attr);
echo $OUTPUT->footer();

View File

@@ -39,4 +39,16 @@ if ($ADMIN->fulltree) {
0,
$ltioptions
));
// My Media link placement.
$settings->add(new admin_setting_configselect(
'filter_mediacms/mymedia_placement',
get_string('mymedia_placement', 'filter_mediacms'),
get_string('mymedia_placement_desc', 'filter_mediacms'),
'top',
[
'top' => get_string('mymedia_placement_top', 'filter_mediacms'),
'user' => get_string('mymedia_placement_user', 'filter_mediacms'),
]
));
}