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