. /** * 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 $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 $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 = << $printlink 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; }