.
/**
* Media plugin filtering
*
* This filter will replace any links to a media file with
* a media plugin that plays that media inline
*
* @package filter
* @subpackage mediaplugin
* @copyright 2004 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/filelib.php');
if (!defined('FILTER_MEDIAPLUGIN_VIDEO_WIDTH')) {
/**
* Default media width, some plugins may use automatic sizes or accept resize parameters.
* This can be defined in config.php.
*/
define('FILTER_MEDIAPLUGIN_VIDEO_WIDTH', 400);
}
if (!defined('FILTER_MEDIAPLUGIN_VIDEO_HEIGHT')) {
/**
* Default video height, plugins that know aspect ration
* should calculate it themselves using the FILTER_MEDIAPLUGIN_VIDEO_HEIGHT
* This can be defined in config.php.
*/
define('FILTER_MEDIAPLUGIN_VIDEO_HEIGHT', 300);
}
//TODO: we should use /u modifier in regex, unfortunately it may not work properly on some misconfigured servers, see lib/filter/urltolink/filter.php ...
//TODO: we should migrate to proper config_plugin settings ...
/**
* Automatic media embedding filter class.
*
* It is highly recommended to configure servers to be compatible with our slasharguments,
* otherwise the "?d=600x400" may not work.
*
* @package filter
* @subpackage mediaplugin
* @copyright 2004 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class filter_mediaplugin extends moodle_text_filter {
function filter($text, array $options = array()) {
global $CFG;
if (!is_string($text) or empty($text)) {
// non string data can not be filtered anyway
return $text;
}
if (stripos($text, '') === false) {
// performance shortcut - all regexes below end with the tag,
// if not present nothing can match
return $text;
}
$newtext = $text; // we need to return the original value if regex fails!
// YouTube and Vimeo are great because the files are not served by Moodle server
if (!empty($CFG->filter_mediaplugin_enable_youtube)) {
$search = '/]*href="(https?:\/\/www\.youtube(-nocookie)?\.com)\/watch\?v=([a-z0-9\-_]+)[^"#]*(#d=([\d]{1,4})x([\d]{1,4}))?"[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_youtube_callback', $newtext);
$search = '/]*href="(https?:\/\/www\.youtube(-nocookie)?\.com)\/v\/([a-z0-9\-_]+)[^"#]*(#d=([\d]{1,4})x([\d]{1,4}))?[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_youtube_callback', $newtext);
$search = '/]*href="(https?:\/\/(www\.)?(youtu|y2u)\.be)\/([a-z0-9\-_]+)[^"#]*(#d=([\d]{1,4})x([\d]{1,4}))?"[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_shortened_youtube_callback', $newtext);
$search = '/]*href="(https?:\/\/www\.youtube(-nocookie)?\.com)\/view_play_list\?p=([a-z0-9\-_]+)[^"#]*(#d=([\d]{1,4})x([\d]{1,4}))?[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_youtube_playlist_callback', $newtext);
$search = '/]*href="(https?:\/\/www\.youtube(-nocookie)?\.com)\/p\/([a-z0-9\-_]+)[^"#]*(#d=([\d]{1,4})x([\d]{1,4}))?[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_youtube_playlist_callback', $newtext);
}
if (!empty($CFG->filter_mediaplugin_enable_vimeo)) {
$search = '/]*href="http:\/\/vimeo\.com\/([0-9]+)[^"#]*(#d=([\d]{1,4})x([\d]{1,4}))?[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_vimeo_callback', $newtext);
}
// HTML 5 audio and video tags are the future! If only if vendors decided to use just one audio and video format...
if (!empty($CFG->filter_mediaplugin_enable_html5audio)) {
$search = '/]*href="([^"#\?]+\.(ogg|oga|aac|m4a)([#\?][^"]*)?)"[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_html5audio_callback', $newtext);
}
if (!empty($CFG->filter_mediaplugin_enable_html5video)) {
$search = '/]*href="([^"#\?]+\.(m4v|webm|ogv|mp4)([#\?][^"]*)?)"[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_html5video_callback', $newtext);
}
// Flash stuff
if (!empty($CFG->filter_mediaplugin_enable_mp3)) {
$search = '/]*href="([^"#\?]+\.mp3)"[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_mp3_callback', $newtext);
}
if ((!empty($options['noclean']) or !empty($CFG->allowobjectembed)) and !empty($CFG->filter_mediaplugin_enable_swf)) {
$search = '/]*href="([^"#\?]+\.swf)([#\?]d=([\d]{1,4})x([\d]{1,4}))?"[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_swf_callback', $newtext);
}
if (!empty($CFG->filter_mediaplugin_enable_flv)) {
$search = '/]*href="([^"#\?]+\.(flv|f4v)([#\?][^"]*)?)"[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_flv_callback', $newtext);
}
// The rest of legacy formats - these should not be used if possible
if (!empty($CFG->filter_mediaplugin_enable_wmp)) {
$search = '/]*href="([^"#\?]+\.(wmv|avi))(\?d=([\d]{1,4})x([\d]{1,4}))?"[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_wmp_callback', $newtext);
}
if (!empty($CFG->filter_mediaplugin_enable_qt)) {
// HTML5 filtering may steal mpeg 4 formats
$search = '/]*href="([^"#\?]+\.(mpg|mpeg|mov|mp4|m4v|m4a))(\?d=([\d]{1,4})x([\d]{1,4}))?"[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_qt_callback', $newtext);
}
if (!empty($CFG->filter_mediaplugin_enable_rm)) {
// hopefully nobody is using this any more!!
// rpm is redhat packaging format these days, it is better to prevent these in default installs
$search = '/]*href="([^"#\?]+\.(ra|ram|rm|rv))"[^>]*>([^>]*)<\/a>/is';
$newtext = preg_replace_callback($search, 'filter_mediaplugin_real_callback', $newtext);
}
if (empty($newtext) or $newtext === $text) {
// error or not filtered
unset($newtext);
return $text;
}
return $newtext;
}
}
///===========================
/// utility functions
/**
* Get mimetype of given url, useful for # alternative urls.
*
* @private
* @param string $url
* @return string $mimetype
*/
function filter_mediaplugin_get_mimetype($url) {
$matches = null;
if (preg_match("|^(.*)/[a-z]*file.php(\?file=)?(/[^&\?#]*)|", $url, $matches)) {
// remove the special moodle file serving hacks so that the *file.php is ignored
$url = $matches[1].$matches[3];
} else {
$url = preg_replace('/[#\?].*$/', '', $url);
}
$mimetype = mimeinfo('type', $url);
return $mimetype;
}
/**
* Parse list of alternative URLs
* @param string $url urls separated with '#', size specified as ?d=640x480 or #d=640x480
* @param int $defaultwidth
* @param int $defaultheight
* @return array (urls, width, height)
*/
function filter_mediaplugin_parse_alternatives($url, $defaultwidth = 0, $defaultheight = 0) {
$urls = explode('#', $url);
$width = $defaultwidth;
$height = $defaultheight;
$returnurls = array();
foreach ($urls as $url) {
$matches = null;
if (preg_match('/^d=([\d]{1,4})x([\d]{1,4})$/i', $url, $matches)) { // #d=640x480
$width = $matches[1];
$height = $matches[2];
continue;
}
if (preg_match('/\?d=([\d]{1,4})x([\d]{1,4})$/i', $url, $matches)) { // old style file.ext?d=640x480
$width = $matches[1];
$height = $matches[2];
$url = str_replace($matches[0], '', $url);
}
$url = str_replace('&', '&', $url);
$url = clean_param($url, PARAM_URL);
if (empty($url)) {
continue;
}
$returnurls[] = $url;
}
return array($returnurls, $width, $height);
}
/**
* Should the current tag be ignored in this filter?
* @param string $tag
* @return bool
*/
function filter_mediaplugin_ignore($tag) {
if (preg_match('/class="[^"]*nomediaplugin/i', $tag)) {
return true;
} else {
false;
}
}
///===========================
/// callback filter functions
/**
* Replace audio links with audio tag.
*
* @param array $link
* @return string
*/
function filter_mediaplugin_html5audio_callback(array $link) {
global $CFG;
if (filter_mediaplugin_ignore($link[0])) {
return $link[0];
}
$info = trim($link[4]);
if (empty($info) or strpos($info, 'http') === 0) {
$info = get_string('fallbackaudio', 'filter_mediaplugin');
}
list($urls, $ignorewidth, $ignoredheight) = filter_mediaplugin_parse_alternatives($link[1]);
$fallbackurl = null;
$fallbackmime = null;
$sources = array();
$fallbacklink = null;
foreach ($urls as $url) {
$mimetype = filter_mediaplugin_get_mimetype($url);
if (strpos($mimetype, 'audio/') !== 0) {
continue;
}
$sources[] = html_writer::tag('source', '', array('src' => $url, 'type' => $mimetype));
if ($fallbacklink === null) {
$fallbacklink = html_writer::link($url.'#', $info); // the extra '#' prevents linking in mp3 filter below
}
if ($fallbackurl === null) {
if ($mimetype === 'audio/mp3' or $mimetype === 'audio/aac') {
$fallbackurl = str_replace('&', '&', $url);
$fallbackmime = $mimetype;
}
}
}
if (!$sources) {
return $link[0];
}
if ($fallbackmime !== null) {
// fallback to quicktime
$fallback = <<
$fallbacklink
OET;
} else {
$fallback = $fallbacklink;
}
$sources = implode("\n", $sources);
$title = s($info);
// audio players are supposed to be inline elements
$output = <<
$sources
$fallback
OET;
return $output;
}
/**
* Replace ogg video links with video tag.
*
* Please note this is not going to work in all browsers,
* it is also not xhtml strict.
*
* @param array $link
* @return string
*/
function filter_mediaplugin_html5video_callback(array $link) {
if (filter_mediaplugin_ignore($link[0])) {
return $link[0];
}
$info = trim($link[4]);
if (empty($info) or strpos($info, 'http') === 0) {
$info = get_string('fallbackvideo', 'filter_mediaplugin');
}
list($urls, $width, $height) = filter_mediaplugin_parse_alternatives($link[1], FILTER_MEDIAPLUGIN_VIDEO_WIDTH, 0);
$fallbackurl = null;
$fallbackmime = null;
$sources = array();
$fallbacklink = null;
foreach ($urls as $url) {
$mimetype = filter_mediaplugin_get_mimetype($url);
if (strpos($mimetype, 'video/') !== 0) {
continue;
}
$source = html_writer::tag('source', '', array('src' => $url, 'type' => $mimetype));
if ($mimetype === 'video/mp4') {
// better add m4v as first source, it might be a bit more compatible with problematic browsers
array_unshift($sources, $source);
} else {
$sources[] = $source;
}
if ($fallbacklink === null) {
$fallbacklink = html_writer::link($url.'#', $info); // the extra '#' prevents linking in mp3 filter below
}
if ($fallbackurl === null) {
if ($mimetype === 'video/mp4') {
$fallbackurl = str_replace('&', '&', $url);
$fallbackmime = $mimetype;
}
}
}
if (!$sources) {
return $link[0];
}
if ($fallbackmime !== null) {
$qtheight = ($height == 0) ? FILTER_MEDIAPLUGIN_VIDEO_HEIGHT : ($height + 15);
// fallback to quicktime
$fallback = <<
$fallbacklink
OET;
} else {
$fallback = $fallbacklink;
}
$sources = implode("\n", $sources);
$title = s($info);
if (empty($height)) {
// automatic height
$size = "width=\"$width\"";
} else {
$size = "width=\"$width\" height=\"$height\"";
}
$output = <<
OET;
return $output;
}
/**
* Replace mp3 links with small audio player.
*
* @param $link
* @return string
*/
function filter_mediaplugin_mp3_callback($link) {
static $count = 0;
if (filter_mediaplugin_ignore($link[0])) {
return $link[0];
}
$count++;
$id = 'filter_mp3_'.time().'_'.$count; //we need something unique because it might be stored in text cache
$url = $link[1];
$rawurl = str_replace('&', '&', $url);
$info = trim($link[2]);
if (empty($info) or strpos($info, 'http') === 0) {
$info = get_string('mp3audio', 'filter_mediaplugin');
}
$printlink = html_writer::link($rawurl, $info, array('class'=>'mediafallbacklink'));
//note: when flash or javascript not available only the $printlink is displayed,
// audio players are supposed to be inline elements
$output = html_writer::tag('span', $printlink, array('id'=>$id, 'class'=>'mediaplugin mediaplugin_mp3'));
$output .= html_writer::script(js_writer::function_call('M.util.add_audio_player', array($id, $rawurl, true))); // we can not use standard JS init because this may be cached
return $output;
}
/**
* Replace swf links with embedded flash objects.
*
* Please note this is not a secure and is recommended to be disabled on production systems.
*
* @deprecated
* @param $link
* @return string
*/
function filter_mediaplugin_swf_callback($link) {
if (filter_mediaplugin_ignore($link[0])) {
return $link[0];
}
$width = empty($link[3]) ? FILTER_MEDIAPLUGIN_VIDEO_WIDTH : $link[3];
$height = empty($link[4]) ? FILTER_MEDIAPLUGIN_VIDEO_HEIGHT : $link[4];
$url = $link[1];
$rawurl = str_replace('&', '&', $url);
$info = trim($link[5]);
if (empty($info) or strpos($info, 'http') === 0) {
$info = get_string('flashanimation', 'filter_mediaplugin');
}
$printlink = html_writer::link($rawurl, $info, array('class'=>'mediafallbacklink'));
$output = <<
OET;
return $output;
}
/**
* Replace flv links with flow player.
*
* @param $link
* @return string
*/
function filter_mediaplugin_flv_callback($link) {
static $count = 0;
if (filter_mediaplugin_ignore($link[0])) {
return $link[0];
}
$count++;
$id = 'filter_flv_'.time().'_'.$count; //we need something unique because it might be stored in text cache
list($urls, $width, $height) = filter_mediaplugin_parse_alternatives($link[1], 0, 0);
$autosize = false;
if (!$width and !$height) {
$width = FILTER_MEDIAPLUGIN_VIDEO_WIDTH;
$height = FILTER_MEDIAPLUGIN_VIDEO_HEIGHT;
$autosize = true;
}
$flashurl = null;
$sources = array();
foreach ($urls as $url) {
$mimetype = filter_mediaplugin_get_mimetype($url);
if (strpos($mimetype, 'video/') !== 0) {
continue;
}
$source = html_writer::tag('source', '', array('src' => $url, 'type' => $mimetype));
if ($mimetype === 'video/mp4') {
// better add m4v as first source, it might be a bit more compatible with problematic browsers
array_unshift($sources, $source);
} else {
$sources[] = $source;
}
if ($flashurl === null) {
$flashurl = $url;
}
}
if (!$sources) {
return $link[0];
}
$info = trim($link[4]);
if (empty($info) or strpos($info, 'http') === 0) {
$info = get_string('fallbackvideo', 'filter_mediaplugin');
}
$printlink = html_writer::link($flashurl.'#', $info, array('class'=>'mediafallbacklink')); // the '#' prevents the QT filter
$title = s($info);
if (count($sources) > 1) {
$sources = implode("\n", $sources);
// html 5 fallback
$printlink = <<
$sources
$printlink
OET;
}
// note: no need to print "this is flv link" because it is printed automatically if JS or Flash not available
$output = html_writer::tag('span', $printlink, array('id'=>$id, 'class'=>'mediaplugin mediaplugin_flv'));
$output .= html_writer::script(js_writer::function_call('M.util.add_video_player', array($id, rawurlencode($flashurl), $width, $height, $autosize))); // we can not use standard JS init because this may be cached
return $output;
}
/**
* Replace real media links with real player.
*
* Note: hopefully nobody is using this obsolete format any more.
*
* @deprectated
* @param $link
* @return string
*/
function filter_mediaplugin_real_callback($link) {
if (filter_mediaplugin_ignore($link[0])) {
return $link[0];
}
$url = $link[1];
$rawurl = str_replace('&', '&', $url);
//Note: the size is hardcoded intentionally because this does not work anyway!
$width = FILTER_MEDIAPLUGIN_VIDEO_WIDTH;
$height = FILTER_MEDIAPLUGIN_VIDEO_HEIGHT;
$info = trim($link[3]);
if (empty($info) or strpos($info, 'http') === 0) {
$info = get_string('fallbackvideo', 'filter_mediaplugin');
}
$printlink = html_writer::link($rawurl, $info, array('class'=>'mediafallbacklink'));
return <<
$printlink
OET;
}
/**
* Change links to YouTube into embedded YouTube videos
*
* Note: resizing via url is not supported, user can click the fullscreen button instead
*
* @param $link
* @return string
*/
function filter_mediaplugin_youtube_callback($link) {
global $CFG;
if (filter_mediaplugin_ignore($link[0])) {
return $link[0];
}
$site = $link[1];
$videoid = $link[3];
$info = trim($link[7]);
if (empty($info) or strpos($info, 'http') === 0) {
$info = get_string('siteyoutube', 'filter_mediaplugin');
}
$info = s($info);
$width = empty($link[5]) ? FILTER_MEDIAPLUGIN_VIDEO_WIDTH : $link[5];
$height = empty($link[6]) ? FILTER_MEDIAPLUGIN_VIDEO_HEIGHT : $link[6];
if (empty($CFG->xmlstrictheaders)) {
return <<
OET;
}
//NOTE: we can not use any link fallback because it breaks built-in player on iOS devices
$output = <<
OET;
return $output;
}
/**
* Change YouTube playlist into embedded YouTube playlist videos
*
* Note: resizing via url is not supported, user can click the fullscreen button instead
*
* @param $link
* @return string
*/
function filter_mediaplugin_youtube_playlist_callback($link) {
global $CFG;
if (filter_mediaplugin_ignore($link[0])) {
return $link[0];
}
$site = $link[1];
$playlist = $link[3];
$info = trim($link[7]);
if (empty($info) or strpos($info, 'http') === 0) {
$info = get_string('siteyoutube', 'filter_mediaplugin');
}
$printlink = html_writer::link("$site/view_play_list\?p=$playlist", $info, array('class'=>'mediafallbacklink'));
$info = s($info);
$width = empty($link[5]) ? FILTER_MEDIAPLUGIN_VIDEO_WIDTH : $link[5];
$height = empty($link[6]) ? FILTER_MEDIAPLUGIN_VIDEO_HEIGHT : $link[6];
// TODO: iframe HTML 5 video not implemented and object does work on iOS devices
$output = <<
$printlink
OET;
return $output;
}
/**
* Change shortened links to YouTube into embedded YouTube videos
*
* @param $link
* @return string
*/
function filter_mediaplugin_shortened_youtube_callback($link) {
$newlink = array($link[0], 'https://www.youtube.com','',$link[4],'',$link[6],$link[7],$link[8]);
return filter_mediaplugin_youtube_callback($newlink);
}
/**
* Change links to Vimeo into embedded Vimeo videos
*
* @param $link
* @return string
*/
function filter_mediaplugin_vimeo_callback($link) {
global $CFG;
if (filter_mediaplugin_ignore($link[0])) {
return $link[0];
}
$videoid = $link[1];
$info = s(strip_tags($link[5]));
//Note: resizing via url is not supported, user can click the fullscreen button instead
// iframe embedding is not xhtml strict but it is the only option that seems to work on most devices
$width = empty($link[3]) ? FILTER_MEDIAPLUGIN_VIDEO_WIDTH : $link[3];
$height = empty($link[4]) ? FILTER_MEDIAPLUGIN_VIDEO_HEIGHT : $link[4];
$output = <<
OET;
return $output;
}
/**
* Embed video using window media player if available
*
* This does not work much outside of IE, hopefully not many ppl use it these days.
*
* @param $link
* @return string
*/
function filter_mediaplugin_wmp_callback($link) {
if (filter_mediaplugin_ignore($link[0])) {
return $link[0];
}
$url = $link[1];
$rawurl = str_replace('&', '&', $url);
$info = trim($link[6]);
if (empty($info) or strpos($info, 'http') === 0) {
$info = get_string('fallbackvideo', 'filter_mediaplugin');
}
$printlink = html_writer::link($rawurl, $info, array('class'=>'mediafallbacklink'));
if (empty($link[4]) or empty($link[5])) {
$mpsize = '';
$size = 'width="'.FILTER_MEDIAPLUGIN_VIDEO_WIDTH.'" height="'.(FILTER_MEDIAPLUGIN_VIDEO_HEIGHT+64).'"';
$autosize = 'true';
} else {
$size = 'width="'.$link[4].'" height="'.($link[5] + 15).'"';
$mpsize = 'width="'.$link[4].'" height="'.($link[5] + 64).'"';
$autosize = 'false';
}
$mimetype = filter_mediaplugin_get_mimetype($url);
return <<
$printlink
OET;
}
/**
* Replace quicktime links with quicktime player.
*
* You need to install a quicktime player, it is not available for all browsers+OS combinations.
*
* @param $link
* @return string
*/
function filter_mediaplugin_qt_callback($link) {
if (filter_mediaplugin_ignore($link[0])) {
return $link[0];
}
$url = $link[1];
$rawurl = str_replace('&', '&', $url);
$info = trim($link[6]);
if (empty($info) or strpos($info, 'http') === 0) {
$info = get_string('fallbackvideo', 'filter_mediaplugin');
}
$printlink = html_writer::link($rawurl, $info, array('class'=>'mediafallbacklink'));
if (empty($link[4]) or empty($link[5])) {
$size = 'width="'.FILTER_MEDIAPLUGIN_VIDEO_WIDTH.'" height="'.(FILTER_MEDIAPLUGIN_VIDEO_HEIGHT+15).'"';
} else {
$size = 'width="'.$link[4].'" height="'.($link[5]+15).'"';
}
$mimetype = filter_mediaplugin_get_mimetype($url);
// this is the safest fallback for incomplete or missing browser support for this format
return <<
$printlink
OET;
}