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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body