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