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