Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
   1  <?php
   2  
   3  declare(strict_types=1);
   4  
   5  namespace OpenSpout\Common\Helper;
   6  
   7  use OpenSpout\Common\Exception\IOException;
   8  use RecursiveDirectoryIterator;
   9  use RecursiveIteratorIterator;
  10  
  11  /**
  12   * @internal
  13   */
  14  final class FileSystemHelper implements FileSystemHelperInterface
  15  {
  16      /** @var string Real path of the base folder where all the I/O can occur */
  17      private string $baseFolderRealPath;
  18  
  19      /**
  20       * @param string $baseFolderPath The path of the base folder where all the I/O can occur
  21       */
  22      public function __construct(string $baseFolderPath)
  23      {
  24          $realpath = realpath($baseFolderPath);
  25          \assert(false !== $realpath);
  26          $this->baseFolderRealPath = $realpath;
  27      }
  28  
  29      public function getBaseFolderRealPath(): string
  30      {
  31          return $this->baseFolderRealPath;
  32      }
  33  
  34      /**
  35       * Creates an empty folder with the given name under the given parent folder.
  36       *
  37       * @param string $parentFolderPath The parent folder path under which the folder is going to be created
  38       * @param string $folderName       The name of the folder to create
  39       *
  40       * @return string Path of the created folder
  41       *
  42       * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder
  43       */
  44      public function createFolder(string $parentFolderPath, string $folderName): string
  45      {
  46          $this->throwIfOperationNotInBaseFolder($parentFolderPath);
  47  
  48          $folderPath = $parentFolderPath.\DIRECTORY_SEPARATOR.$folderName;
  49  
  50          $errorMessage = '';
  51          set_error_handler(static function ($nr, $message) use (&$errorMessage): bool {
  52              $errorMessage = $message;
  53  
  54              return true;
  55          });
  56          $wasCreationSuccessful = mkdir($folderPath, 0777, true);
  57          restore_error_handler();
  58  
  59          if (!$wasCreationSuccessful) {
  60              throw new IOException("Unable to create folder: {$folderPath} - {$errorMessage}");
  61          }
  62  
  63          return $folderPath;
  64      }
  65  
  66      /**
  67       * Creates a file with the given name and content in the given folder.
  68       * The parent folder must exist.
  69       *
  70       * @param string $parentFolderPath The parent folder path where the file is going to be created
  71       * @param string $fileName         The name of the file to create
  72       * @param string $fileContents     The contents of the file to create
  73       *
  74       * @return string Path of the created file
  75       *
  76       * @throws \OpenSpout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder
  77       */
  78      public function createFileWithContents(string $parentFolderPath, string $fileName, string $fileContents): string
  79      {
  80          $this->throwIfOperationNotInBaseFolder($parentFolderPath);
  81  
  82          $filePath = $parentFolderPath.\DIRECTORY_SEPARATOR.$fileName;
  83  
  84          $errorMessage = '';
  85          set_error_handler(static function ($nr, $message) use (&$errorMessage): bool {
  86              $errorMessage = $message;
  87  
  88              return true;
  89          });
  90          $wasCreationSuccessful = file_put_contents($filePath, $fileContents);
  91          restore_error_handler();
  92  
  93          if (false === $wasCreationSuccessful) {
  94              throw new IOException("Unable to create file: {$filePath} - {$errorMessage}");
  95          }
  96  
  97          return $filePath;
  98      }
  99  
 100      /**
 101       * Delete the file at the given path.
 102       *
 103       * @param string $filePath Path of the file to delete
 104       *
 105       * @throws \OpenSpout\Common\Exception\IOException If the file path is not inside of the base folder
 106       */
 107      public function deleteFile(string $filePath): void
 108      {
 109          $this->throwIfOperationNotInBaseFolder($filePath);
 110  
 111          if (file_exists($filePath) && is_file($filePath)) {
 112              unlink($filePath);
 113          }
 114      }
 115  
 116      /**
 117       * Delete the folder at the given path as well as all its contents.
 118       *
 119       * @param string $folderPath Path of the folder to delete
 120       *
 121       * @throws \OpenSpout\Common\Exception\IOException If the folder path is not inside of the base folder
 122       */
 123      public function deleteFolderRecursively(string $folderPath): void
 124      {
 125          $this->throwIfOperationNotInBaseFolder($folderPath);
 126  
 127          $itemIterator = new RecursiveIteratorIterator(
 128              new RecursiveDirectoryIterator($folderPath, RecursiveDirectoryIterator::SKIP_DOTS),
 129              RecursiveIteratorIterator::CHILD_FIRST
 130          );
 131  
 132          foreach ($itemIterator as $item) {
 133              if ($item->isDir()) {
 134                  rmdir($item->getPathname());
 135              } else {
 136                  unlink($item->getPathname());
 137              }
 138          }
 139  
 140          rmdir($folderPath);
 141      }
 142  
 143      /**
 144       * All I/O operations must occur inside the base folder, for security reasons.
 145       * This function will throw an exception if the folder where the I/O operation
 146       * should occur is not inside the base folder.
 147       *
 148       * @param string $operationFolderPath The path of the folder where the I/O operation should occur
 149       *
 150       * @throws \OpenSpout\Common\Exception\IOException If the folder where the I/O operation should occur
 151       *                                                 is not inside the base folder or the base folder does not exist
 152       */
 153      private function throwIfOperationNotInBaseFolder(string $operationFolderPath): void
 154      {
 155          $operationFolderRealPath = realpath($operationFolderPath);
 156          if (false === $operationFolderRealPath) {
 157              throw new IOException("Folder not found: {$operationFolderRealPath}");
 158          }
 159          $isInBaseFolder = str_starts_with($operationFolderRealPath, $this->baseFolderRealPath);
 160          if (!$isInBaseFolder) {
 161              throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");
 162          }
 163      }
 164  }