Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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/>.
  16  
  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   */
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  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;
  45  
  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;
  59  
  60          // Set up the media manager so that media plugins requiring JS are initialised.
  61          $mediamanager = core_media_manager::instance($page);
  62      }
  63  
  64      public function filter($text, array $options = array()) {
  65          global $CFG, $PAGE;
  66  
  67          if (!is_string($text) or empty($text)) {
  68              // non string data can not be filtered anyway
  69              return $text;
  70          }
  71  
  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          }
  76  
  77          // Check SWF permissions.
  78          $this->trusted = !empty($options['noclean']) or !empty($CFG->allowobjectembed);
  79  
  80          // Looking for tags.
  81          $matches = preg_split('/(<[^>]*>)/i', $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
  82  
  83          if (!$matches) {
  84              return $text;
  85          }
  86  
  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';
  90  
  91          $newtext = '';
  92          $validtag = '';
  93          $tagname = '';
  94          $sizeofmatches = count($matches);
  95  
  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;
 102  
 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          }
 133  
 134          // Return the same string except processed by the above.
 135          return $newtext;
 136      }
 137  
 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();
 146  
 147          global $CFG, $PAGE;
 148          // Check if we ignore it.
 149          if (preg_match('/class="[^"]*nomediaplugin/i', $matches[0])) {
 150              return $matches[0];
 151          }
 152  
 153          // Get name.
 154          $name = trim($matches[2]);
 155          if (empty($name) or strpos($name, 'http') === 0) {
 156              $name = ''; // Use default name.
 157          }
 158  
 159          // Split provided URL into alternatives.
 160          $urls = $mediamanager->split_alternatives($matches[1], $width, $height);
 161  
 162          $options = [core_media_manager::OPTION_ORIGINAL_TEXT => $matches[0]];
 163          return $this->embed_alternatives($urls, $name, $width, $height, $options);
 164      }
 165  
 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) {
 179  
 180          // Allow SWF (or not).
 181          if ($this->trusted) {
 182              $options[core_media_manager::OPTION_TRUSTED] = true;
 183          }
 184  
 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;
 189  
 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);
 194  
 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      }
 202  
 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          }
 214  
 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      }
 235  }