MDL-69240 tool_moodlenet: Clean MoodleNet profile field
[moodle.git] / filter / mediaplugin / filter.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  *  Media plugin filtering
19  *
20  *  This filter will replace any links to a media file with
21  *  a media plugin that plays that media inline
22  *
23  * @package    filter
24  * @subpackage mediaplugin
25  * @copyright  2004 onwards Martin Dougiamas  {@link http://moodle.com}
26  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27  */
29 defined('MOODLE_INTERNAL') || die();
31 /**
32  * Automatic media embedding filter class.
33  *
34  * It is highly recommended to configure servers to be compatible with our slasharguments,
35  * otherwise the "?d=600x400" may not work.
36  *
37  * @package    filter
38  * @subpackage mediaplugin
39  * @copyright  2004 onwards Martin Dougiamas  {@link http://moodle.com}
40  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41  */
42 class filter_mediaplugin extends moodle_text_filter {
43     /** @var bool True if currently filtering trusted text */
44     private $trusted;
46     /**
47      * Setup page with filter requirements and other prepare stuff.
48      *
49      * @param moodle_page $page The page we are going to add requirements to.
50      * @param context $context The context which contents are going to be filtered.
51      */
52     public function setup($page, $context) {
53         // This only requires execution once per request.
54         static $jsinitialised = false;
55         if ($jsinitialised) {
56             return;
57         }
58         $jsinitialised = true;
60         // Set up the media manager so that media plugins requiring JS are initialised.
61         $mediamanager = core_media_manager::instance($page);
62     }
64     public function filter($text, array $options = array()) {
65         global $CFG, $PAGE;
67         if (!is_string($text) or empty($text)) {
68             // non string data can not be filtered anyway
69             return $text;
70         }
72         if (stripos($text, '</a>') === false && stripos($text, '</video>') === false && stripos($text, '</audio>') === false) {
73             // Performance shortcut - if there are no </a>, </video> or </audio> tags, nothing can match.
74             return $text;
75         }
77         // Check SWF permissions.
78         $this->trusted = !empty($options['noclean']) or !empty($CFG->allowobjectembed);
80         // Looking for tags.
81         $matches = preg_split('/(<[^>]*>)/i', $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
83         if (!$matches) {
84             return $text;
85         }
87         // Regex to find media extensions in an <a> tag.
88         $embedmarkers = core_media_manager::instance()->get_embeddable_markers();
89         $re = '~<a\s[^>]*href="([^"]*(?:' .  $embedmarkers . ')[^"]*)"[^>]*>([^>]*)</a>~is';
91         $newtext = '';
92         $validtag = '';
93         $tagname = '';
94         $sizeofmatches = count($matches);
96         // We iterate through the given string to find valid <a> tags
97         // and build them so that the callback function can check it for
98         // embedded content. Then we rebuild the string.
99         foreach ($matches as $idx => $tag) {
100             if (preg_match('|</'.$tagname.'>|', $tag) && !empty($validtag)) {
101                 $validtag .= $tag;
103                 // Given we now have a valid <a> tag to process it's time for
104                 // ReDoS protection. Stop processing if a word is too large.
105                 if (strlen($validtag) < 4096) {
106                     if ($tagname === 'a') {
107                         $processed = preg_replace_callback($re, array($this, 'callback'), $validtag);
108                     } else {
109                         // For audio and video tags we just process them without precheck for embeddable markers.
110                         $processed = $this->process_media_tag($validtag);
111                     }
112                 }
113                 // Rebuilding the string with our new processed text.
114                 $newtext .= !empty($processed) ? $processed : $validtag;
115                 // Wipe it so we can catch any more instances to filter.
116                 $validtag = '';
117                 $processed = '';
118             } else if (preg_match('/<(a|video|audio)\s[^>]*/', $tag, $tagmatches) && $sizeofmatches > 1 &&
119                     (empty($validtag) || $tagname === strtolower($tagmatches[1]))) {
120                 // Looking for a starting tag. Ignore tags embedded into each other.
121                 $validtag = $tag;
122                 $tagname = strtolower($tagmatches[1]);
123             } else {
124                 // If we have a validtag add to that to process later,
125                 // else add straight onto our newtext string.
126                 if (!empty($validtag)) {
127                     $validtag .= $tag;
128                 } else {
129                     $newtext .= $tag;
130                 }
131             }
132         }
134         // Return the same string except processed by the above.
135         return $newtext;
136     }
138     /**
139      * Replace link with embedded content, if supported.
140      *
141      * @param array $matches
142      * @return string
143      */
144     private function callback(array $matches) {
145         $mediamanager = core_media_manager::instance();
147         global $CFG, $PAGE;
148         // Check if we ignore it.
149         if (preg_match('/class="[^"]*nomediaplugin/i', $matches[0])) {
150             return $matches[0];
151         }
153         // Get name.
154         $name = trim($matches[2]);
155         if (empty($name) or strpos($name, 'http') === 0) {
156             $name = ''; // Use default name.
157         }
159         // Split provided URL into alternatives.
160         $urls = $mediamanager->split_alternatives($matches[1], $width, $height);
162         $options = [core_media_manager::OPTION_ORIGINAL_TEXT => $matches[0]];
163         return $this->embed_alternatives($urls, $name, $width, $height, $options);
164     }
166     /**
167      * Renders media files (audio or video) using suitable embedded player.
168      *
169      * Wrapper for {@link core_media_manager::embed_alternatives()}
170      *
171      * @param array $urls Array of moodle_url to media files
172      * @param string $name Optional user-readable name to display in download link
173      * @param int $width Width in pixels (optional)
174      * @param int $height Height in pixels (optional)
175      * @param array $options Array of key/value pairs
176      * @return string HTML content of embed
177      */
178     protected function embed_alternatives($urls, $name, $width, $height, $options) {
180         // Allow SWF (or not).
181         if ($this->trusted) {
182             $options[core_media_manager::OPTION_TRUSTED] = true;
183         }
185         // We could test whether embed is possible using can_embed, but to save
186         // time, let's just embed it with the 'fallback to blank' option which
187         // does most of the same stuff anyhow.
188         $options[core_media_manager::OPTION_FALLBACK_TO_BLANK] = true;
190         // NOTE: Options are not passed through from filter because the 'embed'
191         // code does not recognise filter options (it's a different kind of
192         // option-space) as it can be used in non-filter situations.
193         $result = core_media_manager::instance()->embed_alternatives($urls, $name, $width, $height, $options);
195         // If something was embedded, return it, otherwise return original.
196         if ($result !== '') {
197             return $result;
198         } else {
199             return $options[core_media_manager::OPTION_ORIGINAL_TEXT];
200         }
201     }
203     /**
204      * Replaces <video> or <audio> tag with processed contents
205      *
206      * @param string $fulltext complete HTML snipped "<video ...>...</video>" or "<audio ...>....</audio>"
207      * @return string
208      */
209     protected function process_media_tag($fulltext) {
210         // Check if we ignore it.
211         if (preg_match('/^<[^>]*class="[^"]*nomediaplugin/im', $fulltext)) {
212             return $fulltext;
213         }
215         // Find all sources both as <video src=""> and as embedded <source> tags.
216         $urls = [];
217         if (preg_match('/^<[^>]*\bsrc="(.*?)"/im', $fulltext, $matches)) {
218             $urls[] = new moodle_url($matches[1]);
219         }
220         if (preg_match_all('/<source\b[^>]*\bsrc="(.*?)"/im', $fulltext, $matches)) {
221             foreach ($matches[1] as $url) {
222                 $urls[] = new moodle_url($url);
223             }
224         }
225         // Extract width/height/title attributes and call embed_alternatives to find a suitable media player.
226         if ($urls) {
227             $options = [core_media_manager::OPTION_ORIGINAL_TEXT => $fulltext];
228             $width = core_media_player_native::get_attribute($fulltext, 'width', PARAM_INT);
229             $height = core_media_player_native::get_attribute($fulltext, 'height', PARAM_INT);
230             $name = core_media_player_native::get_attribute($fulltext, 'title');
231             return $this->embed_alternatives($urls, $name, $width, $height, $options);
232         }
233         return $fulltext;
234     }