Skip to content
Snippets Groups Projects
Unverified Commit 6e80d6f9 authored by Nina Herrmann's avatar Nina Herrmann Committed by GitHub
Browse files

Merge pull request #44 from justusdieckmann/filter-rewrite

Rewrite filter to also accept <a href="..."> and <video src="...">
parents 48c2bf80 e09b654c
No related branches found
No related tags found
No related merge requests found
...@@ -20,39 +20,68 @@ ...@@ -20,39 +20,68 @@
* This filter will replace any links to opencast videos with the selected player from opencast. * This filter will replace any links to opencast videos with the selected player from opencast.
* *
* @package filter_opencast * @package filter_opencast
* @copyright 2018 Tamara Gunkel * @copyright 2024 Justus Dieckmann and Tamara Gunkel, University of Münster
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/ */
use mod_opencast\local\paella_transform; use mod_opencast\local\paella_transform;
/** /**
* Automatic opencast videos filter class. * Automatic opencast videos filter class.
* *
* @package filter_opencast * @package filter_opencast
* @copyright 2018 Tamara Gunkel * @copyright 2024 Justus Dieckmann and Tamara Gunkel, University of Münster
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/ */
class filter_opencast extends moodle_text_filter { class filter_opencast extends moodle_text_filter {
/**
* Get the content of the attribute $attributename from $tag.
* @param string $tag HTML-Tag.
* @param string $attributename Name of the attribute.
* @param string $attributecontentregex Regex of what the content of the attribute might be.
* @return string|null The content of the attribute or null, if it doesn't exist.
*/
private static function get_attribute(string $tag, string $attributename, string $attributecontentregex = '.*'): string|null {
$pattern = "/$attributename=(['\"])($attributecontentregex)\\1/";
preg_match($pattern, $tag, $matches);
return $matches[2] ?? null;
}
/**
* Test whether $url matches one of the episodeurls.
* @param string $url The url to test.
* @param array $episodeurls array of [ocinstanceid, episoderegex, baseurl].
* @return array|null [ocinstanceid, episodeid] or null if there are no matches.
*/
private static function test_url(string $url, array $episodeurls) : array|null {
foreach ($episodeurls as [$ocinstanceid, $episoderegex, $baseurl]) {
if (preg_match_all($episoderegex, $url, $matches)) {
return [$ocinstanceid, $matches[1][0]];
}
}
return null;
}
/** /**
* Replaces Opencast videos embedded in <video> tags by the paella player. * Replaces Opencast videos embedded in <video> tags by the paella player.
*
* @param string $text * @param string $text
* @param array $options * @param array $options
* @return array|mixed|string|string[]|null * @return array|mixed|string|string[]|null
* @throws dml_exception
*/ */
public function filter($text, array $options = []) { public function filter($text, array $options = []) {
global $PAGE, $OUTPUT;
$i = 0;
if (stripos($text, '</video>') === false) { if (preg_match('</(a|video)>', $text) !== 1) {
// Performance shortcut - if there is no </video> tag, nothing can match. // Performance shortcut - if there are no </video> or </a> tags, nothing can match.
return $text; return $text;
} }
foreach (\tool_opencast\local\settings_api::get_ocinstances() as $ocinstance) { // 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();
$occurrences = [];
foreach ($ocinstances as $ocinstance) {
$episodeurls = get_config('filter_opencast', 'episodeurl_' . $ocinstance->id); $episodeurls = get_config('filter_opencast', 'episodeurl_' . $ocinstance->id);
if (!$episodeurls) { if (!$episodeurls) {
...@@ -63,79 +92,131 @@ class filter_opencast extends moodle_text_filter { ...@@ -63,79 +92,131 @@ class filter_opencast extends moodle_text_filter {
$episodeurl = trim($episodeurl); $episodeurl = trim($episodeurl);
$urlparts = parse_url($episodeurl); $urlparts = parse_url($episodeurl);
if (!isset($urlparts['scheme']) || !isset($urlparts['host'])) {
continue;
}
$baseurl = $urlparts['scheme'] . '://' . $urlparts['host']; $baseurl = $urlparts['scheme'] . '://' . $urlparts['host'];
if (isset($urlparts['port'])) { if (isset($urlparts['port'])) {
$baseurl .= ':' . $urlparts['port']; $baseurl .= ':' . $urlparts['port'];
} }
if (empty($episodeurl) || stripos($text, $baseurl) === false) { if (self::str_contains($text, $baseurl)) {
continue; $episoderegex = "/" . preg_quote($episodeurl, "/") . "/";
$episoderegex = preg_replace('/\\\\\[EPISODEID\\\]/', '([0-9a-zA-Z\-]+)', $episoderegex);
$occurrences[] = [$ocinstance->id, $episoderegex, $baseurl];
}
}
} }
// Looking for tags. if (empty($occurrences)) {
$matches = preg_split('/(<[^>]*>)/i', $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); return $text;
if ($matches) {
$renderer = $PAGE->get_renderer('filter_opencast');
$video = false;
$width = false;
$height = false;
foreach ($matches as $match) {
if (empty(trim($match))) {
continue;
} }
// Check if the match is a video tag.
if (substr($match, 0, 6) === "<video") { // Second section: splitting the text into tags (and stuff between tags), and search for relevant urls in <a> and <video>.
$video = true; $matches = preg_split('/(<[^>]*>)/i', $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
preg_match('/width="([0-9]+)"/', $match, $width); if (!$matches) {
preg_match('/height="([0-9]+)"/', $match, $height); return $text;
$width = $width ? $width[1] : $width;
$height = $height ? $height[1] : $height;
} else if ($video) {
$video = false;
if (substr($match, 0, 7) === "<source") {
// Check if video is from opencast.
if (strpos($match, $baseurl) === false) {
$width = $height = false;
continue;
} }
// Extract url. $i = 0;
preg_match_all('/<source[^>]+src=([\'"])(?<src>.+?)\1[^>]*>/i', $match, $result); $newtext = '';
// Change url for loading the (Paella) Player. $episode = null;
$link = $result['src'][0]; $currenttag = null;
$texttoreplace = '';
$width = null;
$height = null;
// Get episode id from link. // We go through the complete text and transfer it match by match to $newtext.
$episoderegex = "/" . preg_quote($episodeurl, "/") . "/"; // While we are going through interesting tags, $currenttag is set to 'video' or 'a' respectively.
$episoderegex = preg_replace('/\\\\\[EPISODEID\\\]/', '([0-9a-zA-Z\-]+)', $episoderegex); // During that time, the matches are transferred to $texttoreplace instead. When we find the matching closing tag,
$nummatches = preg_match_all($episoderegex, $link, $episodeid); // ... we add either $texttoreplace to $newtext, or the html for the video player, if we found a matching opencast url.
foreach ($matches as $match) {
if ($currenttag) {
$texttoreplace .= $match;
if (self::str_starts_with($match, "</$currenttag")) {
$replacement = null;
if ($episode) {
$replacement = $this->render_player($episode[0], $episode[1], $i++, $width, $height);
}
if ($replacement) {
$newtext .= $replacement;
} else {
$newtext .= $texttoreplace;
}
$episode = null;
$width = null;
$height = null;
$texttoreplace = null;
$currenttag = null;
} else if (!$episode && $currenttag === 'video' && preg_match('/^<source\s/', $match)) {
$src = self::get_attribute($match, 'src');
if ($src) {
$episode = self::test_url($src, $occurrences);
}
}
} else {
if (preg_match('/^<video[>\s]/', $match)) {
$currenttag = 'video';
$width = self::get_attribute($match, 'width', '[0-9]+');
$height = self::get_attribute($match, 'height', '[0-9]+');
$src = self::get_attribute($match, 'src');
if ($src) {
$episode = self::test_url($src, $occurrences);
}
} else if (preg_match('/^<a\s/', $match)) {
$src = self::get_attribute($match, 'href');
if ($src) {
$episode = self::test_url($src, $occurrences);
// Only set currenttag if there is a recognized url,
// ... so that nested <a> or <video> tags can be matched otherwise.
if ($episode) {
$currenttag = 'a';
}
}
}
if ($currenttag) {
$texttoreplace .= $match;
} else {
$newtext .= $match;
}
}
}
if (!$nummatches) { return $newtext;
$width = $height = false;
continue;
} }
$data = paella_transform::get_paella_data_json($ocinstance->id, $episodeid[1][0]); /**
* Render HTML for embedding video player.
* @param int $ocinstanceid Id of ocinstance.
* @param string $episodeid Id opencast episode.
* @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): string|null {
global $OUTPUT, $PAGE;
$data = paella_transform::get_paella_data_json($ocinstanceid, $episodeid);
if (!$data) { if (!$data) {
continue; return null;
} }
// Collect the needed data being submitted to the template. // Collect the needed data being submitted to the template.
$mustachedata = new stdClass(); $mustachedata = new stdClass();
$mustachedata->playerid = 'ocplayer_' . $i++; $mustachedata->playerid = 'ocplayer_' . $playerid;
$mustachedata->configurl = (new moodle_url(get_config('filter_opencast', 'configurl_' . $ocinstance->id)))->out(false); $mustachedata->configurl =
$mustachedata->themeurl = (new moodle_url(get_config('mod_opencast', 'themeurl_' . $ocinstance->id)))->out(false); (new moodle_url(get_config('filter_opencast', 'configurl_' . $ocinstanceid)))->out(false);
if (strpos($mustachedata->configurl, 'http') === false) { $mustachedata->themeurl =
$mustachedata->configurl = (new moodle_url($mustachedata->configurl))->out(); (new moodle_url(get_config('mod_opencast', 'themeurl_' . $ocinstanceid)))->out(false);
}
$mustachedata->data = json_encode($data); $mustachedata->data = json_encode($data);
$mustachedata->width = $width; $mustachedata->width = $width;
$mustachedata->height = $height; $mustachedata->height = $height;
$mustachedata->modplayerpath = (new moodle_url('/mod/opencast/player.html'))->out(); $mustachedata->modplayerpath = (new moodle_url('/mod/opencast/player.html'))->out(false);
if (isset($data['streams'])) { if (isset($data['streams'])) {
if (count($data['streams']) === 1) { if (count($data['streams']) === 1) {
...@@ -151,32 +232,34 @@ class filter_opencast extends moodle_text_filter { ...@@ -151,32 +232,34 @@ class filter_opencast extends moodle_text_filter {
$mustachedata->width = $height * ($res['w'] / $res['h']); $mustachedata->width = $height * ($res['w'] / $res['h']);
} }
} }
} else {
if ($width && $height) {
$mustachedata->width = $width;
$mustachedata->height = $height;
}
} }
$newtext = $renderer->render_player($mustachedata); $renderer = $PAGE->get_renderer('filter_opencast');
return $renderer->render_player($mustachedata);
} else { } else {
$newtext = $OUTPUT->render(new \core\output\notification( return $OUTPUT->render(new \core\output\notification(
get_string('erroremptystreamsources', 'mod_opencast'), get_string('erroremptystreamsources', 'mod_opencast'),
\core\output\notification::NOTIFY_ERROR \core\output\notification::NOTIFY_ERROR
)); ));
} }
// Replace video tag.
$text = preg_replace('/<video(?:(?!<\/video>).)*?' . preg_quote($match, '/') . '.*?<\/video>/s',
$newtext, $text, 1);
}
$width = $height = false;
}
}
}
} }
/**
* Polyfill for str_contains for PHP 7.
* @param string $haystack
* @param string $needle
* @return bool
*/
private static function str_contains(string $haystack, string $needle): bool {
return strpos($haystack, $needle) !== false;
} }
// Return the same string except processed by the above. /**
return $text; * Polyfill for str_starts_with for PHP 7.
* @param string $haystack
* @param string $needle
* @return bool
*/
private static function str_starts_with(string $haystack, string $needle): bool {
return strpos($haystack, $needle) === 0;
} }
} }
<?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/>.
/**
* Testcases for the opencast filter.
*
* @package filter_opencast
* @copyright 2024 Justus Dieckmann, University of Münster.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace filter_opencast;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/filter/opencast/tests/testable_filter.php');
/**
* Testcases for the opencast filter.
*
* @package filter_opencast
* @copyright 2024 Justus Dieckmann, University of Münster.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @group filter_opencast
*/
class replacement_test extends \advanced_testcase {
public function setUp(): void {
$this->resetAfterTest();
set_config('episodeurl_1', "http://localhost:8080/play/[EPISODEID]\nhttps://stable.opencast.de/play/[EPISODEID]",
'filter_opencast');
}
/**
* Actual test function.
*
* @dataProvider replacement_provider
* @covers \filter_opencast
* @param string $input input for filter
* @param string $output expected filter output.
*/
public function test_replacement($input, $output) {
$filter = new testable_filter(\context_system::instance(), []);
$this->assertEquals($output, $filter->filter($input));
}
/**
* Provides test cases.
*/
public function replacement_provider() {
return [
[
' <p> hello </p> <video src="http://localhost:8080/play/f78ac136-8252-4b8e-bfea-4786c6993f03"> hello </video>',
' <p> hello </p> <oc-video episode="f78ac136-8252-4b8e-bfea-4786c6993f03"/>'
],
[
'<video src="https://somethingother.com"></video><video>
<source
src="https://stable.opencast.de/play/370e5bef-1d59-4440-858a-4df62e767dfc">
</video>',
'<video src="https://somethingother.com"></video><oc-video episode="370e5bef-1d59-4440-858a-4df62e767dfc"/>'
],
[
'<video
autoplay loopdiloop
src="http://localhost:8080/play/f9e7b289-c8be-462f-80bf-d1f493c6ed55"></video>',
'<oc-video episode="f9e7b289-c8be-462f-80bf-d1f493c6ed55"/>'
],
[
'begin <video>
<source src="https://somethingother.de/play/4380f73a-47a6-41c6-b854-ec0fa9d0261b">
<source
src="https://stable.opencast.de/play/2e0ca3bb-df8e-4913-9380-c925efaf5ac2">
</video> end',
'begin <oc-video episode="2e0ca3bb-df8e-4913-9380-c925efaf5ac2"/> end'
],
[
'and a link <a href="https://www.google.com">link</a>
<a href="http://localhost:8080/play/09b9d154-c849-429d-adea-3df4f76429b6">look, a video!</a>',
'and a link <a href="https://www.google.com">link</a>
<oc-video episode="09b9d154-c849-429d-adea-3df4f76429b6"/>'
],
[
'and now two <a
href="http://localhost:8080/play/64b085e9-0142-4a10-a08e-3dbce055e740">look, a video!</a>
<video src="http://localhost:8080/play/329885fe-d18e-4c6b-a896-dbc66463a6b2"></video>.',
'and now two <oc-video episode="64b085e9-0142-4a10-a08e-3dbce055e740"/>
<oc-video episode="329885fe-d18e-4c6b-a896-dbc66463a6b2"/>.'
],
];
}
}
<?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/>.
/**
* Testable opencast filter.
*
* @package filter_opencast
* @copyright 2024 Justus Dieckmann, University of Münster.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace filter_opencast;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/filter/opencast/filter.php');
/**
* Testable opencast filter.
*
* @package filter_opencast
* @copyright 2024 Justus Dieckmann, University of Münster.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_filter extends \filter_opencast {
/**
* Render a simple
* @param int $ocinstanceid Id of ocinstance.
* @param string $episodeid Id opencast episode.
* @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): string|null {
return '<oc-video episode="'. $episodeid . '"/>';
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment