Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 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   * Display H5P filter
  18   *
  19   * @package    filter_displayh5p
  20   * @copyright  2019 Victor Deniz <victor@moodle.com>
  21   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22   */
  23  
  24  defined('MOODLE_INTERNAL') || die;
  25  
  26  use core_h5p\local\library\autoloader;
  27  
  28  /**
  29   * Display H5P filter
  30   *
  31   * This filter will replace any occurrence of H5P URLs with the corresponding H5P content embed code
  32   *
  33   * @package    filter_displayh5p
  34   * @copyright  2019 Victor Deniz <victor@moodle.com>
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class filter_displayh5p extends moodle_text_filter {
  38  
  39      /**
  40       * @var boolean $loadresizerjs This is whether to request the resize.js script.
  41       */
  42      private static $loadresizerjs = true;
  43  
  44      /**
  45       * Function filter replaces any h5p-sources.
  46       *
  47       * @param  string $text    HTML content to process
  48       * @param  array  $options options passed to the filters
  49       * @return string
  50       */
  51      public function filter($text, array $options = array()) {
  52          global $CFG;
  53  
  54          if (!is_string($text) or empty($text)) {
  55              // Non string data can not be filtered anyway.
  56              return $text;
  57          }
  58  
  59          // We are trying to minimize performance impact checking there's some H5P related URL.
  60          $h5purl = '(http[^ &<]*h5p)';
  61          if (!preg_match($h5purl, $text)) {
  62              return $text;
  63          }
  64  
  65          $allowedsources = get_config('filter_displayh5p', 'allowedsources');
  66          $allowedsources = array_filter(array_map('trim', explode("\n", $allowedsources)));
  67  
  68          $localsource = '('.preg_quote($CFG->wwwroot, '~').'/[^ &\#"\'<]*\.h5p([?][^ "\'<]*)?[^ \#"\'<]*)';
  69          $allowedsources[] = $localsource;
  70  
  71          $params = array(
  72              'tagbegin' => '<iframe src="',
  73              'tagend' => '</iframe>'
  74          );
  75  
  76          $specialchars = ['?', '&'];
  77          $escapedspecialchars = ['\?', '&amp;'];
  78          $h5pcontents = array();
  79          $h5plinks = array();
  80  
  81          // Check all allowed sources.
  82          foreach ($allowedsources as $source) {
  83              // It is needed to add "/embed" at the end of URLs like https:://*.h5p.com/content/12345 (H5P.com).
  84              $params['urlmodifier'] = '';
  85  
  86              if (($source == $localsource)) {
  87                  $params['tagbegin'] = '<iframe src="'.$CFG->wwwroot.'/h5p/embed.php?url=';
  88                  $escapechars = $source;
  89                  $ultimatepattern = $source;
  90              } else {
  91                  if (!stripos($source, 'embed')) {
  92                      $params['urlmodifier'] = '/embed';
  93                  }
  94                  // Convert special chars.
  95                  $sourceid = str_replace('[id]', '[0-9]+', $source);
  96                  $escapechars = str_replace($specialchars, $escapedspecialchars, $sourceid);
  97                  $ultimatepattern = '(' . $escapechars . ')';
  98              }
  99  
 100              // Improve performance creating filterobjects only when needed.
 101              if (!preg_match($ultimatepattern, $text)) {
 102                  continue;
 103              }
 104  
 105              $h5pcontenturl = new filterobject($source, null, null, false,
 106                  false, null, [$this, 'filterobject_prepare_replacement_callback'], $params);
 107  
 108              $h5pcontenturl->workregexp = '#'.$ultimatepattern.'#';
 109              $h5pcontents[] = $h5pcontenturl;
 110  
 111              // Regex to find h5p extensions in an <a> tag.
 112              $linkregexp = '~<a [^>]*href=["\']('.$escapechars.'[^"\']*)["\'][^>]*>([^<]*)</a>~is';
 113  
 114              $h5plinkurl = new filterobject($linkregexp, null, null, false,
 115                  false, null, [$this, 'filterobject_prepare_replacement_callback'], $params);
 116              $h5plinkurl->workregexp = $linkregexp;
 117              $h5plinks[] = $h5plinkurl;
 118          }
 119  
 120          if (empty($h5pcontents) && empty($h5links)) {
 121              // No matches to deal with.
 122              return $text;
 123          }
 124  
 125          // Apply filter inside <a> tag href attribute.
 126          // We can not use filter_phrase function because it removes all tags and can not be applied in tag attributes.
 127          foreach ($h5plinks as $h5plink) {
 128              $text = preg_replace_callback($h5plink->workregexp,
 129                  function ($matches) use ($h5plink) {
 130                      if ($matches[1] == $matches[2]) {
 131                          filter_prepare_phrase_for_replacement($h5plink);
 132  
 133                          return str_replace('$1', $matches[1], $h5plink->workreplacementphrase);
 134                      } else {
 135                          return $matches[0];
 136                      }
 137                  }, $text);
 138  
 139          }
 140  
 141          $result = filter_phrases($text, $h5pcontents, null, null, false, true);
 142  
 143          // Encoding H5P file URLs.
 144          // embed.php page is requesting a PARAM_LOCALURL url parameter, so for files/directories use non-alphanumeric
 145          // characters, we need to encode the parameter. Fetch url parameter added to embed.php and encode the whole url.
 146          $localurl = '#\?url=([^" <]*[\/]+[^" <]*\.h5p)([?][^"]*)?#';
 147          $result = preg_replace_callback($localurl,
 148              function ($matches) {
 149                  $baseurl = rawurlencode($matches[1]);
 150                  // Deal with possible parameters in the url link.
 151                  if (!empty($matches[2])) {
 152                      $match = explode('?', $matches[2]);
 153                      if (!empty($match[1])) {
 154                          $baseurl = $baseurl."&".$match[1];
 155                      }
 156                  }
 157                  return "?url=".$baseurl;
 158              }, $result);
 159  
 160          return $result;
 161      }
 162  
 163      /**
 164       * Callback used by filterobject / filter_phrases.
 165       *
 166       * @param string $tagbegin HTML to insert before any match
 167       * @param string $tagend HTML to insert after any match
 168       * @param string $urlmodifier string to add to the match URL
 169       * @return array [$hreftagbegin, $hreftagend, $replacementphrase] for filterobject.
 170       */
 171      public function filterobject_prepare_replacement_callback($tagbegin, $tagend, $urlmodifier) {
 172          $sourceurl = "$1";
 173          if ($urlmodifier !== "") {
 174              $sourceurl .= $urlmodifier;
 175          }
 176  
 177          $h5piframesrc = $sourceurl . '" class="h5p-iframe" name="h5pcontent"' .
 178              ' style="height:230px; width: 100%; border: 0;" allowfullscreen="allowfullscreen">';
 179  
 180          // We want to request the resizing script only once.
 181          if (self::$loadresizerjs) {
 182              $resizerurl = autoloader::get_h5p_core_library_url('js/h5p-resizer.js');
 183              $tagend .= '<script src="' . $resizerurl->out() . '"></script>';
 184              self::$loadresizerjs = false;
 185          }
 186  
 187          return [$tagbegin, $tagend, $h5piframesrc];
 188      }
 189  }