diff --git a/src/Spout/Common/Escaper/ODS.php b/src/Spout/Common/Escaper/ODS.php index 3e252a7..86caeb3 100644 --- a/src/Spout/Common/Escaper/ODS.php +++ b/src/Spout/Common/Escaper/ODS.php @@ -2,6 +2,8 @@ namespace Box\Spout\Common\Escaper; +use Box\Spout\Common\Singleton; + /** * Class ODS * Provides functions to escape and unescape data for ODS files @@ -10,6 +12,8 @@ namespace Box\Spout\Common\Escaper; */ class ODS implements EscaperInterface { + use Singleton; + /** * Escapes the given string to make it compatible with XLSX * diff --git a/src/Spout/Common/Escaper/XLSX.php b/src/Spout/Common/Escaper/XLSX.php index 6f5bd1f..b5719a0 100644 --- a/src/Spout/Common/Escaper/XLSX.php +++ b/src/Spout/Common/Escaper/XLSX.php @@ -2,6 +2,8 @@ namespace Box\Spout\Common\Escaper; +use Box\Spout\Common\Singleton; + /** * Class XLSX * Provides functions to escape and unescape data for XLSX files @@ -10,13 +12,15 @@ namespace Box\Spout\Common\Escaper; */ class XLSX implements EscaperInterface { + use Singleton; + /** @var string[] Control characters to be escaped */ protected $controlCharactersEscapingMap; /** - * + * Initializes the singleton instance */ - public function __construct() + protected function init() { $this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap(); } diff --git a/src/Spout/Common/Helper/EncodingHelper.php b/src/Spout/Common/Helper/EncodingHelper.php index 3edd7a1..3a30aaa 100644 --- a/src/Spout/Common/Helper/EncodingHelper.php +++ b/src/Spout/Common/Helper/EncodingHelper.php @@ -59,7 +59,7 @@ class EncodingHelper { $byteOffsetToSkipBom = 0; - if ($this->hasBom($filePointer, $encoding)) { + if ($this->hasBOM($filePointer, $encoding)) { $bomUsed = $this->supportedEncodingsWithBom[$encoding]; // we skip the N first bytes diff --git a/src/Spout/Common/Singleton.php b/src/Spout/Common/Singleton.php new file mode 100644 index 0000000..015ede8 --- /dev/null +++ b/src/Spout/Common/Singleton.php @@ -0,0 +1,41 @@ +init(); + } + + /** + * Initializes the singleton + * @return void + */ + protected function init() {} + + final private function __wakeup() {} + final private function __clone() {} +} diff --git a/src/Spout/Reader/CSV/RowIterator.php b/src/Spout/Reader/CSV/RowIterator.php index 95b2596..5189dfc 100644 --- a/src/Spout/Reader/CSV/RowIterator.php +++ b/src/Spout/Reader/CSV/RowIterator.php @@ -57,6 +57,7 @@ class RowIterator implements IteratorInterface * @param string $fieldDelimiter Character that delimits fields * @param string $fieldEnclosure Character that enclose fields * @param string $encoding Encoding of the CSV file to be read + * @param string $endOfLineDelimiter End of line delimiter * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper */ public function __construct($filePointer, $fieldDelimiter, $fieldEnclosure, $encoding, $endOfLineDelimiter, $globalFunctionsHelper) diff --git a/src/Spout/Reader/ODS/Helper/CellValueFormatter.php b/src/Spout/Reader/ODS/Helper/CellValueFormatter.php index b39af21..38c1c85 100644 --- a/src/Spout/Reader/ODS/Helper/CellValueFormatter.php +++ b/src/Spout/Reader/ODS/Helper/CellValueFormatter.php @@ -48,7 +48,7 @@ class CellValueFormatter $this->shouldFormatDates = $shouldFormatDates; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->escaper = new \Box\Spout\Common\Escaper\ODS(); + $this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance(); } /** diff --git a/src/Spout/Reader/ODS/SheetIterator.php b/src/Spout/Reader/ODS/SheetIterator.php index d0010bd..f6cfdbe 100644 --- a/src/Spout/Reader/ODS/SheetIterator.php +++ b/src/Spout/Reader/ODS/SheetIterator.php @@ -15,6 +15,8 @@ use Box\Spout\Reader\Wrapper\XMLReader; */ class SheetIterator implements IteratorInterface { + const CONTENT_XML_FILE_PATH = 'content.xml'; + /** Definition of XML nodes name and attribute used to parse sheet data */ const XML_NODE_TABLE = 'table:table'; const XML_ATTRIBUTE_TABLE_NAME = 'table:name'; @@ -49,7 +51,7 @@ class SheetIterator implements IteratorInterface $this->xmlReader = new XMLReader(); /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->escaper = new \Box\Spout\Common\Escaper\ODS(); + $this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance(); } /** @@ -63,8 +65,8 @@ class SheetIterator implements IteratorInterface { $this->xmlReader->close(); - $contentXmlFilePath = $this->filePath . '#content.xml'; - if ($this->xmlReader->open('zip://' . $contentXmlFilePath) === false) { + if ($this->xmlReader->openFileInZip($this->filePath, self::CONTENT_XML_FILE_PATH) === false) { + $contentXmlFilePath = $this->filePath . '#' . self::CONTENT_XML_FILE_PATH; throw new IOException("Could not open \"{$contentXmlFilePath}\"."); } diff --git a/src/Spout/Reader/Wrapper/XMLReader.php b/src/Spout/Reader/Wrapper/XMLReader.php index 42bd92c..c979819 100644 --- a/src/Spout/Reader/Wrapper/XMLReader.php +++ b/src/Spout/Reader/Wrapper/XMLReader.php @@ -14,66 +14,44 @@ class XMLReader extends \XMLReader { use XMLInternalErrorsHelper; + const ZIP_WRAPPER = 'zip://'; + /** - * Set the URI containing the XML to parse - * @see \XMLReader::open + * Opens the XML Reader to read a file located inside a ZIP file. * - * @param string $URI URI pointing to the document - * @param string|null|void $encoding The document encoding - * @param int $options A bitmask of the LIBXML_* constants + * @param string $zipFilePath Path to the ZIP file + * @param string $fileInsideZipPath Relative or absolute path of the file inside the zip * @return bool TRUE on success or FALSE on failure */ - public function open($URI, $encoding = null, $options = 0) + public function openFileInZip($zipFilePath, $fileInsideZipPath) { $wasOpenSuccessful = false; - $realPathURI = $this->convertURIToUseRealPath($URI); + $realPathURI = $this->getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath); // HHVM does not check if file exists within zip file // @link https://github.com/facebook/hhvm/issues/5779 - if ($this->isRunningHHVM() && $this->isZipStream($realPathURI)) { + if ($this->isRunningHHVM()) { if ($this->fileExistsWithinZip($realPathURI)) { - $wasOpenSuccessful = parent::open($realPathURI, $encoding, $options|LIBXML_NONET); + $wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET); } } else { - $wasOpenSuccessful = parent::open($realPathURI, $encoding, $options|LIBXML_NONET); + $wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET); } return $wasOpenSuccessful; } /** - * Updates the given URI to use a real path. - * This is to avoid issues on some Windows setup. + * Returns the real path for the given path components. + * This is useful to avoid issues on some Windows setup. * - * @param string $URI URI - * @return string The URI using a real path + * @param string $zipFilePath Path to the ZIP file + * @param string $fileInsideZipPath Relative or absolute path of the file inside the zip + * @return string The real path URI */ - protected function convertURIToUseRealPath($URI) + public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath) { - $realPathURI = $URI; - - if ($this->isZipStream($URI)) { - if (preg_match('/zip:\/\/(.*)#(.*)/', $URI, $matches)) { - $documentPath = $matches[1]; - $documentInsideZipPath = $matches[2]; - $realPathURI = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath; - } - } else { - $realPathURI = realpath($URI); - } - - return $realPathURI; - } - - /** - * Returns whether the given URI is a zip stream. - * - * @param string $URI URI pointing to a document - * @return bool TRUE if URI is a zip stream, FALSE otherwise - */ - protected function isZipStream($URI) - { - return (strpos($URI, 'zip://') === 0); + return (self::ZIP_WRAPPER . realpath($zipFilePath) . '#' . $fileInsideZipPath); } /** @@ -138,9 +116,10 @@ class XMLReader extends \XMLReader */ public function readUntilNodeFound($nodeName) { - while (($wasReadSuccessful = $this->read()) && ($this->nodeType !== \XMLReader::ELEMENT || $this->name !== $nodeName)) { - // do nothing - } + do { + $wasReadSuccessful = $this->read(); + $isNotPositionedOnStartingNode = !$this->isPositionedOnStartingNode($nodeName); + } while ($wasReadSuccessful && $isNotPositionedOnStartingNode); return $wasReadSuccessful; } @@ -170,7 +149,7 @@ class XMLReader extends \XMLReader */ public function isPositionedOnStartingNode($nodeName) { - return ($this->nodeType === XMLReader::ELEMENT && $this->name === $nodeName); + return $this->isPositionedOnNode($nodeName, XMLReader::ELEMENT); } /** @@ -179,6 +158,22 @@ class XMLReader extends \XMLReader */ public function isPositionedOnEndingNode($nodeName) { - return ($this->nodeType === XMLReader::END_ELEMENT && $this->name === $nodeName); + return $this->isPositionedOnNode($nodeName, XMLReader::END_ELEMENT); + } + + /** + * @param string $nodeName + * @param int $nodeType + * @return bool Whether the XML Reader is currently positioned on the node with given name and type + */ + private function isPositionedOnNode($nodeName, $nodeType) + { + // In some cases, the node has a prefix (for instance, "" can also be ""). + // So if the given node name does not have a prefix, we need to look at the unprefixed name ("localName"). + // @see https://github.com/box/spout/issues/233 + $hasPrefix = (strpos($nodeName, ':') !== false); + $currentNodeName = ($hasPrefix) ? $this->name : $this->localName; + + return ($this->nodeType === $nodeType && $currentNodeName === $nodeName); } } diff --git a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php index 286d348..a272a2e 100644 --- a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php +++ b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php @@ -62,7 +62,7 @@ class CellValueFormatter $this->shouldFormatDates = $shouldFormatDates; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->escaper = new \Box\Spout\Common\Escaper\XLSX(); + $this->escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); } /** diff --git a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php b/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php index 1ba4e6a..750c53e 100644 --- a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php +++ b/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php @@ -80,7 +80,7 @@ class SharedStringsHelper $xmlReader = new XMLReader(); $sharedStringIndex = 0; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $escaper = new \Box\Spout\Common\Escaper\XLSX(); + $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); $sharedStringsFilePath = $this->getSharedStringsFilePath(); if ($xmlReader->open($sharedStringsFilePath) === false) { diff --git a/src/Spout/Reader/XLSX/Helper/SheetHelper.php b/src/Spout/Reader/XLSX/Helper/SheetHelper.php index 5f74f44..a6ff909 100644 --- a/src/Spout/Reader/XLSX/Helper/SheetHelper.php +++ b/src/Spout/Reader/XLSX/Helper/SheetHelper.php @@ -2,7 +2,7 @@ namespace Box\Spout\Reader\XLSX\Helper; -use Box\Spout\Reader\Wrapper\SimpleXMLElement; +use Box\Spout\Reader\Wrapper\XMLReader; use Box\Spout\Reader\XLSX\Sheet; /** @@ -17,10 +17,6 @@ class SheetHelper const WORKBOOK_XML_RELS_FILE_PATH = 'xl/_rels/workbook.xml.rels'; const WORKBOOK_XML_FILE_PATH = 'xl/workbook.xml'; - /** Namespaces for the XML files */ - const MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS = 'http://schemas.openxmlformats.org/package/2006/relationships'; - const MAIN_NAMESPACE_FOR_WORKBOOK_XML = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'; - /** @var string Path of the XLSX file being read */ protected $filePath; @@ -33,12 +29,6 @@ class SheetHelper /** @var bool Whether date/time values should be returned as PHP objects or be formatted as strings */ protected $shouldFormatDates; - /** @var \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representing the workbook.xml.rels file */ - protected $workbookXMLRelsAsXMLElement; - - /** @var \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representing the workbook.xml file */ - protected $workbookXMLAsXMLElement; - /** * @param string $filePath Path of the XLSX file being read * @param \Box\Spout\Reader\XLSX\Helper\SharedStringsHelper Helper to work with shared strings @@ -62,13 +52,21 @@ class SheetHelper public function getSheets() { $sheets = []; + $sheetIndex = 0; - // Starting from "workbook.xml" as this file is the source of truth for the sheets order - $workbookXMLElement = $this->getWorkbookXMLAsXMLElement(); - $sheetNodes = $workbookXMLElement->xpath('//ns:sheet'); + $xmlReader = new XMLReader(); + if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) { + while ($xmlReader->read()) { + if ($xmlReader->isPositionedOnStartingNode('sheet')) { + $sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex); + $sheetIndex++; + } else if ($xmlReader->isPositionedOnEndingNode('sheets')) { + // stop reading once all sheets have been read + break; + } + } - foreach ($sheetNodes as $sheetIndex => $sheetNode) { - $sheets[] = $this->getSheetFromSheetXMLNode($sheetNode, $sheetIndex); + $xmlReader->close(); } return $sheets; @@ -79,88 +77,56 @@ class SheetHelper * We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID * ("r:id" in "workbook.xml", "Id" in "workbook.xml.res"). * - * @param \Box\Spout\Reader\Wrapper\SimpleXMLElement $sheetNode XML Node describing the sheet, as defined in "workbook.xml" + * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml" * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based) * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance */ - protected function getSheetFromSheetXMLNode($sheetNode, $sheetIndexZeroBased) + protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased) { - // To retrieve namespaced attributes, some versions of LibXML will accept prefixing the attribute - // with the namespace directly (tested on LibXML 2.9.3). For older versions (tested on LibXML 2.7.8), - // attributes need to be retrieved without the namespace hint. - $sheetId = $sheetNode->getAttribute('r:id'); - if ($sheetId === null) { - $sheetId = $sheetNode->getAttribute('id'); - } - - $escapedSheetName = $sheetNode->getAttribute('name'); + $sheetId = $xmlReaderOnSheetNode->getAttribute('r:id'); + $escapedSheetName = $xmlReaderOnSheetNode->getAttribute('name'); /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $escaper = new \Box\Spout\Common\Escaper\XLSX(); + $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); $sheetName = $escaper->unescape($escapedSheetName); - // find the file path of the sheet, by looking at the "workbook.xml.res" file - $workbookXMLResElement = $this->getWorkbookXMLRelsAsXMLElement(); - $relationshipNodes = $workbookXMLResElement->xpath('//ns:Relationship[@Id="' . $sheetId . '"]'); - $relationshipNode = $relationshipNodes[0]; - - // In workbook.xml.rels, it is only "worksheets/sheet1.xml" - // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml" - $sheetDataXMLFilePath = '/xl/' . $relationshipNode->getAttribute('Target'); + $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId); return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $this->shouldFormatDates, $sheetIndexZeroBased, $sheetName); } /** - * Returns a representation of the workbook.xml.rels file, ready to be parsed. - * The returned value is cached. - * - * @return \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representating the workbook.xml.rels file + * @param string $sheetId The sheet ID, as defined in "workbook.xml" + * @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID */ - protected function getWorkbookXMLRelsAsXMLElement() + protected function getSheetDataXMLFilePathForSheetId($sheetId) { - if (!$this->workbookXMLRelsAsXMLElement) { - $this->workbookXMLRelsAsXMLElement = $this->getFileAsXMLElementWithNamespace( - self::WORKBOOK_XML_RELS_FILE_PATH, - self::MAIN_NAMESPACE_FOR_WORKBOOK_XML_RELS - ); + $sheetDataXMLFilePath = ''; + + // find the file path of the sheet, by looking at the "workbook.xml.res" file + $xmlReader = new XMLReader(); + if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) { + while ($xmlReader->read()) { + if ($xmlReader->isPositionedOnStartingNode('Relationship')) { + $relationshipSheetId = $xmlReader->getAttribute('Id'); + + if ($relationshipSheetId === $sheetId) { + // In workbook.xml.rels, it is only "worksheets/sheet1.xml" + // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml" + $sheetDataXMLFilePath = $xmlReader->getAttribute('Target'); + + // sometimes, the sheet data file path already contains "/xl/"... + if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) { + $sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath; + break; + } + } + } + } + + $xmlReader->close(); } - return $this->workbookXMLRelsAsXMLElement; - } - - /** - * Returns a representation of the workbook.xml file, ready to be parsed. - * The returned value is cached. - * - * @return \Box\Spout\Reader\Wrapper\SimpleXMLElement XML element representating the workbook.xml.rels file - */ - protected function getWorkbookXMLAsXMLElement() - { - if (!$this->workbookXMLAsXMLElement) { - $this->workbookXMLAsXMLElement = $this->getFileAsXMLElementWithNamespace( - self::WORKBOOK_XML_FILE_PATH, - self::MAIN_NAMESPACE_FOR_WORKBOOK_XML - ); - } - - return $this->workbookXMLAsXMLElement; - } - - /** - * Loads the contents of the given file in an XML parser and register the given XPath namespace. - * - * @param string $xmlFilePath The path of the XML file inside the XLSX file - * @param string $mainNamespace The main XPath namespace to register - * @return \Box\Spout\Reader\Wrapper\SimpleXMLElement The XML element representing the file - */ - protected function getFileAsXMLElementWithNamespace($xmlFilePath, $mainNamespace) - { - $xmlContents = $this->globalFunctionsHelper->file_get_contents('zip://' . $this->filePath . '#' . $xmlFilePath); - - $xmlElement = new SimpleXMLElement($xmlContents); - $xmlElement->registerXPathNamespace('ns', $mainNamespace); - - return $xmlElement; + return $sheetDataXMLFilePath; } } diff --git a/src/Spout/Reader/XLSX/Helper/StyleHelper.php b/src/Spout/Reader/XLSX/Helper/StyleHelper.php index 462433c..6fdcd80 100644 --- a/src/Spout/Reader/XLSX/Helper/StyleHelper.php +++ b/src/Spout/Reader/XLSX/Helper/StyleHelper.php @@ -76,10 +76,9 @@ class StyleHelper $this->customNumberFormats = []; $this->stylesAttributes = []; - $stylesXmlFilePath = $this->filePath .'#' . self::STYLES_XML_FILE_PATH; $xmlReader = new XMLReader(); - if ($xmlReader->open('zip://' . $stylesXmlFilePath)) { + if ($xmlReader->openFileInZip($this->filePath, self::STYLES_XML_FILE_PATH)) { while ($xmlReader->read()) { if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) { $numFmtsNode = new SimpleXMLElement($xmlReader->readOuterXml()); diff --git a/src/Spout/Writer/ODS/Internal/Worksheet.php b/src/Spout/Writer/ODS/Internal/Worksheet.php index 7a0df1a..cec30af 100644 --- a/src/Spout/Writer/ODS/Internal/Worksheet.php +++ b/src/Spout/Writer/ODS/Internal/Worksheet.php @@ -47,7 +47,7 @@ class Worksheet implements WorksheetInterface { $this->externalSheet = $externalSheet; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->stringsEscaper = new \Box\Spout\Common\Escaper\ODS(); + $this->stringsEscaper = \Box\Spout\Common\Escaper\ODS::getInstance(); $this->worksheetFilePath = $worksheetFilesFolder . '/sheet' . $externalSheet->getIndex() . '.xml'; $this->stringHelper = new StringHelper(); diff --git a/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php b/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php index 59df98a..786e62e 100644 --- a/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php +++ b/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php @@ -284,7 +284,7 @@ EOD; EOD; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $escaper = new \Box\Spout\Common\Escaper\XLSX(); + $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); /** @var Worksheet $worksheet */ foreach ($worksheets as $worksheet) { diff --git a/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php b/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php index 9d9639d..292b663 100644 --- a/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php +++ b/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php @@ -49,7 +49,7 @@ EOD; fwrite($this->sharedStringsFilePointer, $header); /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX(); + $this->stringsEscaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); } /** diff --git a/src/Spout/Writer/XLSX/Internal/Worksheet.php b/src/Spout/Writer/XLSX/Internal/Worksheet.php index ab959c6..2439db1 100644 --- a/src/Spout/Writer/XLSX/Internal/Worksheet.php +++ b/src/Spout/Writer/XLSX/Internal/Worksheet.php @@ -56,7 +56,7 @@ EOD; $this->shouldUseInlineStrings = $shouldUseInlineStrings; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->stringsEscaper = new \Box\Spout\Common\Escaper\XLSX(); + $this->stringsEscaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); $this->worksheetFilePath = $worksheetFilesFolder . '/' . strtolower($this->externalSheet->getName()) . '.xml'; $this->startSheet(); diff --git a/tests/Spout/Common/Escaper/XLSXTest.php b/tests/Spout/Common/Escaper/XLSXTest.php index 7c0b2db..289e2a2 100644 --- a/tests/Spout/Common/Escaper/XLSXTest.php +++ b/tests/Spout/Common/Escaper/XLSXTest.php @@ -35,7 +35,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase public function testEscape($stringToEscape, $expectedEscapedString) { /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $escaper = new \Box\Spout\Common\Escaper\XLSX(); + $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); $escapedString = $escaper->escape($stringToEscape); $this->assertEquals($expectedEscapedString, $escapedString, 'Incorrect escaped string'); @@ -67,7 +67,7 @@ class XLSXTest extends \PHPUnit_Framework_TestCase public function testUnescape($stringToUnescape, $expectedUnescapedString) { /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $escaper = new \Box\Spout\Common\Escaper\XLSX(); + $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); $unescapedString = $escaper->unescape($stringToUnescape); $this->assertEquals($expectedUnescapedString, $unescapedString, 'Incorrect escaped string'); diff --git a/tests/Spout/Reader/Wrapper/XMLReaderTest.php b/tests/Spout/Reader/Wrapper/XMLReaderTest.php index a4deacd..b16c648 100644 --- a/tests/Spout/Reader/Wrapper/XMLReaderTest.php +++ b/tests/Spout/Reader/Wrapper/XMLReaderTest.php @@ -20,12 +20,11 @@ class XMLReaderTest extends \PHPUnit_Framework_TestCase public function testOpenShouldFailIfFileInsideZipDoesNotExist() { $resourcePath = $this->getResourcePath('one_sheet_with_inline_strings.xlsx'); - $nonExistingXMLFilePath = 'zip://' . $resourcePath . '#path/to/fake/file.xml'; $xmlReader = new XMLReader(); // using "@" to prevent errors/warning to be displayed - $wasOpenSuccessful = @$xmlReader->open($nonExistingXMLFilePath); + $wasOpenSuccessful = @$xmlReader->openFileInZip($resourcePath, 'path/to/fake/file.xml'); $this->assertTrue($wasOpenSuccessful === false); } @@ -72,10 +71,9 @@ class XMLReaderTest extends \PHPUnit_Framework_TestCase public function testReadShouldThrowExceptionOnError() { $resourcePath = $this->getResourcePath('one_sheet_with_invalid_xml_characters.xlsx'); - $sheetDataXMLFilePath = 'zip://' . $resourcePath . '#xl/worksheets/sheet1.xml'; $xmlReader = new XMLReader(); - if ($xmlReader->open($sheetDataXMLFilePath) === false) { + if ($xmlReader->openFileInZip($resourcePath, 'xl/worksheets/sheet1.xml') === false) { $this->fail(); } @@ -95,43 +93,13 @@ class XMLReaderTest extends \PHPUnit_Framework_TestCase // The sharedStrings.xml file in "attack_billion_laughs.xlsx" contains // a doctype element that causes read errors $resourcePath = $this->getResourcePath('attack_billion_laughs.xlsx'); - $sheetDataXMLFilePath = 'zip://' . $resourcePath . '#xl/sharedStrings.xml'; $xmlReader = new XMLReader(); - if ($xmlReader->open($sheetDataXMLFilePath) !== false) { + if ($xmlReader->openFileInZip($resourcePath, 'xl/sharedStrings.xml') !== false) { @$xmlReader->next('sst'); } } - /** - * @return array - */ - public function dataProviderForTestIsZipStream() - { - return [ - ['/absolute/path/to/file.xlsx', false], - ['relative/path/to/file.xlsx', false], - ['php://temp', false], - ['zip:///absolute/path/to/file.xlsx', true], - ['zip://relative/path/to/file.xlsx', true], - ]; - } - - /** - * @dataProvider dataProviderForTestIsZipStream - * - * @param string $URI - * @param bool $expectedResult - * @return void - */ - public function testIsZipStream($URI, $expectedResult) - { - $xmlReader = new XMLReader(); - $isZipStream = \ReflectionHelper::callMethodOnObject($xmlReader, 'isZipStream', $URI); - - $this->assertEquals($expectedResult, $isZipStream); - } - /** * @return array */ @@ -167,35 +135,70 @@ class XMLReaderTest extends \PHPUnit_Framework_TestCase /** * @return array */ - public function dataProviderForTestConvertURIToUseRealPath() + public function dataProviderForTestGetRealPathURIForFileInZip() { $tempFolder = realpath(sys_get_temp_dir()); + $expectedRealPathURI = 'zip://' . $tempFolder . '/test.xlsx#test.xml'; return [ - ['/../../../' . $tempFolder . '/test.xlsx', $tempFolder . '/test.xlsx'], - [$tempFolder . '/test.xlsx', $tempFolder . '/test.xlsx'], - ['zip://' . $tempFolder . '/test.xlsx#test.xml', 'zip://' . $tempFolder . '/test.xlsx#test.xml'], - ['zip:///../../../' . $tempFolder . '/test.xlsx#test.xml', 'zip://' . $tempFolder . '/test.xlsx#test.xml'], + [$tempFolder, "$tempFolder/test.xlsx", 'test.xml', $expectedRealPathURI], + [$tempFolder, "/../../../$tempFolder/test.xlsx", 'test.xml', $expectedRealPathURI], ]; } /** - * @dataProvider dataProviderForTestConvertURIToUseRealPath + * @dataProvider dataProviderForTestGetRealPathURIForFileInZip * - * @param string $URI - * @param string $expectedConvertedURI + * @param string $tempFolder + * @param string $zipFilePath + * @param string $fileInsideZipPath + * @param string $expectedRealPathURI * @return void */ - public function testConvertURIToUseRealPath($URI, $expectedConvertedURI) + public function testGetRealPathURIForFileInZip($tempFolder, $zipFilePath, $fileInsideZipPath, $expectedRealPathURI) { - $tempFolder = sys_get_temp_dir(); touch($tempFolder . '/test.xlsx'); $xmlReader = new XMLReader(); - $convertedURI = \ReflectionHelper::callMethodOnObject($xmlReader, 'convertURIToUseRealPath', $URI); + $realPathURI = \ReflectionHelper::callMethodOnObject($xmlReader, 'getRealPathURIForFileInZip', $zipFilePath, $fileInsideZipPath); - $this->assertEquals($expectedConvertedURI, $convertedURI); + $this->assertEquals($expectedRealPathURI, $realPathURI); unlink($tempFolder . '/test.xlsx'); } + + /** + * @return array + */ + public function dataProviderForTestIsPositionedOnStartingAndEndingNode() + { + return [ + [''], // not prefixed + [''], // prefixed + ]; + } + + /** + * @dataProvider dataProviderForTestIsPositionedOnStartingAndEndingNode + * + * @param string $testXML + * @return void + */ + public function testIsPositionedOnStartingAndEndingNode($testXML) + { + $xmlReader = new XMLReader(); + $xmlReader->XML($testXML); + + // the first read moves the pointer to "" + $xmlReader->read(); + $this->assertTrue($xmlReader->isPositionedOnStartingNode('test')); + $this->assertFalse($xmlReader->isPositionedOnEndingNode('test')); + + // the seconds read moves the pointer to "" + $xmlReader->read(); + $this->assertFalse($xmlReader->isPositionedOnStartingNode('test')); + $this->assertTrue($xmlReader->isPositionedOnEndingNode('test')); + + $xmlReader->close(); + } } diff --git a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php index 92831ab..96c71a9 100644 --- a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php +++ b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php @@ -87,6 +87,10 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase */ public function dataProviderForTestFormatNumericCellValueWithNumbers() { + // Some test values exceed PHP_INT_MAX on 32-bit PHP. They are + // therefore converted to as doubles automatically by PHP. + $expectedBigNumberType = (PHP_INT_SIZE < 8 ? 'double' : 'integer'); + return [ [42, 42, 'integer'], [42.5, 42.5, 'double'], @@ -94,8 +98,8 @@ class CellValueFormatterTest extends \PHPUnit_Framework_TestCase [-42.5, -42.5, 'double'], ['42', 42, 'integer'], ['42.5', 42.5, 'double'], - [865640023012945, 865640023012945, 'integer'], - ['865640023012945', 865640023012945, 'integer'], + [865640023012945, 865640023012945, $expectedBigNumberType], + ['865640023012945', 865640023012945, $expectedBigNumberType], [865640023012945.5, 865640023012945.5, 'double'], ['865640023012945.5', 865640023012945.5, 'double'], [PHP_INT_MAX, PHP_INT_MAX, 'integer'], diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index 8620ed5..2703799 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -95,6 +95,23 @@ class ReaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expectedRows, $allRows); } + /** + * @return void + */ + public function testReadShouldSupportPrefixedXMLFiles() + { + // The XML files of this spreadsheet are prefixed. + // For instance, they use "" instead of "", etc. + $allRows = $this->getAllRowsForFile('sheet_with_prefixed_xml_files.xlsx'); + + $expectedRows = [ + ['s1 - A1', 's1 - B1', 's1 - C1'], + ['s1 - A2', 's1 - B2', 's1 - C2'], + ['s1 - A3', 's1 - B3', 's1 - C3'], + ]; + $this->assertEquals($expectedRows, $allRows); + } + /** * @return void */ diff --git a/tests/Spout/Writer/ODS/WriterTest.php b/tests/Spout/Writer/ODS/WriterTest.php index 4c67e4e..43383e4 100644 --- a/tests/Spout/Writer/ODS/WriterTest.php +++ b/tests/Spout/Writer/ODS/WriterTest.php @@ -538,10 +538,9 @@ class WriterTest extends \PHPUnit_Framework_TestCase private function moveReaderToCorrectTableNode($fileName, $sheetIndex) { $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToSheetFile = $resourcePath . '#content.xml'; $xmlReader = new XMLReader(); - $xmlReader->open('zip://' . $pathToSheetFile); + $xmlReader->openFileInZip($resourcePath, 'content.xml'); $xmlReader->readUntilNodeFound('table:table'); for ($i = 1; $i < $sheetIndex; $i++) { diff --git a/tests/Spout/Writer/ODS/WriterWithStyleTest.php b/tests/Spout/Writer/ODS/WriterWithStyleTest.php index cc3fc50..9888bfc 100644 --- a/tests/Spout/Writer/ODS/WriterWithStyleTest.php +++ b/tests/Spout/Writer/ODS/WriterWithStyleTest.php @@ -296,17 +296,18 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase $cellElements = []; $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToStylesXmlFile = $resourcePath . '#content.xml'; - $xmlReader = new \XMLReader(); - $xmlReader->open('zip://' . $pathToStylesXmlFile); + $xmlReader = new XMLReader(); + $xmlReader->openFileInZip($resourcePath, 'content.xml'); while ($xmlReader->read()) { - if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'table:table-cell' && $xmlReader->getAttribute('office:value-type') !== null) { + if ($xmlReader->isPositionedOnStartingNode('table:table-cell') && $xmlReader->getAttribute('office:value-type') !== null) { $cellElements[] = $xmlReader->expand(); } } + $xmlReader->close(); + return $cellElements; } @@ -319,17 +320,18 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase $cellStyleElements = []; $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToStylesXmlFile = $resourcePath . '#content.xml'; - $xmlReader = new \XMLReader(); - $xmlReader->open('zip://' . $pathToStylesXmlFile); + $xmlReader = new XMLReader(); + $xmlReader->openFileInZip($resourcePath, 'content.xml'); while ($xmlReader->read()) { - if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'style:style' && $xmlReader->getAttribute('style:family') === 'table-cell') { + if ($xmlReader->isPositionedOnStartingNode('style:style') && $xmlReader->getAttribute('style:family') === 'table-cell') { $cellStyleElements[] = $xmlReader->expand(); } } + $xmlReader->close(); + return $cellStyleElements; } @@ -341,10 +343,9 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase private function getXmlSectionFromStylesXmlFile($fileName, $section) { $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToStylesXmlFile = $resourcePath . '#styles.xml'; $xmlReader = new XMLReader(); - $xmlReader->open('zip://' . $pathToStylesXmlFile); + $xmlReader->openFileInZip($resourcePath, 'styles.xml'); $xmlReader->readUntilNodeFound($section); return $xmlReader->expand(); diff --git a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php index 9a531b9..d43b566 100644 --- a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php +++ b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php @@ -293,13 +293,16 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase private function getXmlSectionFromStylesXmlFile($fileName, $section) { $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToStylesXmlFile = $resourcePath . '#xl/styles.xml'; $xmlReader = new XMLReader(); - $xmlReader->open('zip://' . $pathToStylesXmlFile); + $xmlReader->openFileInZip($resourcePath, 'xl/styles.xml'); $xmlReader->readUntilNodeFound($section); - return $xmlReader->expand(); + $xmlSection = $xmlReader->expand(); + + $xmlReader->close(); + + return $xmlSection; } /** @@ -311,17 +314,18 @@ class WriterWithStyleTest extends \PHPUnit_Framework_TestCase $cellElements = []; $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToStylesXmlFile = $resourcePath . '#xl/worksheets/sheet1.xml'; - $xmlReader = new \XMLReader(); - $xmlReader->open('zip://' . $pathToStylesXmlFile); + $xmlReader = new XMLReader(); + $xmlReader->openFileInZip($resourcePath, 'xl/worksheets/sheet1.xml'); while ($xmlReader->read()) { - if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'c') { + if ($xmlReader->isPositionedOnStartingNode('c')) { $cellElements[] = $xmlReader->expand(); } } + $xmlReader->close(); + return $cellElements; } diff --git a/tests/resources/xlsx/sheet_with_prefixed_xml_files.xlsx b/tests/resources/xlsx/sheet_with_prefixed_xml_files.xlsx new file mode 100644 index 0000000..d8bbc39 Binary files /dev/null and b/tests/resources/xlsx/sheet_with_prefixed_xml_files.xlsx differ