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