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 402] [Versions 310 and 403] [Versions 39 and 310]

   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   * Class \core_h5p\file_storage.
  19   *
  20   * @package    core_h5p
  21   * @copyright  2019 Victor Deniz <victor@moodle.com>, base on code by Joubel AS
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_h5p;
  26  
  27  use stored_file;
  28  use Moodle\H5PCore;
  29  use Moodle\H5peditorFile;
  30  use Moodle\H5PFileStorage;
  31  
  32  /**
  33   * Class to handle storage and export of H5P Content.
  34   *
  35   * @package    core_h5p
  36   * @copyright  2019 Victor Deniz <victor@moodle.com>
  37   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class file_storage implements H5PFileStorage {
  40  
  41      /** The component for H5P. */
  42      public const COMPONENT   = 'core_h5p';
  43      /** The library file area. */
  44      public const LIBRARY_FILEAREA = 'libraries';
  45      /** The content file area */
  46      public const CONTENT_FILEAREA = 'content';
  47      /** The cached assest file area. */
  48      public const CACHED_ASSETS_FILEAREA = 'cachedassets';
  49      /** The export file area */
  50      public const EXPORT_FILEAREA = 'export';
  51      /** The icon filename */
  52      public const ICON_FILENAME = 'icon.svg';
  53  
  54      /**
  55       * The editor file area.
  56       * @deprecated since Moodle 3.10 MDL-68909. Please do not use this constant any more.
  57       * @todo MDL-69530 This will be deleted in Moodle 4.2.
  58       */
  59      public const EDITOR_FILEAREA = 'editor';
  60  
  61      /**
  62       * @var \context $context Currently we use the system context everywhere.
  63       * Don't feel forced to keep it this way in the future.
  64       */
  65      protected $context;
  66  
  67      /** @var \file_storage $fs File storage. */
  68      protected $fs;
  69  
  70      /**
  71       * Initial setup for file_storage.
  72       */
  73      public function __construct() {
  74          // Currently everything uses the system context.
  75          $this->context = \context_system::instance();
  76          $this->fs = get_file_storage();
  77      }
  78  
  79      /**
  80       * Stores a H5P library in the Moodle filesystem.
  81       *
  82       * @param array $library Library properties.
  83       */
  84      public function saveLibrary($library) {
  85          $options = [
  86              'contextid' => $this->context->id,
  87              'component' => self::COMPONENT,
  88              'filearea' => self::LIBRARY_FILEAREA,
  89              'filepath' => '/' . H5PCore::libraryToString($library, true) . '/',
  90              'itemid' => $library['libraryId']
  91          ];
  92  
  93          // Easiest approach: delete the existing library version and copy the new one.
  94          $this->delete_library($library);
  95          $this->copy_directory($library['uploadDirectory'], $options);
  96      }
  97  
  98      /**
  99       * Store the content folder.
 100       *
 101       * @param string $source Path on file system to content directory.
 102       * @param array $content Content properties
 103       */
 104      public function saveContent($source, $content) {
 105          $options = [
 106                  'contextid' => $this->context->id,
 107                  'component' => self::COMPONENT,
 108                  'filearea' => self::CONTENT_FILEAREA,
 109                  'itemid' => $content['id'],
 110                  'filepath' => '/',
 111          ];
 112  
 113          $this->delete_directory($this->context->id, self::COMPONENT, self::CONTENT_FILEAREA, $content['id']);
 114          // Copy content directory into Moodle filesystem.
 115          $this->copy_directory($source, $options);
 116      }
 117  
 118      /**
 119       * Remove content folder.
 120       *
 121       * @param array $content Content properties
 122       */
 123      public function deleteContent($content) {
 124  
 125          $this->delete_directory($this->context->id, self::COMPONENT, self::CONTENT_FILEAREA, $content['id']);
 126      }
 127  
 128      /**
 129       * Creates a stored copy of the content folder.
 130       *
 131       * @param string $id Identifier of content to clone.
 132       * @param int $newid The cloned content's identifier
 133       */
 134      public function cloneContent($id, $newid) {
 135          // Not implemented in Moodle.
 136      }
 137  
 138      /**
 139       * Get path to a new unique tmp folder.
 140       * Please note this needs to not be a directory.
 141       *
 142       * @return string Path
 143       */
 144      public function getTmpPath(): string {
 145          return make_request_directory() . '/' . uniqid('h5p-');
 146      }
 147  
 148      /**
 149       * Fetch content folder and save in target directory.
 150       *
 151       * @param int $id Content identifier
 152       * @param string $target Where the content folder will be saved
 153       */
 154      public function exportContent($id, $target) {
 155          $this->export_file_tree($target, $this->context->id, self::CONTENT_FILEAREA, '/', $id);
 156      }
 157  
 158      /**
 159       * Fetch library folder and save in target directory.
 160       *
 161       * @param array $library Library properties
 162       * @param string $target Where the library folder will be saved
 163       */
 164      public function exportLibrary($library, $target) {
 165          $folder = H5PCore::libraryToString($library, true);
 166          $this->export_file_tree($target . '/' . $folder, $this->context->id, self::LIBRARY_FILEAREA,
 167                  '/' . $folder . '/', $library['libraryId']);
 168      }
 169  
 170      /**
 171       * Save export in file system
 172       *
 173       * @param string $source Path on file system to temporary export file.
 174       * @param string $filename Name of export file.
 175       */
 176      public function saveExport($source, $filename) {
 177          global $USER;
 178  
 179          // Remove old export.
 180          $this->deleteExport($filename);
 181  
 182          $filerecord = [
 183              'contextid' => $this->context->id,
 184              'component' => self::COMPONENT,
 185              'filearea' => self::EXPORT_FILEAREA,
 186              'itemid' => 0,
 187              'filepath' => '/',
 188              'filename' => $filename,
 189              'userid' => $USER->id
 190          ];
 191          $this->fs->create_file_from_pathname($filerecord, $source);
 192      }
 193  
 194      /**
 195       * Removes given export file
 196       *
 197       * @param string $filename filename of the export to delete.
 198       */
 199      public function deleteExport($filename) {
 200          $file = $this->get_export_file($filename);
 201          if ($file) {
 202              $file->delete();
 203          }
 204      }
 205  
 206      /**
 207       * Check if the given export file exists
 208       *
 209       * @param string $filename The export file to check.
 210       * @return boolean True if the export file exists.
 211       */
 212      public function hasExport($filename) {
 213          return !!$this->get_export_file($filename);
 214      }
 215  
 216      /**
 217       * Will concatenate all JavaScrips and Stylesheets into two files in order
 218       * to improve page performance.
 219       *
 220       * @param array $files A set of all the assets required for content to display
 221       * @param string $key Hashed key for cached asset
 222       */
 223      public function cacheAssets(&$files, $key) {
 224  
 225          foreach ($files as $type => $assets) {
 226              if (empty($assets)) {
 227                  continue;
 228              }
 229  
 230              // Create new file for cached assets.
 231              $ext = ($type === 'scripts' ? 'js' : 'css');
 232              $filename = $key . '.' . $ext;
 233              $fileinfo = [
 234                  'contextid' => $this->context->id,
 235                  'component' => self::COMPONENT,
 236                  'filearea' => self::CACHED_ASSETS_FILEAREA,
 237                  'itemid' => 0,
 238                  'filepath' => '/',
 239                  'filename' => $filename
 240              ];
 241  
 242              // Store concatenated content.
 243              $this->fs->create_file_from_string($fileinfo, $this->concatenate_files($assets, $type, $this->context));
 244              $files[$type] = [
 245                  (object) [
 246                      'path' => '/' . self::CACHED_ASSETS_FILEAREA . '/' . $filename,
 247                      'version' => ''
 248                  ]
 249              ];
 250          }
 251      }
 252  
 253      /**
 254       * Will check if there are cache assets available for content.
 255       *
 256       * @param string $key Hashed key for cached asset
 257       * @return array
 258       */
 259      public function getCachedAssets($key) {
 260          $files = [];
 261  
 262          $js = $this->fs->get_file($this->context->id, self::COMPONENT, self::CACHED_ASSETS_FILEAREA, 0, '/', "{$key}.js");
 263          if ($js && $js->get_filesize() > 0) {
 264              $files['scripts'] = [
 265                  (object) [
 266                      'path' => '/' . self::CACHED_ASSETS_FILEAREA . '/' . "{$key}.js",
 267                      'version' => ''
 268                  ]
 269              ];
 270          }
 271  
 272          $css = $this->fs->get_file($this->context->id, self::COMPONENT, self::CACHED_ASSETS_FILEAREA, 0, '/', "{$key}.css");
 273          if ($css && $css->get_filesize() > 0) {
 274              $files['styles'] = [
 275                  (object) [
 276                      'path' => '/' . self::CACHED_ASSETS_FILEAREA . '/' . "{$key}.css",
 277                      'version' => ''
 278                  ]
 279              ];
 280          }
 281  
 282          return empty($files) ? null : $files;
 283      }
 284  
 285      /**
 286       * Remove the aggregated cache files.
 287       *
 288       * @param array $keys The hash keys of removed files
 289       */
 290      public function deleteCachedAssets($keys) {
 291  
 292          if (empty($keys)) {
 293              return;
 294          }
 295  
 296          foreach ($keys as $hash) {
 297              foreach (['js', 'css'] as $type) {
 298                  $cachedasset = $this->fs->get_file($this->context->id, self::COMPONENT, self::CACHED_ASSETS_FILEAREA, 0, '/',
 299                          "{$hash}.{$type}");
 300                  if ($cachedasset) {
 301                      $cachedasset->delete();
 302                  }
 303              }
 304          }
 305      }
 306  
 307      /**
 308       * Read file content of given file and then return it.
 309       *
 310       * @param string $filepath
 311       * @return string contents
 312       */
 313      public function getContent($filepath) {
 314          list(
 315              'filearea' => $filearea,
 316              'filepath' => $filepath,
 317              'filename' => $filename,
 318              'itemid' => $itemid
 319          ) = $this->get_file_elements_from_filepath($filepath);
 320  
 321          if (!$itemid) {
 322              throw new \file_serving_exception('Could not retrieve the requested file, check your file permissions.');
 323          }
 324  
 325          // Locate file.
 326          $file = $this->fs->get_file($this->context->id, self::COMPONENT, $filearea, $itemid, $filepath, $filename);
 327  
 328          // Return content.
 329          return $file->get_content();
 330      }
 331  
 332      /**
 333       * Save files uploaded through the editor.
 334       *
 335       * @param H5peditorFile $file
 336       * @param int $contentid
 337       *
 338       * @return int The id of the saved file.
 339       */
 340      public function saveFile($file, $contentid) {
 341          global $USER;
 342  
 343          $context = $this->context->id;
 344          $component = self::COMPONENT;
 345          $filearea = self::CONTENT_FILEAREA;
 346          if ($contentid === 0) {
 347              $usercontext = \context_user::instance($USER->id);
 348              $context = $usercontext->id;
 349              $component = 'user';
 350              $filearea = 'draft';
 351          }
 352  
 353          $record = array(
 354              'contextid' => $context,
 355              'component' => $component,
 356              'filearea' => $filearea,
 357              'itemid' => $contentid,
 358              'filepath' => '/' . $file->getType() . 's/',
 359              'filename' => $file->getName()
 360          );
 361  
 362          $storedfile = $this->fs->create_file_from_pathname($record, $_FILES['file']['tmp_name']);
 363  
 364          return $storedfile->get_id();
 365      }
 366  
 367      /**
 368       * Copy a file from another content or editor tmp dir.
 369       * Used when copy pasting content in H5P.
 370       *
 371       * @param string $file path + name
 372       * @param string|int $fromid Content ID or 'editor' string
 373       * @param \stdClass $tocontent Target Content
 374       *
 375       * @return void
 376       */
 377      public function cloneContentFile($file, $fromid, $tocontent): void {
 378          // Determine source filearea and itemid.
 379          if ($fromid === 'editor') {
 380              $sourcefilearea = 'draft';
 381              $sourceitemid = 0;
 382          } else {
 383              $sourcefilearea = self::CONTENT_FILEAREA;
 384              $sourceitemid = (int)$fromid;
 385          }
 386  
 387          $filepath = '/' . dirname($file) . '/';
 388          $filename = basename($file);
 389  
 390          // Check to see if source exists.
 391          $sourcefile = $this->get_file($sourcefilearea, $sourceitemid, $file);
 392          if ($sourcefile === null) {
 393              return; // Nothing to copy from.
 394          }
 395  
 396          // Check to make sure that file doesn't exist already in target.
 397          $targetfile = $this->get_file(self::CONTENT_FILEAREA, $tocontent->id, $file);
 398          if ( $targetfile !== null) {
 399              return; // File exists, no need to copy.
 400          }
 401  
 402          // Create new file record.
 403          $record = [
 404              'contextid' => $this->context->id,
 405              'component' => self::COMPONENT,
 406              'filearea' => self::CONTENT_FILEAREA,
 407              'itemid' => $tocontent->id,
 408              'filepath' => $filepath,
 409              'filename' => $filename,
 410          ];
 411  
 412          $this->fs->create_file_from_storedfile($record, $sourcefile);
 413      }
 414  
 415      /**
 416       * Copy content from one directory to another.
 417       * Defaults to cloning content from the current temporary upload folder to the editor path.
 418       *
 419       * @param string $source path to source directory
 420       * @param string $contentid Id of content
 421       *
 422       */
 423      public function moveContentDirectory($source, $contentid = null) {
 424          $contentidint = (int)$contentid;
 425  
 426          if ($source === null) {
 427              return;
 428          }
 429  
 430          // Get H5P and content json.
 431          $contentsource = $source . '/content';
 432  
 433          // Move all temporary content files to editor.
 434          $it = new \RecursiveIteratorIterator(
 435              new \RecursiveDirectoryIterator($contentsource,\RecursiveDirectoryIterator::SKIP_DOTS),
 436              \RecursiveIteratorIterator::SELF_FIRST
 437          );
 438  
 439          $it->rewind();
 440          while ($it->valid()) {
 441              $item = $it->current();
 442              $pathname = $it->getPathname();
 443              if (!$item->isDir() && !($item->getFilename() === 'content.json')) {
 444                  $this->move_file($pathname, $contentidint);
 445              }
 446              $it->next();
 447          }
 448      }
 449  
 450      /**
 451       * Get the file URL or given library and then return it.
 452       *
 453       * @param int $itemid
 454       * @param string $machinename
 455       * @param int $majorversion
 456       * @param int $minorversion
 457       * @return string url or false if the file doesn't exist
 458       */
 459      public function get_icon_url(int $itemid, string $machinename, int $majorversion, int $minorversion) {
 460          $filepath = '/' . "{$machinename}-{$majorversion}.{$minorversion}" . '/';
 461          if ($file = $this->fs->get_file(
 462              $this->context->id,
 463              self::COMPONENT,
 464              self::LIBRARY_FILEAREA,
 465              $itemid,
 466              $filepath,
 467              self::ICON_FILENAME)
 468          ) {
 469              $iconurl  = \moodle_url::make_pluginfile_url(
 470                  $this->context->id,
 471                  self::COMPONENT,
 472                  self::LIBRARY_FILEAREA,
 473                  $itemid,
 474                  $filepath,
 475                  $file->get_filename());
 476  
 477              // Return image URL.
 478              return $iconurl->out();
 479          }
 480  
 481          return false;
 482      }
 483  
 484      /**
 485       * Checks to see if an H5P content has the given file.
 486       *
 487       * @param string $file File path and name.
 488       * @param int $content Content id.
 489       *
 490       * @return int|null File ID or NULL if not found
 491       */
 492      public function getContentFile($file, $content): ?int {
 493          if (is_object($content)) {
 494              $content = $content->id;
 495          }
 496          $contentfile = $this->get_file(self::CONTENT_FILEAREA, $content, $file);
 497  
 498          return ($contentfile === null ? null : $contentfile->get_id());
 499      }
 500  
 501      /**
 502       * Remove content files that are no longer used.
 503       *
 504       * Used when saving content.
 505       *
 506       * @param string $file File path and name.
 507       * @param int $contentid Content id.
 508       *
 509       * @return void
 510       */
 511      public function removeContentFile($file, $contentid): void {
 512          // Although the interface defines $contentid as int, object given in H5peditor::processParameters.
 513          if (is_object($contentid)) {
 514              $contentid = $contentid->id;
 515          }
 516          $existingfile = $this->get_file(self::CONTENT_FILEAREA, $contentid, $file);
 517          if ($existingfile !== null) {
 518              $existingfile->delete();
 519          }
 520      }
 521  
 522      /**
 523       * Check if server setup has write permission to
 524       * the required folders
 525       *
 526       * @return bool True if server has the proper write access
 527       */
 528      public function hasWriteAccess() {
 529          // Moodle has access to the files table which is where all of the folders are stored.
 530          return true;
 531      }
 532  
 533      /**
 534       * Check if the library has a presave.js in the root folder
 535       *
 536       * @param string $libraryname
 537       * @param string $developmentpath
 538       * @return bool
 539       */
 540      public function hasPresave($libraryname, $developmentpath = null) {
 541          return false;
 542      }
 543  
 544      /**
 545       * Check if upgrades script exist for library.
 546       *
 547       * @param string $machinename
 548       * @param int $majorversion
 549       * @param int $minorversion
 550       * @return string Relative path
 551       */
 552      public function getUpgradeScript($machinename, $majorversion, $minorversion) {
 553          $path = '/' . "{$machinename}-{$majorversion}.{$minorversion}" . '/';
 554          $file = 'upgrade.js';
 555          $itemid = $this->get_itemid_for_file(self::LIBRARY_FILEAREA, $path, $file);
 556          if ($this->fs->get_file($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $itemid, $path, $file)) {
 557              return '/' . self::LIBRARY_FILEAREA . $path. $file;
 558          } else {
 559              return null;
 560          }
 561      }
 562  
 563      /**
 564       * Store the given stream into the given file.
 565       *
 566       * @param string $path
 567       * @param string $file
 568       * @param resource $stream
 569       * @return bool|int
 570       */
 571      public function saveFileFromZip($path, $file, $stream) {
 572          $fullpath = $path . '/' . $file;
 573          check_dir_exists(pathinfo($fullpath, PATHINFO_DIRNAME));
 574          return file_put_contents($fullpath, $stream);
 575      }
 576  
 577      /**
 578       * Deletes a library from the file system.
 579       *
 580       * @param  array $library Library details
 581       */
 582      public function delete_library(array $library): void {
 583          global $DB;
 584  
 585          // A library ID of false would result in all library files being deleted, which we don't want. Return instead.
 586          if ($library['libraryId'] === false) {
 587              return;
 588          }
 589  
 590          $areafiles = $this->fs->get_area_files($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $library['libraryId']);
 591          $this->delete_directory($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $library['libraryId']);
 592          $librarycache = \cache::make('core', 'h5p_library_files');
 593          foreach ($areafiles as $file) {
 594              if (!$DB->record_exists('files', array('contenthash' => $file->get_contenthash(),
 595                                                     'component' => self::COMPONENT,
 596                                                     'filearea' => self::LIBRARY_FILEAREA))) {
 597                  $librarycache->delete($file->get_contenthash());
 598              }
 599          }
 600      }
 601  
 602      /**
 603       * Remove an H5P directory from the filesystem.
 604       *
 605       * @param int $contextid context ID
 606       * @param string $component component
 607       * @param string $filearea file area or all areas in context if not specified
 608       * @param int $itemid item ID or all files if not specified
 609       */
 610      private function delete_directory(int $contextid, string $component, string $filearea, int $itemid): void {
 611  
 612          $this->fs->delete_area_files($contextid, $component, $filearea, $itemid);
 613      }
 614  
 615      /**
 616       * Copy an H5P directory from the temporary directory into the file system.
 617       *
 618       * @param  string $source  Temporary location for files.
 619       * @param  array  $options File system information.
 620       */
 621      private function copy_directory(string $source, array $options): void {
 622          $librarycache = \cache::make('core', 'h5p_library_files');
 623          $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
 624                  \RecursiveIteratorIterator::SELF_FIRST);
 625  
 626          $root = $options['filepath'];
 627  
 628          $it->rewind();
 629          while ($it->valid()) {
 630              $item = $it->current();
 631              $subpath = $it->getSubPath();
 632              if (!$item->isDir()) {
 633                  $options['filename'] = $it->getFilename();
 634                  if (!$subpath == '') {
 635                      $options['filepath'] = $root . $subpath . '/';
 636                  } else {
 637                      $options['filepath'] = $root;
 638                  }
 639  
 640                  $file = $this->fs->create_file_from_pathname($options, $item->getPathName());
 641  
 642                  if ($options['filearea'] == self::LIBRARY_FILEAREA) {
 643                      if (!$librarycache->has($file->get_contenthash())) {
 644                          $librarycache->set($file->get_contenthash(), file_get_contents($item->getPathName()));
 645                      }
 646                  }
 647              }
 648              $it->next();
 649          }
 650      }
 651  
 652      /**
 653       * Copies files from storage to temporary folder.
 654       *
 655       * @param string $target Path to temporary folder
 656       * @param int $contextid context where the files are found
 657       * @param string $filearea file area
 658       * @param string $filepath file path
 659       * @param int $itemid Optional item ID
 660       */
 661      private function export_file_tree(string $target, int $contextid, string $filearea, string $filepath, int $itemid = 0): void {
 662          // Make sure target folder exists.
 663          check_dir_exists($target);
 664  
 665          // Read source files.
 666          $files = $this->fs->get_directory_files($contextid, self::COMPONENT, $filearea, $itemid, $filepath, true);
 667  
 668          $librarycache = \cache::make('core', 'h5p_library_files');
 669  
 670          foreach ($files as $file) {
 671              $path = $target . str_replace($filepath, DIRECTORY_SEPARATOR, $file->get_filepath());
 672              if ($file->is_directory()) {
 673                  check_dir_exists(rtrim($path));
 674              } else {
 675                  if ($filearea == self::LIBRARY_FILEAREA) {
 676                      $cachedfile = $librarycache->get($file->get_contenthash());
 677                      if (empty($cachedfile)) {
 678                          $file->copy_content_to($path . $file->get_filename());
 679                          $librarycache->set($file->get_contenthash(), file_get_contents($path . $file->get_filename()));
 680                      } else {
 681                          file_put_contents($path . $file->get_filename(), $cachedfile);
 682                      }
 683                  } else {
 684                      $file->copy_content_to($path . $file->get_filename());
 685                  }
 686              }
 687          }
 688      }
 689  
 690      /**
 691       * Adds all files of a type into one file.
 692       *
 693       * @param  array    $assets  A list of files.
 694       * @param  string   $type    The type of files in assets. Either 'scripts' or 'styles'
 695       * @param  \context $context Context
 696       * @return string All of the file content in one string.
 697       */
 698      private function concatenate_files(array $assets, string $type, \context $context): string {
 699          $content = '';
 700          foreach ($assets as $asset) {
 701              // Find location of asset.
 702              list(
 703                  'filearea' => $filearea,
 704                  'filepath' => $filepath,
 705                  'filename' => $filename,
 706                  'itemid' => $itemid
 707              ) = $this->get_file_elements_from_filepath($asset->path);
 708  
 709              if ($itemid === false) {
 710                  continue;
 711              }
 712  
 713              // Locate file.
 714              $file = $this->fs->get_file($context->id, self::COMPONENT, $filearea, $itemid, $filepath, $filename);
 715  
 716              // Get file content and concatenate.
 717              if ($type === 'scripts') {
 718                  $content .= $file->get_content() . ";\n";
 719              } else {
 720                  // Rewrite relative URLs used inside stylesheets.
 721                  $content .= preg_replace_callback(
 722                      '/url\([\'"]?([^"\')]+)[\'"]?\)/i',
 723                      function ($matches) use ($filearea, $filepath, $itemid) {
 724                          if (preg_match("/^(data:|([a-z0-9]+:)?\/)/i", $matches[1]) === 1) {
 725                              return $matches[0]; // Not relative, skip.
 726                          }
 727                          // Find "../" in matches[1].
 728                          // If it exists, we have to remove "../".
 729                          // And switch the last folder in the filepath for the first folder in $matches[1].
 730                          // For instance:
 731                          // $filepath: /H5P.Question-1.4/styles/
 732                          // $matches[1]: ../images/plus-one.svg
 733                          // We want to avoid this: H5P.Question-1.4/styles/ITEMID/../images/minus-one.svg
 734                          // We want this: H5P.Question-1.4/images/ITEMID/minus-one.svg.
 735                          if (preg_match('/\.\.\//', $matches[1], $pathmatches)) {
 736                              $path = preg_split('/\//', $filepath, -1, PREG_SPLIT_NO_EMPTY);
 737                              $pathfilename = preg_split('/\//', $matches[1], -1, PREG_SPLIT_NO_EMPTY);
 738                              // Remove the first element: ../.
 739                              array_shift($pathfilename);
 740                              // Replace pathfilename into the filepath.
 741                              $path[count($path) - 1] = $pathfilename[0];
 742                              $filepath = '/' . implode('/', $path) . '/';
 743                              // Remove the element used to replace.
 744                              array_shift($pathfilename);
 745                              $matches[1] = implode('/', $pathfilename);
 746                          }
 747                          return 'url("../' . $filearea . $filepath . $itemid . '/' . $matches[1] . '")';
 748                      },
 749                      $file->get_content()) . "\n";
 750              }
 751          }
 752          return $content;
 753      }
 754  
 755      /**
 756       * Get files ready for export.
 757       *
 758       * @param  string $filename File name to retrieve.
 759       * @return bool|\stored_file Stored file instance if exists, false if not
 760       */
 761      public function get_export_file(string $filename) {
 762          return $this->fs->get_file($this->context->id, self::COMPONENT, self::EXPORT_FILEAREA, 0, '/', $filename);
 763      }
 764  
 765      /**
 766       * Converts a relative system file path into Moodle File API elements.
 767       *
 768       * @param  string $filepath The system filepath to get information from.
 769       * @return array File information.
 770       */
 771      private function get_file_elements_from_filepath(string $filepath): array {
 772          $sections = explode('/', $filepath);
 773          // Get the filename.
 774          $filename = array_pop($sections);
 775          // Discard first element.
 776          if (empty($sections[0])) {
 777              array_shift($sections);
 778          }
 779          // Get the filearea.
 780          $filearea = array_shift($sections);
 781          $itemid = array_shift($sections);
 782          // Get the filepath.
 783          $filepath = implode('/', $sections);
 784          $filepath = '/' . $filepath . '/';
 785  
 786          return ['filearea' => $filearea, 'filepath' => $filepath, 'filename' => $filename, 'itemid' => $itemid];
 787      }
 788  
 789      /**
 790       * Returns the item id given the other necessary variables.
 791       *
 792       * @param  string $filearea The file area.
 793       * @param  string $filepath The file path.
 794       * @param  string $filename The file name.
 795       * @return mixed the specified value false if not found.
 796       */
 797      private function get_itemid_for_file(string $filearea, string $filepath, string $filename) {
 798          global $DB;
 799          return $DB->get_field('files', 'itemid', ['component' => self::COMPONENT, 'filearea' => $filearea, 'filepath' => $filepath,
 800                  'filename' => $filename]);
 801      }
 802  
 803      /**
 804       * Helper to make it easy to load content files.
 805       *
 806       * @param string $filearea File area where the file is saved.
 807       * @param int $itemid Content instance or content id.
 808       * @param string $file File path and name.
 809       *
 810       * @return stored_file|null
 811       */
 812      private function get_file(string $filearea, int $itemid, string $file): ?stored_file {
 813          global $USER;
 814  
 815          $component = self::COMPONENT;
 816          $context = $this->context->id;
 817          if ($filearea === 'draft') {
 818              $itemid = 0;
 819              $component = 'user';
 820              $usercontext = \context_user::instance($USER->id);
 821              $context = $usercontext->id;
 822          }
 823  
 824          $filepath = '/'. dirname($file). '/';
 825          $filename = basename($file);
 826  
 827          // Load file.
 828          $existingfile = $this->fs->get_file($context, $component, $filearea, $itemid, $filepath, $filename);
 829          if (!$existingfile) {
 830              return null;
 831          }
 832  
 833          return $existingfile;
 834      }
 835  
 836      /**
 837       * Move a single file
 838       *
 839       * @param string $sourcefile Path to source file
 840       * @param int $contentid Content id or 0 if the file is in the editor file area
 841       *
 842       * @return void
 843       */
 844      private function move_file(string $sourcefile, int $contentid): void {
 845          $pathparts = pathinfo($sourcefile);
 846          $filename  = $pathparts['basename'];
 847          $filepath  = $pathparts['dirname'];
 848          $foldername = basename($filepath);
 849  
 850          // Create file record for content.
 851          $record = array(
 852              'contextid' => $this->context->id,
 853              'component' => $contentid > 0 ? self::COMPONENT : 'user',
 854              'filearea' => $contentid > 0 ? self::CONTENT_FILEAREA : 'draft',
 855              'itemid' => $contentid > 0 ? $contentid : 0,
 856              'filepath' => '/' . $foldername . '/',
 857              'filename' => $filename
 858          );
 859  
 860          $file = $this->fs->get_file(
 861              $record['contextid'], $record['component'],
 862              $record['filearea'], $record['itemid'], $record['filepath'],
 863              $record['filename']
 864          );
 865  
 866          if ($file) {
 867              // Delete it to make sure that it is replaced with correct content.
 868              $file->delete();
 869          }
 870  
 871          $this->fs->create_file_from_pathname($record, $sourcefile);
 872      }
 873  }