280 lines
10 KiB
PHP
280 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Box\Spout\Writer\ODS\Helper;
|
|
|
|
use Box\Spout\Writer\Common\Helper\ZipHelper;
|
|
use Box\Spout\Writer\ODS\Internal\Worksheet;
|
|
|
|
/**
|
|
* Class FileSystemHelper
|
|
* This class provides helper functions to help with the file system operations
|
|
* like files/folders creation & deletion for ODS files
|
|
*
|
|
* @package Box\Spout\Writer\ODS\Helper
|
|
*/
|
|
class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
|
|
{
|
|
const APP_NAME = 'Spout';
|
|
const MIMETYPE = 'application/vnd.oasis.opendocument.spreadsheet';
|
|
|
|
const META_INF_FOLDER_NAME = 'META-INF';
|
|
const SHEETS_CONTENT_TEMP_FOLDER_NAME = 'worksheets-temp';
|
|
|
|
const MANIFEST_XML_FILE_NAME = 'manifest.xml';
|
|
const CONTENT_XML_FILE_NAME = 'content.xml';
|
|
const META_XML_FILE_NAME = 'meta.xml';
|
|
const MIMETYPE_FILE_NAME = 'mimetype';
|
|
const STYLES_XML_FILE_NAME = 'styles.xml';
|
|
|
|
/** @var string Path to the root folder inside the temp folder where the files to create the ODS will be stored */
|
|
protected $rootFolder;
|
|
|
|
/** @var string Path to the "META-INF" folder inside the root folder */
|
|
protected $metaInfFolder;
|
|
|
|
/** @var string Path to the temp folder, inside the root folder, where specific sheets content will be written to */
|
|
protected $sheetsContentTempFolder;
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getRootFolder()
|
|
{
|
|
return $this->rootFolder;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getSheetsContentTempFolder()
|
|
{
|
|
return $this->sheetsContentTempFolder;
|
|
}
|
|
|
|
/**
|
|
* Creates all the folders needed to create a ODS file, as well as the files that won't change.
|
|
*
|
|
* @return void
|
|
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
|
|
*/
|
|
public function createBaseFilesAndFolders()
|
|
{
|
|
$this
|
|
->createRootFolder()
|
|
->createMetaInfoFolderAndFile()
|
|
->createSheetsContentTempFolder()
|
|
->createMetaFile()
|
|
->createMimetypeFile();
|
|
}
|
|
|
|
/**
|
|
* Creates the folder that will be used as root
|
|
*
|
|
* @return FileSystemHelper
|
|
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
|
|
*/
|
|
protected function createRootFolder()
|
|
{
|
|
$this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('ods'));
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it
|
|
*
|
|
* @return FileSystemHelper
|
|
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file
|
|
*/
|
|
protected function createMetaInfoFolderAndFile()
|
|
{
|
|
$this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME);
|
|
|
|
$this->createManifestFile();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Creates the "manifest.xml" file under the "META-INF" folder (under root)
|
|
*
|
|
* @return FileSystemHelper
|
|
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
|
|
*/
|
|
protected function createManifestFile()
|
|
{
|
|
$manifestXmlFileContents = <<<EOD
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">
|
|
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>
|
|
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
|
|
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
|
|
<manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
|
|
</manifest:manifest>
|
|
EOD;
|
|
|
|
$this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Creates the temp folder where specific sheets content will be written to.
|
|
* This folder is not part of the final ODS file and is only used to be able to jump between sheets.
|
|
*
|
|
* @return FileSystemHelper
|
|
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
|
|
*/
|
|
protected function createSheetsContentTempFolder()
|
|
{
|
|
$this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, self::SHEETS_CONTENT_TEMP_FOLDER_NAME);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Creates the "meta.xml" file under the root folder
|
|
*
|
|
* @return FileSystemHelper
|
|
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
|
|
*/
|
|
protected function createMetaFile()
|
|
{
|
|
$appName = self::APP_NAME;
|
|
$createdDate = (new \DateTime())->format(\DateTime::W3C);
|
|
|
|
$metaXmlFileContents = <<<EOD
|
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<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">
|
|
<office:meta>
|
|
<dc:creator>$appName</dc:creator>
|
|
<meta:creation-date>$createdDate</meta:creation-date>
|
|
<dc:date>$createdDate</dc:date>
|
|
</office:meta>
|
|
</office:document-meta>
|
|
EOD;
|
|
|
|
$this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Creates the "mimetype" file under the root folder
|
|
*
|
|
* @return FileSystemHelper
|
|
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
|
|
*/
|
|
protected function createMimetypeFile()
|
|
{
|
|
$this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Creates the "content.xml" file under the root folder
|
|
*
|
|
* @param Worksheet[] $worksheets
|
|
* @param StyleHelper $styleHelper
|
|
* @return FileSystemHelper
|
|
*/
|
|
public function createContentFile($worksheets, $styleHelper)
|
|
{
|
|
$contentXmlFileContents = <<<EOD
|
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<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">
|
|
EOD;
|
|
|
|
$contentXmlFileContents .= $styleHelper->getContentXmlFontFaceSectionContent();
|
|
$contentXmlFileContents .= $styleHelper->getContentXmlAutomaticStylesSectionContent(count($worksheets));
|
|
|
|
$contentXmlFileContents .= '<office:body><office:spreadsheet>';
|
|
|
|
$this->createFileWithContents($this->rootFolder, self::CONTENT_XML_FILE_NAME, $contentXmlFileContents);
|
|
|
|
// Append sheets content to "content.xml"
|
|
$contentXmlFilePath = $this->rootFolder . '/' . self::CONTENT_XML_FILE_NAME;
|
|
$contentXmlHandle = fopen($contentXmlFilePath, 'a');
|
|
|
|
foreach ($worksheets as $worksheet) {
|
|
// write the "<table:table>" node, with the final sheet's name
|
|
fwrite($contentXmlHandle, $worksheet->getTableElementStartAsString());
|
|
|
|
$worksheetFilePath = $worksheet->getWorksheetFilePath();
|
|
$this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle);
|
|
|
|
fwrite($contentXmlHandle, '</table:table>');
|
|
}
|
|
|
|
$contentXmlFileContents = '</office:spreadsheet></office:body></office:document-content>';
|
|
|
|
fwrite($contentXmlHandle, $contentXmlFileContents);
|
|
fclose($contentXmlHandle);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Streams the content of the file at the given path into the target resource.
|
|
* Depending on which mode the target resource was created with, it will truncate then copy
|
|
* or append the content to the target file.
|
|
*
|
|
* @param string $sourceFilePath Path of the file whose content will be copied
|
|
* @param resource $targetResource Target resource that will receive the content
|
|
* @return void
|
|
*/
|
|
protected function copyFileContentsToTarget($sourceFilePath, $targetResource)
|
|
{
|
|
$sourceHandle = fopen($sourceFilePath, 'r');
|
|
stream_copy_to_stream($sourceHandle, $targetResource);
|
|
fclose($sourceHandle);
|
|
}
|
|
|
|
/**
|
|
* Deletes the temporary folder where sheets content was stored.
|
|
*
|
|
* @return FileSystemHelper
|
|
*/
|
|
public function deleteWorksheetTempFolder()
|
|
{
|
|
$this->deleteFolderRecursively($this->sheetsContentTempFolder);
|
|
return $this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates the "styles.xml" file under the root folder
|
|
*
|
|
* @param StyleHelper $styleHelper
|
|
* @param int $numWorksheets Number of created worksheets
|
|
* @return FileSystemHelper
|
|
*/
|
|
public function createStylesFile($styleHelper, $numWorksheets)
|
|
{
|
|
$stylesXmlFileContents = $styleHelper->getStylesXMLFileContent($numWorksheets);
|
|
$this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Zips the root folder and streams the contents of the zip into the given stream
|
|
*
|
|
* @param resource $streamPointer Pointer to the stream to copy the zip
|
|
* @return void
|
|
*/
|
|
public function zipRootFolderAndCopyToStream($streamPointer)
|
|
{
|
|
$zipHelper = new ZipHelper($this->rootFolder);
|
|
|
|
// In order to have the file's mime type detected properly, files need to be added
|
|
// to the zip file in a particular order.
|
|
// @see http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/
|
|
$zipHelper->addUncompressedFileToArchive($this->rootFolder, self::MIMETYPE_FILE_NAME);
|
|
|
|
$zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
|
|
$zipHelper->closeArchiveAndCopyToStream($streamPointer);
|
|
|
|
// once the zip is copied, remove it
|
|
$this->deleteFile($zipHelper->getZipFilePath());
|
|
}
|
|
}
|