Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 402 and 403]

   1  <?php
   2  
   3  declare(strict_types=1);
   4  
   5  namespace OpenSpout\Writer\ODS\Helper;
   6  
   7  use DateTimeImmutable;
   8  use OpenSpout\Common\Helper\FileSystemHelper as CommonFileSystemHelper;
   9  use OpenSpout\Writer\Common\Entity\Worksheet;
  10  use OpenSpout\Writer\Common\Helper\FileSystemWithRootFolderHelperInterface;
  11  use OpenSpout\Writer\Common\Helper\ZipHelper;
  12  use OpenSpout\Writer\ODS\Manager\Style\StyleManager;
  13  use OpenSpout\Writer\ODS\Manager\WorksheetManager;
  14  
  15  /**
  16   * @internal
  17   */
  18  final class FileSystemHelper implements FileSystemWithRootFolderHelperInterface
  19  {
  20      public const MIMETYPE = 'application/vnd.oasis.opendocument.spreadsheet';
  21  
  22      public const META_INF_FOLDER_NAME = 'META-INF';
  23  
  24      public const MANIFEST_XML_FILE_NAME = 'manifest.xml';
  25      public const CONTENT_XML_FILE_NAME = 'content.xml';
  26      public const META_XML_FILE_NAME = 'meta.xml';
  27      public const MIMETYPE_FILE_NAME = 'mimetype';
  28      public const STYLES_XML_FILE_NAME = 'styles.xml';
  29  
  30      private string $baseFolderRealPath;
  31  
  32      /** @var string document creator */
  33      private string $creator;
  34      private CommonFileSystemHelper $baseFileSystemHelper;
  35  
  36      /** @var string Path to the root folder inside the temp folder where the files to create the ODS will be stored */
  37      private string $rootFolder;
  38  
  39      /** @var string Path to the "META-INF" folder inside the root folder */
  40      private string $metaInfFolder;
  41  
  42      /** @var string Path to the temp folder, inside the root folder, where specific sheets content will be written to */
  43      private string $sheetsContentTempFolder;
  44  
  45      /** @var ZipHelper Helper to perform tasks with Zip archive */
  46      private ZipHelper $zipHelper;
  47  
  48      /**
  49       * @param string    $baseFolderPath The path of the base folder where all the I/O can occur
  50       * @param ZipHelper $zipHelper      Helper to perform tasks with Zip archive
  51       * @param string    $creator        document creator
  52       */
  53      public function __construct(string $baseFolderPath, ZipHelper $zipHelper, string $creator)
  54      {
  55          $this->baseFileSystemHelper = new CommonFileSystemHelper($baseFolderPath);
  56          $this->baseFolderRealPath = $this->baseFileSystemHelper->getBaseFolderRealPath();
  57          $this->zipHelper = $zipHelper;
  58          $this->creator = $creator;
  59      }
  60  
  61      public function createFolder(string $parentFolderPath, string $folderName): string
  62      {
  63          return $this->baseFileSystemHelper->createFolder($parentFolderPath, $folderName);
  64      }
  65  
  66      public function createFileWithContents(string $parentFolderPath, string $fileName, string $fileContents): string
  67      {
  68          return $this->baseFileSystemHelper->createFileWithContents($parentFolderPath, $fileName, $fileContents);
  69      }
  70  
  71      public function deleteFile(string $filePath): void
  72      {
  73          $this->baseFileSystemHelper->deleteFile($filePath);
  74      }
  75  
  76      public function deleteFolderRecursively(string $folderPath): void
  77      {
  78          $this->baseFileSystemHelper->deleteFolderRecursively($folderPath);
  79      }
  80  
  81      public function getRootFolder(): string
  82      {
  83          return $this->rootFolder;
  84      }
  85  
  86      public function getSheetsContentTempFolder(): string
  87      {
  88          return $this->sheetsContentTempFolder;
  89      }
  90  
  91      /**
  92       * Creates all the folders needed to create a ODS file, as well as the files that won't change.
  93       *
  94       * @throws \OpenSpout\Common\Exception\IOException If unable to create at least one of the base folders
  95       */
  96      public function createBaseFilesAndFolders(): void
  97      {
  98          $this
  99              ->createRootFolder()
 100              ->createMetaInfoFolderAndFile()
 101              ->createSheetsContentTempFolder()
 102              ->createMetaFile()
 103              ->createMimetypeFile()
 104          ;
 105      }
 106  
 107      /**
 108       * Creates the "content.xml" file under the root folder.
 109       *
 110       * @param Worksheet[] $worksheets
 111       */
 112      public function createContentFile(WorksheetManager $worksheetManager, StyleManager $styleManager, array $worksheets): self
 113      {
 114          $contentXmlFileContents = <<<'EOD'
 115              <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 116              <office:document-content office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:msoxl="http://schemas.microsoft.com/office/excel/formula" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">
 117              EOD;
 118  
 119          $contentXmlFileContents .= $styleManager->getContentXmlFontFaceSectionContent();
 120          $contentXmlFileContents .= $styleManager->getContentXmlAutomaticStylesSectionContent($worksheets);
 121  
 122          $contentXmlFileContents .= '<office:body><office:spreadsheet>';
 123  
 124          $topContentTempFile = uniqid(self::CONTENT_XML_FILE_NAME);
 125          $this->createFileWithContents($this->rootFolder, $topContentTempFile, $contentXmlFileContents);
 126  
 127          // Append sheets content to "content.xml"
 128          $contentXmlFilePath = $this->rootFolder.\DIRECTORY_SEPARATOR.self::CONTENT_XML_FILE_NAME;
 129          $contentXmlHandle = fopen($contentXmlFilePath, 'w');
 130          \assert(false !== $contentXmlHandle);
 131  
 132          $topContentTempPathname = $this->rootFolder.\DIRECTORY_SEPARATOR.$topContentTempFile;
 133          $topContentTempHandle = fopen($topContentTempPathname, 'r');
 134          \assert(false !== $topContentTempHandle);
 135          stream_copy_to_stream($topContentTempHandle, $contentXmlHandle);
 136          fclose($topContentTempHandle);
 137          unlink($topContentTempPathname);
 138  
 139          foreach ($worksheets as $worksheet) {
 140              // write the "<table:table>" node, with the final sheet's name
 141              fwrite($contentXmlHandle, $worksheetManager->getTableElementStartAsString($worksheet));
 142  
 143              $worksheetFilePath = $worksheet->getFilePath();
 144              $this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle);
 145  
 146              fwrite($contentXmlHandle, '</table:table>');
 147          }
 148  
 149          // add AutoFilter
 150          $databaseRanges = '';
 151          foreach ($worksheets as $worksheet) {
 152              $databaseRanges .= $worksheetManager->getTableDatabaseRangeElementAsString($worksheet);
 153          }
 154          if ('' !== $databaseRanges) {
 155              fwrite($contentXmlHandle, '<table:database-ranges>');
 156              fwrite($contentXmlHandle, $databaseRanges);
 157              fwrite($contentXmlHandle, '</table:database-ranges>');
 158          }
 159  
 160          $contentXmlFileContents = '</office:spreadsheet></office:body></office:document-content>';
 161  
 162          fwrite($contentXmlHandle, $contentXmlFileContents);
 163          fclose($contentXmlHandle);
 164  
 165          return $this;
 166      }
 167  
 168      /**
 169       * Deletes the temporary folder where sheets content was stored.
 170       */
 171      public function deleteWorksheetTempFolder(): self
 172      {
 173          $this->deleteFolderRecursively($this->sheetsContentTempFolder);
 174  
 175          return $this;
 176      }
 177  
 178      /**
 179       * Creates the "styles.xml" file under the root folder.
 180       *
 181       * @param int $numWorksheets Number of created worksheets
 182       */
 183      public function createStylesFile(StyleManager $styleManager, int $numWorksheets): self
 184      {
 185          $stylesXmlFileContents = $styleManager->getStylesXMLFileContent($numWorksheets);
 186          $this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
 187  
 188          return $this;
 189      }
 190  
 191      /**
 192       * Zips the root folder and streams the contents of the zip into the given stream.
 193       *
 194       * @param resource $streamPointer Pointer to the stream to copy the zip
 195       */
 196      public function zipRootFolderAndCopyToStream($streamPointer): void
 197      {
 198          $zip = $this->zipHelper->createZip($this->rootFolder);
 199  
 200          $zipFilePath = $this->zipHelper->getZipFilePath($zip);
 201  
 202          // In order to have the file's mime type detected properly, files need to be added
 203          // to the zip file in a particular order.
 204          // @see http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/
 205          $this->zipHelper->addUncompressedFileToArchive($zip, $this->rootFolder, self::MIMETYPE_FILE_NAME);
 206  
 207          $this->zipHelper->addFolderToArchive($zip, $this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
 208          $this->zipHelper->closeArchiveAndCopyToStream($zip, $streamPointer);
 209  
 210          // once the zip is copied, remove it
 211          $this->deleteFile($zipFilePath);
 212      }
 213  
 214      /**
 215       * Creates the folder that will be used as root.
 216       *
 217       * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder
 218       */
 219      private function createRootFolder(): self
 220      {
 221          $this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('ods'));
 222  
 223          return $this;
 224      }
 225  
 226      /**
 227       * Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it.
 228       *
 229       * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file
 230       */
 231      private function createMetaInfoFolderAndFile(): self
 232      {
 233          $this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME);
 234  
 235          $this->createManifestFile();
 236  
 237          return $this;
 238      }
 239  
 240      /**
 241       * Creates the "manifest.xml" file under the "META-INF" folder (under root).
 242       *
 243       * @throws \OpenSpout\Common\Exception\IOException If unable to create the file
 244       */
 245      private function createManifestFile(): self
 246      {
 247          $manifestXmlFileContents = <<<'EOD'
 248              <?xml version="1.0" encoding="UTF-8"?>
 249              <manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">
 250                  <manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>
 251                  <manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
 252                  <manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
 253                  <manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
 254              </manifest:manifest>
 255              EOD;
 256  
 257          $this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents);
 258  
 259          return $this;
 260      }
 261  
 262      /**
 263       * Creates the temp folder where specific sheets content will be written to.
 264       * This folder is not part of the final ODS file and is only used to be able to jump between sheets.
 265       *
 266       * @throws \OpenSpout\Common\Exception\IOException If unable to create the folder
 267       */
 268      private function createSheetsContentTempFolder(): self
 269      {
 270          $this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, 'worksheets-temp');
 271  
 272          return $this;
 273      }
 274  
 275      /**
 276       * Creates the "meta.xml" file under the root folder.
 277       *
 278       * @throws \OpenSpout\Common\Exception\IOException If unable to create the file
 279       */
 280      private function createMetaFile(): self
 281      {
 282          $createdDate = (new DateTimeImmutable())->format(DateTimeImmutable::W3C);
 283  
 284          $metaXmlFileContents = <<<EOD
 285              <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 286              <office:document-meta office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">
 287                  <office:meta>
 288                      <dc:creator>{$this->creator}</dc:creator>
 289                      <meta:creation-date>{$createdDate}</meta:creation-date>
 290                      <dc:date>{$createdDate}</dc:date>
 291                  </office:meta>
 292              </office:document-meta>
 293              EOD;
 294  
 295          $this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents);
 296  
 297          return $this;
 298      }
 299  
 300      /**
 301       * Creates the "mimetype" file under the root folder.
 302       *
 303       * @throws \OpenSpout\Common\Exception\IOException If unable to create the file
 304       */
 305      private function createMimetypeFile(): self
 306      {
 307          $this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE);
 308  
 309          return $this;
 310      }
 311  
 312      /**
 313       * Streams the content of the file at the given path into the target resource.
 314       * Depending on which mode the target resource was created with, it will truncate then copy
 315       * or append the content to the target file.
 316       *
 317       * @param string   $sourceFilePath Path of the file whose content will be copied
 318       * @param resource $targetResource Target resource that will receive the content
 319       */
 320      private function copyFileContentsToTarget(string $sourceFilePath, $targetResource): void
 321      {
 322          $sourceHandle = fopen($sourceFilePath, 'r');
 323          \assert(false !== $sourceHandle);
 324          stream_copy_to_stream($sourceHandle, $targetResource);
 325          fclose($sourceHandle);
 326      }
 327  }