Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   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, $USER;
  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              // Local files may display a button below the content to modify it when editing mode is on. This button will appear
  87              // only if the user has the proper capabilities.
  88              $params['canbeedited'] = (!empty($USER->editing)) && ($source == $localsource);
  89              if ($source == $localsource) {
  90                  $params['tagbegin'] = '<iframe src="'.$CFG->wwwroot.'/h5p/embed.php?url=';
  91                  $escapechars = $source;
  92                  $ultimatepattern = $source;
  93              } else {
  94                  if (!stripos($source, 'embed')) {
  95                      $params['urlmodifier'] = '/embed';
  96                  }
  97                  // Convert special chars.
  98                  $sourceid = str_replace('[id]', '[0-9]+', $source);
  99                  $escapechars = str_replace($specialchars, $escapedspecialchars, $sourceid);
 100                  $ultimatepattern = '(' . $escapechars . ')';
 101              }
 102  
 103              // Improve performance creating filterobjects only when needed.
 104              if (!preg_match($ultimatepattern, $text)) {
 105                  continue;
 106              }
 107  
 108              $h5pcontenturl = new filterobject($source, null, null, false,
 109                  false, null, [$this, 'filterobject_prepare_replacement_callback'], $params + ['ish5plink' => false]);
 110  
 111              $h5pcontenturl->workregexp = '#'.$ultimatepattern.'#';
 112              $h5pcontents[] = $h5pcontenturl;
 113  
 114              // Regex to find h5p extensions in an <a> tag.
 115              $linkregexp = '~<a [^>]*href=["\']('.$escapechars.'[^"\']*)["\'][^>]*>([^<]*)</a>~is';
 116  
 117              $h5plinkurl = new filterobject($linkregexp, null, null, false,
 118                  false, null, [$this, 'filterobject_prepare_replacement_callback'], $params + ['ish5plink' => true]);
 119              $h5plinkurl->workregexp = $linkregexp;
 120              $h5plinks[] = $h5plinkurl;
 121          }
 122  
 123          if (empty($h5pcontents) && empty($h5links)) {
 124              // No matches to deal with.
 125              return $text;
 126          }
 127  
 128          // Apply filter inside <a> tag href attribute.
 129          // We can not use filter_phrase function because it removes all tags and can not be applied in tag attributes.
 130          foreach ($h5plinks as $h5plink) {
 131              $text = preg_replace_callback($h5plink->workregexp,
 132                  function ($matches) use ($h5plink) {
 133                      if ($matches[1] == $matches[2]) {
 134                          filter_prepare_phrase_for_replacement($h5plink);
 135  
 136                          return str_replace('$1', $matches[1], $h5plink->workreplacementphrase);
 137                      } else {
 138                          return $matches[0];
 139                      }
 140                  }, $text);
 141          }
 142  
 143          // The "Edit" button below each H5P content will be displayed only for users with permissions to edit the content (to
 144          // avoid confusion). So the original H5P file behind this URL will be obtained and checked using the methods in the API.
 145          // As the H5P URL is required in order to get this information, this action can be done only here(the
 146          // prepare_replacement_callback method has only the placeholders).
 147          foreach ($h5pcontents as $h5pcontent) {
 148              $text = preg_replace_callback($h5pcontent->workregexp,
 149                  function ($matches) use ($h5pcontent) {
 150                      global $USER, $CFG;
 151  
 152                      // The Edit button placeholder has been added only if the file can be edited.
 153                      if ($h5pcontent->replacementcallbackdata['canbeedited']) {
 154                          // If the content was originally a link, ignore it (it won't have the placeholder).
 155                          $matchurl = new \moodle_url($matches[0]);
 156                          if (strpos($matchurl->get_path(), 'h5p/embed.php') !== false) {
 157                              return $matches[0];
 158                          }
 159  
 160                          $contenturl = $matches[0];
 161                          list($file, $h5p) = \core_h5p\api::get_original_content_from_pluginfile_url($contenturl, true, true);
 162                          if ($file) {
 163                              filter_prepare_phrase_for_replacement($h5pcontent);
 164  
 165                              // Check if the user can edit this content.
 166                              if (\core_h5p\api::can_edit_content($file)) {
 167                                  // If the user can modify the content, replace the placeholder with a link to the editor.
 168                                  $title = get_string('editcontent', 'core_h5p');
 169                                  $editorurl = $CFG->wwwroot . '/h5p/edit.php?url=' . $contenturl;
 170                                  $htmlcode = html_writer::start_tag(
 171                                      'a',
 172                                      ['class' => 'autolink', 'title' => $title, 'href' => $editorurl]
 173                                  );
 174                                  $htmlcode .= $title . html_writer::end_tag('a');
 175                                  $content = str_replace('$2', $htmlcode, $h5pcontent->workreplacementphrase);
 176                              } else {
 177                                  // If the user can't edit the content, remove the placeholder.
 178                                  $content = str_replace('$2', '', $h5pcontent->workreplacementphrase);
 179                              }
 180  
 181                              return str_replace('$1', $contenturl, $content);
 182                          }
 183                      }
 184  
 185                      return $matches[0];
 186                  }, $text);
 187          }
 188  
 189          $result = filter_phrases($text, $h5pcontents, null, null, false, true);
 190  
 191          // Encoding H5P file URLs.
 192          // embed.php page is requesting a PARAM_LOCALURL url parameter, so for files/directories use non-alphanumeric
 193          // characters, we need to encode the parameter. Fetch url parameter added to embed.php and encode the whole url.
 194          $localurl = '#\?url=([^" <]*[\/]+[^" <]*\.h5p)([?][^"]*)?#';
 195          $result = preg_replace_callback($localurl,
 196              function ($matches) {
 197                  $baseurl = rawurlencode($matches[1]);
 198                  // Deal with possible parameters in the url link.
 199                  if (!empty($matches[2])) {
 200                      $match = explode('?', $matches[2]);
 201                      if (!empty($match[1])) {
 202                          $baseurl = $baseurl."&".$match[1];
 203                      }
 204                  }
 205                  return "?url=".$baseurl;
 206              }, $result);
 207  
 208          return $result;
 209      }
 210  
 211      /**
 212       * Callback used by filterobject / filter_phrases.
 213       *
 214       * @param string $tagbegin HTML to insert before any match
 215       * @param string $tagend HTML to insert after any match
 216       * @param string $urlmodifier string to add to the match URL
 217       * @param bool $canbeedited Whether the content can be modified or not (to display a link to edit it or not).
 218       * @param bool $ish5plink Whether the original content comes from an H5P link or not.
 219       * @return array [$hreftagbegin, $hreftagend, $replacementphrase] for filterobject.
 220       */
 221      public function filterobject_prepare_replacement_callback($tagbegin, $tagend, $urlmodifier, $canbeedited, $ish5plink) {
 222  
 223          $sourceurl = "$1";
 224          if ($urlmodifier !== "") {
 225              $sourceurl .= $urlmodifier;
 226          }
 227  
 228          $h5piframesrc = $sourceurl . '" class="h5p-iframe" name="h5pcontent"' .
 229              ' style="height:230px; width: 100%; border: 0;" allowfullscreen="allowfullscreen">';
 230  
 231          // We want to request the resizing script only once.
 232          if (self::$loadresizerjs) {
 233              $resizerurl = autoloader::get_h5p_core_library_url('js/h5p-resizer.js');
 234              $tagend .= '<script src="' . $resizerurl->out() . '"></script>';
 235              self::$loadresizerjs = false;
 236          }
 237  
 238          if ($canbeedited && !$ish5plink) {
 239              // Placeholder to be replaced by the edit content button (depending on the user permissions).
 240              $tagend .= "$2";
 241          }
 242  
 243          return [$tagbegin, $tagend, $h5piframesrc];
 244      }
 245  }