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  /**
  18   * Manager for media files
  19   *
  20   * @package   core_media
  21   * @copyright 2016 Marina Glancy
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  /**
  28   * Manager for media files.
  29   *
  30   * Used in file resources, media filter, and any other places that need to
  31   * output embedded media.
  32   *
  33   * Usage:
  34   * $manager = core_media_manager::instance();
  35   *
  36   *
  37   * @package   core_media
  38   * @copyright 2016 Marina Glancy
  39   * @author    2011 The Open University
  40   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  final class core_media_manager {
  43      /**
  44       * Option: Disable text link fallback.
  45       *
  46       * Use this option if you are going to print a visible link anyway so it is
  47       * pointless to have one as fallback.
  48       *
  49       * To enable, set value to true.
  50       */
  51      const OPTION_NO_LINK = 'nolink';
  52  
  53      /**
  54       * Option: When embedding, if there is no matching embed, do not use the
  55       * default link fallback player; instead return blank.
  56       *
  57       * This is different from OPTION_NO_LINK because this option still uses the
  58       * fallback link if there is some kind of embedding. Use this option if you
  59       * are going to check if the return value is blank and handle it specially.
  60       *
  61       * To enable, set value to true.
  62       */
  63      const OPTION_FALLBACK_TO_BLANK = 'embedorblank';
  64  
  65      /**
  66       * Option: Enable players which are only suitable for use when we trust the
  67       * user who embedded the content.
  68       *
  69       * In the past, this option enabled the SWF player (which was removed).
  70       * However, this setting will remain because it might be used by third-party plugins.
  71       *
  72       * To enable, set value to true.
  73       */
  74      const OPTION_TRUSTED = 'trusted';
  75  
  76      /**
  77       * Option: Put a div around the output (if not blank) so that it displays
  78       * as a block using the 'resourcecontent' CSS class.
  79       *
  80       * To enable, set value to true.
  81       */
  82      const OPTION_BLOCK = 'block';
  83  
  84      /**
  85       * Option: When the request for media players came from a text filter this option will contain the
  86       * original HTML snippet, usually one of the tags: <a> or <video> or <audio>
  87       *
  88       * Players that support other HTML5 features such as tracks may find them in this option.
  89       */
  90      const OPTION_ORIGINAL_TEXT = 'originaltext';
  91  
  92      /** @var array Array of available 'player' objects */
  93      private $players;
  94  
  95      /** @var string Regex pattern for links which may contain embeddable content */
  96      private $embeddablemarkers;
  97  
  98      /** @var core_media_manager caches a singleton instance */
  99      static private $instance;
 100  
 101      /** @var moodle_page page this instance was initialised for */
 102      private $page;
 103  
 104      /**
 105       * Returns a singleton instance of a manager
 106       *
 107       * Note as of Moodle 3.3, this will call setup for you.
 108       *
 109       * @return core_media_manager
 110       */
 111      public static function instance($page = null) {
 112          // Use the passed $page if given, otherwise the $PAGE global.
 113          if (!$page) {
 114              global $PAGE;
 115              $page = $PAGE;
 116          }
 117          if (self::$instance === null || ($page && self::$instance->page !== $page)) {
 118              self::$instance = new self($page);
 119          }
 120          return self::$instance;
 121      }
 122  
 123      /**
 124       * Construct a new core_media_manager instance
 125       *
 126       * @param moodle_page $page The page we are going to add requirements to.
 127       * @see core_media_manager::instance()
 128       */
 129      private function __construct($page) {
 130          if ($page) {
 131              $this->page = $page;
 132              $players = $this->get_players();
 133              foreach ($players as $player) {
 134                  $player->setup($page);
 135              }
 136          } else {
 137              debugging('Could not determine the $PAGE. Media plugins will not be set up', DEBUG_DEVELOPER);
 138          }
 139      }
 140  
 141      /**
 142       * @deprecated since Moodle 3.3. The setup is now done in ::instance() so there is no need to call this.
 143       */
 144      public function setup() {
 145          throw new coding_exception('core_media_manager::setup() can not be used any more because it is done in ::instance()');
 146      }
 147  
 148      /**
 149       * Resets cached singleton instance. To be used after $CFG->media_plugins_sortorder is modified
 150       */
 151      public static function reset_caches() {
 152          self::$instance = null;
 153      }
 154  
 155      /**
 156       * Obtains the list of core_media_player objects currently in use to render
 157       * items.
 158       *
 159       * The list is in rank order (highest first) and does not include players
 160       * which are disabled.
 161       *
 162       * @return core_media_player[] Array of core_media_player objects in rank order
 163       */
 164      private function get_players() {
 165          // Save time by only building the list once.
 166          if (!$this->players) {
 167              $sortorder = \core\plugininfo\media::get_enabled_plugins();
 168  
 169              $this->players = [];
 170              foreach ($sortorder as $name) {
 171                  $classname = "media_" . $name . "_plugin";
 172                  if (class_exists($classname)) {
 173                      $this->players[] = new $classname();
 174                  }
 175              }
 176          }
 177          return $this->players;
 178      }
 179  
 180      /**
 181       * Renders a media file (audio or video) using suitable embedded player.
 182       *
 183       * See embed_alternatives function for full description of parameters.
 184       * This function calls through to that one.
 185       *
 186       * When using this function you can also specify width and height in the
 187       * URL by including ?d=100x100 at the end. If specified in the URL, this
 188       * will override the $width and $height parameters.
 189       *
 190       * @param moodle_url $url Full URL of media file
 191       * @param string $name Optional user-readable name to display in download link
 192       * @param int $width Width in pixels (optional)
 193       * @param int $height Height in pixels (optional)
 194       * @param array $options Array of key/value pairs
 195       * @return string HTML content of embed
 196       */
 197      public function embed_url(moodle_url $url, $name = '', $width = 0, $height = 0,
 198                                $options = array()) {
 199  
 200          // Get width and height from URL if specified (overrides parameters in
 201          // function call).
 202          $rawurl = $url->out(false);
 203          if (preg_match('/[?#]d=([\d]{1,4}%?)x([\d]{1,4}%?)/', $rawurl, $matches)) {
 204              $width = $matches[1];
 205              $height = $matches[2];
 206              $url = new moodle_url(str_replace($matches[0], '', $rawurl));
 207          }
 208  
 209          // Defer to array version of function.
 210          return $this->embed_alternatives(array($url), $name, $width, $height, $options);
 211      }
 212  
 213      /**
 214       * Renders media files (audio or video) using suitable embedded player.
 215       * The list of URLs should be alternative versions of the same content in
 216       * multiple formats. If there is only one format it should have a single
 217       * entry.
 218       *
 219       * If the media files are not in a supported format, this will give students
 220       * a download link to each format. The download link uses the filename
 221       * unless you supply the optional name parameter.
 222       *
 223       * Width and height are optional. If specified, these are suggested sizes
 224       * and should be the exact values supplied by the user, if they come from
 225       * user input. These will be treated as relating to the size of the video
 226       * content, not including any player control bar.
 227       *
 228       * For audio files, height will be ignored. For video files, a few formats
 229       * work if you specify only width, but in general if you specify width
 230       * you must specify height as well.
 231       *
 232       * The $options array is passed through to the core_media_player classes
 233       * that render the object tag. The keys can contain values from
 234       * core_media::OPTION_xx.
 235       *
 236       * @param array $alternatives Array of moodle_url to media files
 237       * @param string $name Optional user-readable name to display in download link
 238       * @param int $width Width in pixels (optional)
 239       * @param int $height Height in pixels (optional)
 240       * @param array $options Array of key/value pairs
 241       * @return string HTML content of embed
 242       */
 243      public function embed_alternatives($alternatives, $name = '', $width = 0, $height = 0,
 244                                         $options = array()) {
 245  
 246          // Get list of player plugins.
 247          $players = $this->get_players();
 248  
 249          // Set up initial text which will be replaced by first player that
 250          // supports any of the formats.
 251          $out = core_media_player::PLACEHOLDER;
 252  
 253          // Loop through all players that support any of these URLs.
 254          foreach ($players as $player) {
 255              $supported = $player->list_supported_urls($alternatives, $options);
 256              if ($supported) {
 257                  // Embed.
 258                  $text = $player->embed($supported, $name, $width, $height, $options);
 259  
 260                  // Put this in place of the 'fallback' slot in the previous text.
 261                  $out = str_replace(core_media_player::PLACEHOLDER, $text, $out);
 262  
 263                  // Check if we need to continue looking for players.
 264                  if (strpos($out, core_media_player::PLACEHOLDER) === false) {
 265                      break;
 266                  }
 267              }
 268          }
 269  
 270          if (!empty($options[self::OPTION_FALLBACK_TO_BLANK]) && $out === core_media_player::PLACEHOLDER) {
 271              // In case of OPTION_FALLBACK_TO_BLANK and no player matched do not fallback to link, just return empty string.
 272              return '';
 273          }
 274  
 275          // Remove 'fallback' slot from final version and return it.
 276          $fallback = $this->fallback_to_link($alternatives, $name, $options);
 277          $out = str_replace(core_media_player::PLACEHOLDER, $fallback, $out);
 278          $out = str_replace(core_media_player::LINKPLACEHOLDER, $fallback, $out);
 279          if (!empty($options[self::OPTION_BLOCK]) && $out !== '') {
 280              $out = html_writer::tag('div', $out, array('class' => 'resourcecontent'));
 281          }
 282          return $out;
 283      }
 284  
 285      /**
 286       * Returns links to the specified URLs unless OPTION_NO_LINK is passed.
 287       *
 288       * @param array $urls URLs of media files
 289       * @param string $name Display name; '' to use default
 290       * @param array $options Options array
 291       * @return string HTML code for embed
 292       */
 293      private function fallback_to_link($urls, $name, $options) {
 294          // If link is turned off, return empty.
 295          if (!empty($options[self::OPTION_NO_LINK])) {
 296              return '';
 297          }
 298  
 299          // Build up link content.
 300          $output = '';
 301          foreach ($urls as $url) {
 302              if (strval($name) !== '' && $output === '') {
 303                  $title = $name;
 304              } else {
 305                  $title = $this->get_filename($url);
 306              }
 307              $printlink = html_writer::link($url, $title, array('class' => 'mediafallbacklink'));
 308              if ($output) {
 309                  // Where there are multiple available formats, there are fallback links
 310                  // for all formats, separated by /.
 311                  $output .= ' / ';
 312              }
 313              $output .= $printlink;
 314          }
 315          return $output;
 316      }
 317  
 318      /**
 319       * Checks whether a file can be embedded. If this returns true you will get
 320       * an embedded player; if this returns false, you will just get a download
 321       * link.
 322       *
 323       * This is a wrapper for can_embed_urls.
 324       *
 325       * @param moodle_url $url URL of media file
 326       * @param array $options Options (same as when embedding)
 327       * @return bool True if file can be embedded
 328       */
 329      public function can_embed_url(moodle_url $url, $options = array()) {
 330          return $this->can_embed_urls(array($url), $options);
 331      }
 332  
 333      /**
 334       * Checks whether a file can be embedded. If this returns true you will get
 335       * an embedded player; if this returns false, you will just get a download
 336       * link.
 337       *
 338       * @param array $urls URL of media file and any alternatives (moodle_url)
 339       * @param array $options Options (same as when embedding)
 340       * @return bool True if file can be embedded
 341       */
 342      public function can_embed_urls(array $urls, $options = array()) {
 343          // Check all players to see if any of them support it.
 344          foreach ($this->get_players() as $player) {
 345              // First player that supports it, return true.
 346              if ($player->list_supported_urls($urls, $options)) {
 347                  return true;
 348              }
 349          }
 350          return false;
 351      }
 352  
 353      /**
 354       * Obtains a list of markers that can be used in a regular expression when
 355       * searching for URLs that can be embedded by any player type.
 356       *
 357       * This string is used to improve peformance of regex matching by ensuring
 358       * that the (presumably C) regex code can do a quick keyword check on the
 359       * URL part of a link to see if it matches one of these, rather than having
 360       * to go into PHP code for every single link to see if it can be embedded.
 361       *
 362       * @return string String suitable for use in regex such as '(\.mp4|\.flv)'
 363       */
 364      public function get_embeddable_markers() {
 365          if (empty($this->embeddablemarkers)) {
 366              $markers = '';
 367              foreach ($this->get_players() as $player) {
 368                  foreach ($player->get_embeddable_markers() as $marker) {
 369                      if ($markers !== '') {
 370                          $markers .= '|';
 371                      }
 372                      $markers .= preg_quote($marker);
 373                  }
 374              }
 375              $this->embeddablemarkers = $markers;
 376          }
 377          return $this->embeddablemarkers;
 378      }
 379  
 380      /**
 381       * Given a string containing multiple URLs separated by #, this will split
 382       * it into an array of moodle_url objects suitable for using when calling
 383       * embed_alternatives.
 384       *
 385       * Note that the input string should NOT be html-escaped (i.e. if it comes
 386       * from html, call html_entity_decode first).
 387       *
 388       * @param string $combinedurl String of 1 or more alternatives separated by #
 389       * @param int $width Output variable: width (will be set to 0 if not specified)
 390       * @param int $height Output variable: height (0 if not specified)
 391       * @return array Array of 1 or more moodle_url objects
 392       */
 393      public function split_alternatives($combinedurl, &$width, &$height) {
 394          global $CFG;
 395          $urls = explode('#', $combinedurl);
 396          $width = 0;
 397          $height = 0;
 398          $returnurls = array();
 399  
 400          foreach ($urls as $url) {
 401              $matches = null;
 402  
 403              // You can specify the size as a separate part of the array like
 404              // #d=640x480 without actually including a url in it.
 405              if (preg_match('/^d=([\d]{1,4})x([\d]{1,4})$/i', $url, $matches)) {
 406                  $width  = $matches[1];
 407                  $height = $matches[2];
 408                  continue;
 409              }
 410  
 411              // Can also include the ?d= as part of one of the URLs (if you use
 412              // more than one they will be ignored except the last).
 413              if (preg_match('/\?d=([\d]{1,4})x([\d]{1,4})$/i', $url, $matches)) {
 414                  $width  = $matches[1];
 415                  $height = $matches[2];
 416  
 417                  // Trim from URL.
 418                  $url = str_replace($matches[0], '', $url);
 419              }
 420  
 421              // Clean up url.
 422              $url = fix_utf8($url);
 423              include_once($CFG->dirroot . '/lib/validateurlsyntax.php');
 424              if (!validateUrlSyntax($url, 's?H?S?F?R?E?u-P-a?I?p?f?q?r?')) {
 425                  continue;
 426              }
 427  
 428              // Turn it into moodle_url object.
 429              $returnurls[] = new moodle_url($url);
 430          }
 431  
 432          return $returnurls;
 433      }
 434  
 435      /**
 436       * Returns the file extension for a URL.
 437       * @param moodle_url $url URL
 438       */
 439      public function get_extension(moodle_url $url) {
 440          // Note: Does not use core_text (. is UTF8-safe).
 441          $filename = self::get_filename($url);
 442          $dot = strrpos($filename, '.');
 443          if ($dot === false) {
 444              return '';
 445          } else {
 446              return strtolower(substr($filename, $dot + 1));
 447          }
 448      }
 449  
 450      /**
 451       * Obtains the filename from the moodle_url.
 452       * @param moodle_url $url URL
 453       * @return string Filename only (not escaped)
 454       */
 455      public function get_filename(moodle_url $url) {
 456          // Use the 'file' parameter if provided (for links created when
 457          // slasharguments was off). If not present, just use URL path.
 458          $path = $url->get_param('file');
 459          if (!$path) {
 460              $path = $url->get_path();
 461          }
 462  
 463          // Remove everything before last / if present. Does not use textlib as / is UTF8-safe.
 464          $slash = strrpos($path, '/');
 465          if ($slash !== false) {
 466              $path = substr($path, $slash + 1);
 467          }
 468          return $path;
 469      }
 470  
 471      /**
 472       * Guesses MIME type for a moodle_url based on file extension.
 473       * @param moodle_url $url URL
 474       * @return string MIME type
 475       */
 476      public function get_mimetype(moodle_url $url) {
 477          return mimeinfo('type', $this->get_filename($url));
 478      }
 479  
 480  }