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 39 and 401]

   1  <?php
   2  
   3  namespace Box\Spout\Writer\Common\Helper;
   4  
   5  use Box\Spout\Writer\Common\Creator\InternalEntityFactory;
   6  
   7  /**
   8   * Class ZipHelper
   9   * This class provides helper functions to create zip files
  10   */
  11  class ZipHelper
  12  {
  13      const ZIP_EXTENSION = '.zip';
  14  
  15      /** Controls what to do when trying to add an existing file */
  16      const EXISTING_FILES_SKIP = 'skip';
  17      const EXISTING_FILES_OVERWRITE = 'overwrite';
  18  
  19      /** @var InternalEntityFactory Factory to create entities */
  20      private $entityFactory;
  21  
  22      /**
  23       * @param InternalEntityFactory $entityFactory Factory to create entities
  24       */
  25      public function __construct($entityFactory)
  26      {
  27          $this->entityFactory = $entityFactory;
  28      }
  29  
  30      /**
  31       * Returns a new ZipArchive instance pointing at the given path.
  32       *
  33       * @param string $tmpFolderPath Path of the temp folder where the zip file will be created
  34       * @return \ZipArchive
  35       */
  36      public function createZip($tmpFolderPath)
  37      {
  38          $zip = $this->entityFactory->createZipArchive();
  39          $zipFilePath = $tmpFolderPath . self::ZIP_EXTENSION;
  40  
  41          $zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
  42  
  43          return $zip;
  44      }
  45  
  46      /**
  47       * @param \ZipArchive $zip An opened zip archive object
  48       * @return string Path where the zip file of the given folder will be created
  49       */
  50      public function getZipFilePath(\ZipArchive $zip)
  51      {
  52          return $zip->filename;
  53      }
  54  
  55      /**
  56       * Adds the given file, located under the given root folder to the archive.
  57       * The file will be compressed.
  58       *
  59       * Example of use:
  60       *   addFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml');
  61       *   => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
  62       *
  63       * @param \ZipArchive $zip An opened zip archive object
  64       * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
  65       * @param string $localFilePath Path of the file to be added, under the root folder
  66       * @param string $existingFileMode Controls what to do when trying to add an existing file
  67       * @return void
  68       */
  69      public function addFileToArchive($zip, $rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
  70      {
  71          $this->addFileToArchiveWithCompressionMethod(
  72              $zip,
  73              $rootFolderPath,
  74              $localFilePath,
  75              $existingFileMode,
  76              \ZipArchive::CM_DEFAULT
  77          );
  78      }
  79  
  80      /**
  81       * Adds the given file, located under the given root folder to the archive.
  82       * The file will NOT be compressed.
  83       *
  84       * Example of use:
  85       *   addUncompressedFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml');
  86       *   => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
  87       *
  88       * @param \ZipArchive $zip An opened zip archive object
  89       * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
  90       * @param string $localFilePath Path of the file to be added, under the root folder
  91       * @param string $existingFileMode Controls what to do when trying to add an existing file
  92       * @return void
  93       */
  94      public function addUncompressedFileToArchive($zip, $rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
  95      {
  96          $this->addFileToArchiveWithCompressionMethod(
  97              $zip,
  98              $rootFolderPath,
  99              $localFilePath,
 100              $existingFileMode,
 101              \ZipArchive::CM_STORE
 102          );
 103      }
 104  
 105      /**
 106       * Adds the given file, located under the given root folder to the archive.
 107       * The file will NOT be compressed.
 108       *
 109       * Example of use:
 110       *   addUncompressedFileToArchive($zip, '/tmp/xlsx/foo', 'bar/baz.xml');
 111       *   => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
 112       *
 113       * @param \ZipArchive $zip An opened zip archive object
 114       * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
 115       * @param string $localFilePath Path of the file to be added, under the root folder
 116       * @param string $existingFileMode Controls what to do when trying to add an existing file
 117       * @param int $compressionMethod The compression method
 118       * @return void
 119       */
 120      protected function addFileToArchiveWithCompressionMethod($zip, $rootFolderPath, $localFilePath, $existingFileMode, $compressionMethod)
 121      {
 122          if (!$this->shouldSkipFile($zip, $localFilePath, $existingFileMode)) {
 123              $normalizedFullFilePath = $this->getNormalizedRealPath($rootFolderPath . '/' . $localFilePath);
 124              $zip->addFile($normalizedFullFilePath, $localFilePath);
 125  
 126              if (self::canChooseCompressionMethod()) {
 127                  $zip->setCompressionName($localFilePath, $compressionMethod);
 128              }
 129          }
 130      }
 131  
 132      /**
 133       * @return bool Whether it is possible to choose the desired compression method to be used
 134       */
 135      public static function canChooseCompressionMethod()
 136      {
 137          // setCompressionName() is a PHP7+ method...
 138          return (\method_exists(new \ZipArchive(), 'setCompressionName'));
 139      }
 140  
 141      /**
 142       * @param \ZipArchive $zip An opened zip archive object
 143       * @param string $folderPath Path to the folder to be zipped
 144       * @param string $existingFileMode Controls what to do when trying to add an existing file
 145       * @return void
 146       */
 147      public function addFolderToArchive($zip, $folderPath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
 148      {
 149          $folderRealPath = $this->getNormalizedRealPath($folderPath) . '/';
 150          $itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
 151  
 152          foreach ($itemIterator as $itemInfo) {
 153              $itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname());
 154              $itemLocalPath = \str_replace($folderRealPath, '', $itemRealPath);
 155  
 156              if ($itemInfo->isFile() && !$this->shouldSkipFile($zip, $itemLocalPath, $existingFileMode)) {
 157                  $zip->addFile($itemRealPath, $itemLocalPath);
 158              }
 159          }
 160      }
 161  
 162      /**
 163       * @param \ZipArchive $zip
 164       * @param string $itemLocalPath
 165       * @param string $existingFileMode
 166       * @return bool Whether the file should be added to the archive or skipped
 167       */
 168      protected function shouldSkipFile($zip, $itemLocalPath, $existingFileMode)
 169      {
 170          // Skip files if:
 171          //   - EXISTING_FILES_SKIP mode chosen
 172          //   - File already exists in the archive
 173          return ($existingFileMode === self::EXISTING_FILES_SKIP && $zip->locateName($itemLocalPath) !== false);
 174      }
 175  
 176      /**
 177       * Returns canonicalized absolute pathname, containing only forward slashes.
 178       *
 179       * @param string $path Path to normalize
 180       * @return string Normalized and canonicalized path
 181       */
 182      protected function getNormalizedRealPath($path)
 183      {
 184          $realPath = \realpath($path);
 185  
 186          return \str_replace(DIRECTORY_SEPARATOR, '/', $realPath);
 187      }
 188  
 189      /**
 190       * Closes the archive and copies it into the given stream
 191       *
 192       * @param \ZipArchive $zip An opened zip archive object
 193       * @param resource $streamPointer Pointer to the stream to copy the zip
 194       * @return void
 195       */
 196      public function closeArchiveAndCopyToStream($zip, $streamPointer)
 197      {
 198          $zipFilePath = $zip->filename;
 199          $zip->close();
 200  
 201          $this->copyZipToStream($zipFilePath, $streamPointer);
 202      }
 203  
 204      /**
 205       * Streams the contents of the zip file into the given stream
 206       *
 207       * @param string $zipFilePath Path of the zip file
 208       * @param resource $pointer Pointer to the stream to copy the zip
 209       * @return void
 210       */
 211      protected function copyZipToStream($zipFilePath, $pointer)
 212      {
 213          $zipFilePointer = \fopen($zipFilePath, 'r');
 214          \stream_copy_to_stream($zipFilePointer, $pointer);
 215          \fclose($zipFilePointer);
 216      }
 217  }