diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index ced31605..a082ff27 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -4755,11 +4755,6 @@ parameters:
count: 1
path: src/PhpSpreadsheet/Reader/Xml.php
- -
- message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:\\$mappings has no typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
-
message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|false given\\.$#"
count: 1
@@ -4775,66 +4770,6 @@ parameters:
count: 1
path: src/PhpSpreadsheet/Reader/Xml.php
- -
- message: "#^Cannot call method getNamespaces\\(\\) on SimpleXMLElement\\|false\\.$#"
- count: 3
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Cannot call method children\\(\\) on SimpleXMLElement\\|false\\.$#"
- count: 3
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:identifyFixedStyleValue\\(\\) has no return typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:identifyFixedStyleValue\\(\\) has parameter \\$styleAttributeValue with no typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:identifyFixedStyleValue\\(\\) has parameter \\$styleList with no typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:hex2str\\(\\) has no return typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:hex2str\\(\\) has parameter \\$hex with no typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Cannot access property \\$DocumentProperties on SimpleXMLElement\\|false\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Parameter \\#1 \\$timestamp of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setCreated\\(\\) expects int\\|string\\|null, int\\|false given\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Parameter \\#1 \\$timestamp of method PhpOffice\\\\PhpSpreadsheet\\\\Document\\\\Properties\\:\\:setModified\\(\\) expects int\\|string\\|null, int\\|false given\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Parameter \\#2 \\$callback of function preg_replace_callback expects callable\\(\\)\\: mixed, array\\('self', 'hex2str'\\) given\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Parameter \\#1 \\$xml of method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:parseStyles\\(\\) expects SimpleXMLElement, SimpleXMLElement\\|false given\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
-
message: "#^Cannot call method setWidth\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\ColumnDimension\\|null\\.$#"
count: 1
@@ -4845,46 +4780,6 @@ parameters:
count: 1
path: src/PhpSpreadsheet/Reader/Xml.php
- -
- message: "#^Cannot access property \\$Names on SimpleXMLElement\\|false\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:parseRichText\\(\\) has no return typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:parseRichText\\(\\) has parameter \\$is with no typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:\\$borderPositions has no typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:parseStyleBorders\\(\\) has parameter \\$styleID with no typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:\\$underlineStyles has no typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:parseStyleInterior\\(\\) has parameter \\$styleID with no typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
- -
- message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xml\\:\\:parseStyleNumberFormat\\(\\) has parameter \\$styleID with no typehint specified\\.$#"
- count: 1
- path: src/PhpSpreadsheet/Reader/Xml.php
-
-
message: "#^Parameter \\#2 \\$cmp_function of function uksort expects callable\\(mixed, mixed\\)\\: int, array\\('self', 'cellReverseSort'\\) given\\.$#"
count: 4
diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php
index a900ad9b..282cd528 100644
--- a/src/PhpSpreadsheet/Reader/Xml.php
+++ b/src/PhpSpreadsheet/Reader/Xml.php
@@ -8,20 +8,16 @@ use PhpOffice\PhpSpreadsheet\Cell\AddressHelper;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\DefinedName;
-use PhpOffice\PhpSpreadsheet\Document\Properties;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use PhpOffice\PhpSpreadsheet\Reader\Xml\PageSettings;
+use PhpOffice\PhpSpreadsheet\Reader\Xml\Properties;
+use PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Settings;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
-use PhpOffice\PhpSpreadsheet\Style\Alignment;
-use PhpOffice\PhpSpreadsheet\Style\Border;
-use PhpOffice\PhpSpreadsheet\Style\Borders;
-use PhpOffice\PhpSpreadsheet\Style\Fill;
-use PhpOffice\PhpSpreadsheet\Style\Font;
use SimpleXMLElement;
/**
@@ -47,52 +43,12 @@ class Xml extends BaseReader
private $fileContents = '';
- private static $mappings = [
- 'borderStyle' => [
- '1continuous' => Border::BORDER_THIN,
- '1dash' => Border::BORDER_DASHED,
- '1dashdot' => Border::BORDER_DASHDOT,
- '1dashdotdot' => Border::BORDER_DASHDOTDOT,
- '1dot' => Border::BORDER_DOTTED,
- '1double' => Border::BORDER_DOUBLE,
- '2continuous' => Border::BORDER_MEDIUM,
- '2dash' => Border::BORDER_MEDIUMDASHED,
- '2dashdot' => Border::BORDER_MEDIUMDASHDOT,
- '2dashdotdot' => Border::BORDER_MEDIUMDASHDOTDOT,
- '2dot' => Border::BORDER_DOTTED,
- '2double' => Border::BORDER_DOUBLE,
- '3continuous' => Border::BORDER_THICK,
- '3dash' => Border::BORDER_MEDIUMDASHED,
- '3dashdot' => Border::BORDER_MEDIUMDASHDOT,
- '3dashdotdot' => Border::BORDER_MEDIUMDASHDOTDOT,
- '3dot' => Border::BORDER_DOTTED,
- '3double' => Border::BORDER_DOUBLE,
- ],
- 'fillType' => [
- 'solid' => Fill::FILL_SOLID,
- 'gray75' => Fill::FILL_PATTERN_DARKGRAY,
- 'gray50' => Fill::FILL_PATTERN_MEDIUMGRAY,
- 'gray25' => Fill::FILL_PATTERN_LIGHTGRAY,
- 'gray125' => Fill::FILL_PATTERN_GRAY125,
- 'gray0625' => Fill::FILL_PATTERN_GRAY0625,
- 'horzstripe' => Fill::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe
- 'vertstripe' => Fill::FILL_PATTERN_DARKVERTICAL, // vertical stripe
- 'reversediagstripe' => Fill::FILL_PATTERN_DARKUP, // reverse diagonal stripe
- 'diagstripe' => Fill::FILL_PATTERN_DARKDOWN, // diagonal stripe
- 'diagcross' => Fill::FILL_PATTERN_DARKGRID, // diagoanl crosshatch
- 'thickdiagcross' => Fill::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch
- 'thinhorzstripe' => Fill::FILL_PATTERN_LIGHTHORIZONTAL,
- 'thinvertstripe' => Fill::FILL_PATTERN_LIGHTVERTICAL,
- 'thinreversediagstripe' => Fill::FILL_PATTERN_LIGHTUP,
- 'thindiagstripe' => Fill::FILL_PATTERN_LIGHTDOWN,
- 'thinhorzcross' => Fill::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch
- 'thindiagcross' => Fill::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch
- ],
- ];
-
public static function xmlMappings(): array
{
- return self::$mappings;
+ return array_merge(
+ Style\Fill::FILL_MAPPINGS,
+ Style\Border::BORDER_MAPPINGS
+ );
}
/**
@@ -174,20 +130,23 @@ class Xml extends BaseReader
/**
* Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
*
- * @param string $pFilename
+ * @param string $filename
*
* @return array
*/
- public function listWorksheetNames($pFilename)
+ public function listWorksheetNames($filename)
{
- File::assertFile($pFilename);
- if (!$this->canRead($pFilename)) {
- throw new Exception($pFilename . ' is an Invalid Spreadsheet file.');
+ File::assertFile($filename);
+ if (!$this->canRead($filename)) {
+ throw new Exception($filename . ' is an Invalid Spreadsheet file.');
}
$worksheetNames = [];
- $xml = $this->trySimpleXMLLoadString($pFilename);
+ $xml = $this->trySimpleXMLLoadString($filename);
+ if ($xml === false) {
+ throw new Exception("Problem reading {$filename}");
+ }
$namespaces = $xml->getNamespaces(true);
@@ -203,20 +162,23 @@ class Xml extends BaseReader
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*
- * @param string $pFilename
+ * @param string $filename
*
* @return array
*/
- public function listWorksheetInfo($pFilename)
+ public function listWorksheetInfo($filename)
{
- File::assertFile($pFilename);
- if (!$this->canRead($pFilename)) {
- throw new Exception($pFilename . ' is an Invalid Spreadsheet file.');
+ File::assertFile($filename);
+ if (!$this->canRead($filename)) {
+ throw new Exception($filename . ' is an Invalid Spreadsheet file.');
}
$worksheetInfo = [];
- $xml = $this->trySimpleXMLLoadString($pFilename);
+ $xml = $this->trySimpleXMLLoadString($filename);
+ if ($xml === false) {
+ throw new Exception("Problem reading {$filename}");
+ }
$namespaces = $xml->getNamespaces(true);
@@ -274,150 +236,44 @@ class Xml extends BaseReader
/**
* Loads Spreadsheet from file.
*
- * @param string $pFilename
+ * @param string $filename
*
* @return Spreadsheet
*/
- public function load($pFilename)
+ public function load($filename)
{
// Create new Spreadsheet
$spreadsheet = new Spreadsheet();
$spreadsheet->removeSheetByIndex(0);
// Load into this instance
- return $this->loadIntoExisting($pFilename, $spreadsheet);
- }
-
- private static function identifyFixedStyleValue($styleList, &$styleAttributeValue)
- {
- $returnValue = false;
- $styleAttributeValue = strtolower($styleAttributeValue);
- foreach ($styleList as $style) {
- if ($styleAttributeValue == strtolower($style)) {
- $styleAttributeValue = $style;
- $returnValue = true;
-
- break;
- }
- }
-
- return $returnValue;
- }
-
- protected static function hex2str($hex)
- {
- return mb_chr((int) hexdec($hex[1]), 'UTF-8');
+ return $this->loadIntoExisting($filename, $spreadsheet);
}
/**
* Loads from file into Spreadsheet instance.
*
- * @param string $pFilename
+ * @param string $filename
*
* @return Spreadsheet
*/
- public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet)
+ public function loadIntoExisting($filename, Spreadsheet $spreadsheet)
{
- File::assertFile($pFilename);
- if (!$this->canRead($pFilename)) {
- throw new Exception($pFilename . ' is an Invalid Spreadsheet file.');
+ File::assertFile($filename);
+ if (!$this->canRead($filename)) {
+ throw new Exception($filename . ' is an Invalid Spreadsheet file.');
}
- $xml = $this->trySimpleXMLLoadString($pFilename);
+ $xml = $this->trySimpleXMLLoadString($filename);
+ if ($xml === false) {
+ throw new Exception("Problem reading {$filename}");
+ }
$namespaces = $xml->getNamespaces(true);
- $docProps = $spreadsheet->getProperties();
- if (isset($xml->DocumentProperties[0])) {
- foreach ($xml->DocumentProperties[0] as $propertyName => $propertyValue) {
- $stringValue = (string) $propertyValue;
- switch ($propertyName) {
- case 'Title':
- $docProps->setTitle($stringValue);
+ (new Properties($spreadsheet))->readProperties($xml, $namespaces);
- break;
- case 'Subject':
- $docProps->setSubject($stringValue);
-
- break;
- case 'Author':
- $docProps->setCreator($stringValue);
-
- break;
- case 'Created':
- $creationDate = strtotime($stringValue);
- $docProps->setCreated($creationDate);
-
- break;
- case 'LastAuthor':
- $docProps->setLastModifiedBy($stringValue);
-
- break;
- case 'LastSaved':
- $lastSaveDate = strtotime($stringValue);
- $docProps->setModified($lastSaveDate);
-
- break;
- case 'Company':
- $docProps->setCompany($stringValue);
-
- break;
- case 'Category':
- $docProps->setCategory($stringValue);
-
- break;
- case 'Manager':
- $docProps->setManager($stringValue);
-
- break;
- case 'Keywords':
- $docProps->setKeywords($stringValue);
-
- break;
- case 'Description':
- $docProps->setDescription($stringValue);
-
- break;
- }
- }
- }
- if (isset($xml->CustomDocumentProperties)) {
- foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) {
- $propertyAttributes = self::getAttributes($propertyValue, $namespaces['dt']);
- $propertyName = preg_replace_callback('/_x([0-9a-f]{4})_/i', ['self', 'hex2str'], $propertyName);
- $propertyType = Properties::PROPERTY_TYPE_UNKNOWN;
- switch ((string) $propertyAttributes) {
- case 'string':
- $propertyType = Properties::PROPERTY_TYPE_STRING;
- $propertyValue = trim((string) $propertyValue);
-
- break;
- case 'boolean':
- $propertyType = Properties::PROPERTY_TYPE_BOOLEAN;
- $propertyValue = (bool) $propertyValue;
-
- break;
- case 'integer':
- $propertyType = Properties::PROPERTY_TYPE_INTEGER;
- $propertyValue = (int) $propertyValue;
-
- break;
- case 'float':
- $propertyType = Properties::PROPERTY_TYPE_FLOAT;
- $propertyValue = (float) $propertyValue;
-
- break;
- case 'dateTime.tz':
- $propertyType = Properties::PROPERTY_TYPE_DATE;
- $propertyValue = strtotime(trim((string) $propertyValue));
-
- break;
- }
- $docProps->setCustomProperty($propertyName, $propertyValue, $propertyType);
- }
- }
-
- $this->parseStyles($xml, $namespaces);
+ $this->styles = (new Style())->parseStyles($xml, $namespaces);
$worksheetID = 0;
$xml_ss = $xml->children($namespaces['ss']);
@@ -587,14 +443,7 @@ class Xml extends BaseReader
}
if (isset($cell->Comment)) {
- $commentAttributes = $cell->Comment->attributes($namespaces['ss']);
- $author = 'unknown';
- if (isset($commentAttributes->Author)) {
- $author = (string) $commentAttributes->Author;
- }
- $node = $cell->Comment->Data->asXML();
- $annotation = strip_tags((string) $node);
- $spreadsheet->getActiveSheet()->getComment($columnID . $rowID)->setAuthor($author)->setText($this->parseRichText($annotation));
+ $this->parseCellComment($cell->Comment, $namespaces, $spreadsheet, $columnID, $rowID);
}
if (isset($cell_ss['StyleID'])) {
@@ -603,7 +452,8 @@ class Xml extends BaseReader
//if (!$spreadsheet->getActiveSheet()->cellExists($columnID . $rowID)) {
// $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValue(null);
//}
- $spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($this->styles[$style]);
+ $spreadsheet->getActiveSheet()->getStyle($cellRange)
+ ->applyFromArray($this->styles[$style]);
}
}
++$columnID;
@@ -652,250 +502,39 @@ class Xml extends BaseReader
return $spreadsheet;
}
- protected function parseRichText($is)
+ protected function parseCellComment(
+ SimpleXMLElement $comment,
+ array $namespaces,
+ Spreadsheet $spreadsheet,
+ string $columnID,
+ int $rowID
+ ): void {
+ $commentAttributes = $comment->attributes($namespaces['ss']);
+ $author = 'unknown';
+ if (isset($commentAttributes->Author)) {
+ $author = (string) $commentAttributes->Author;
+ }
+
+ $node = $comment->Data->asXML();
+ $annotation = strip_tags((string) $node);
+ $spreadsheet->getActiveSheet()->getComment($columnID . $rowID)
+ ->setAuthor($author)
+ ->setText($this->parseRichText($annotation));
+ }
+
+ protected function parseRichText(string $annotation): RichText
{
$value = new RichText();
- $value->createText($is);
+ $value->createText($annotation);
return $value;
}
- private function parseStyles(SimpleXMLElement $xml, array $namespaces): void
- {
- if (!isset($xml->Styles)) {
- return;
- }
-
- foreach ($xml->Styles[0] as $style) {
- $style_ss = self::getAttributes($style, $namespaces['ss']);
- $styleID = (string) $style_ss['ID'];
- $this->styles[$styleID] = $this->styles['Default'] ?? [];
- foreach ($style as $styleType => $styleDatax) {
- $styleData = $styleDatax ?? new SimpleXMLElement('');
- $styleAttributes = $styleData->attributes($namespaces['ss']);
- switch ($styleType) {
- case 'Alignment':
- $this->parseStyleAlignment($styleID, $styleAttributes);
-
- break;
- case 'Borders':
- $this->parseStyleBorders($styleID, $styleData, $namespaces);
-
- break;
- case 'Font':
- $this->parseStyleFont($styleID, $styleAttributes);
-
- break;
- case 'Interior':
- $this->parseStyleInterior($styleID, $styleAttributes);
-
- break;
- case 'NumberFormat':
- $this->parseStyleNumberFormat($styleID, $styleAttributes);
-
- break;
- }
- }
- }
- }
-
- /**
- * @param string $styleID
- */
- private function parseStyleAlignment($styleID, SimpleXMLElement $styleAttributes): void
- {
- $verticalAlignmentStyles = [
- Alignment::VERTICAL_BOTTOM,
- Alignment::VERTICAL_TOP,
- Alignment::VERTICAL_CENTER,
- Alignment::VERTICAL_JUSTIFY,
- ];
- $horizontalAlignmentStyles = [
- Alignment::HORIZONTAL_GENERAL,
- Alignment::HORIZONTAL_LEFT,
- Alignment::HORIZONTAL_RIGHT,
- Alignment::HORIZONTAL_CENTER,
- Alignment::HORIZONTAL_CENTER_CONTINUOUS,
- Alignment::HORIZONTAL_JUSTIFY,
- ];
-
- foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
- $styleAttributeValue = (string) $styleAttributeValue;
- switch ($styleAttributeKey) {
- case 'Vertical':
- if (self::identifyFixedStyleValue($verticalAlignmentStyles, $styleAttributeValue)) {
- $this->styles[$styleID]['alignment']['vertical'] = $styleAttributeValue;
- }
-
- break;
- case 'Horizontal':
- if (self::identifyFixedStyleValue($horizontalAlignmentStyles, $styleAttributeValue)) {
- $this->styles[$styleID]['alignment']['horizontal'] = $styleAttributeValue;
- }
-
- break;
- case 'WrapText':
- $this->styles[$styleID]['alignment']['wrapText'] = true;
-
- break;
- case 'Rotate':
- $this->styles[$styleID]['alignment']['textRotation'] = $styleAttributeValue;
-
- break;
- }
- }
- }
-
- private static $borderPositions = ['top', 'left', 'bottom', 'right'];
-
- private function parseStyleBorders($styleID, SimpleXMLElement $styleData, array $namespaces): void
- {
- $diagonalDirection = '';
- $borderPosition = '';
- foreach ($styleData->Border as $borderStyle) {
- $borderAttributes = self::getAttributes($borderStyle, $namespaces['ss']);
- $thisBorder = [];
- $style = (string) $borderAttributes->Weight;
- $style .= strtolower((string) $borderAttributes->LineStyle);
- $thisBorder['borderStyle'] = self::$mappings['borderStyle'][$style] ?? Border::BORDER_NONE;
- foreach ($borderAttributes as $borderStyleKey => $borderStyleValuex) {
- $borderStyleValue = (string) $borderStyleValuex;
- switch ($borderStyleKey) {
- case 'Position':
- $borderStyleValue = strtolower($borderStyleValue);
- if (in_array($borderStyleValue, self::$borderPositions)) {
- $borderPosition = $borderStyleValue;
- } elseif ($borderStyleValue == 'diagonalleft') {
- $diagonalDirection = $diagonalDirection ? Borders::DIAGONAL_BOTH : Borders::DIAGONAL_DOWN;
- } elseif ($borderStyleValue == 'diagonalright') {
- $diagonalDirection = $diagonalDirection ? Borders::DIAGONAL_BOTH : Borders::DIAGONAL_UP;
- }
-
- break;
- case 'Color':
- $borderColour = substr($borderStyleValue, 1);
- $thisBorder['color']['rgb'] = $borderColour;
-
- break;
- }
- }
- if ($borderPosition) {
- $this->styles[$styleID]['borders'][$borderPosition] = $thisBorder;
- } elseif ($diagonalDirection) {
- $this->styles[$styleID]['borders']['diagonalDirection'] = $diagonalDirection;
- $this->styles[$styleID]['borders']['diagonal'] = $thisBorder;
- }
- }
- }
-
private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement
{
- return ($simple === null) ? new SimpleXMLElement('') : ($simple->attributes($node) ?? new SimpleXMLElement(''));
- }
-
- private static $underlineStyles = [
- Font::UNDERLINE_NONE,
- Font::UNDERLINE_DOUBLE,
- Font::UNDERLINE_DOUBLEACCOUNTING,
- Font::UNDERLINE_SINGLE,
- Font::UNDERLINE_SINGLEACCOUNTING,
- ];
-
- private function parseStyleFontUnderline(string $styleID, string $styleAttributeValue): void
- {
- if (self::identifyFixedStyleValue(self::$underlineStyles, $styleAttributeValue)) {
- $this->styles[$styleID]['font']['underline'] = $styleAttributeValue;
- }
- }
-
- private function parseStyleFontVerticalAlign(string $styleID, string $styleAttributeValue): void
- {
- if ($styleAttributeValue == 'Superscript') {
- $this->styles[$styleID]['font']['superscript'] = true;
- }
- if ($styleAttributeValue == 'Subscript') {
- $this->styles[$styleID]['font']['subscript'] = true;
- }
- }
-
- private function parseStyleFont(string $styleID, SimpleXMLElement $styleAttributes): void
- {
- foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
- $styleAttributeValue = (string) $styleAttributeValue;
- switch ($styleAttributeKey) {
- case 'FontName':
- $this->styles[$styleID]['font']['name'] = $styleAttributeValue;
-
- break;
- case 'Size':
- $this->styles[$styleID]['font']['size'] = $styleAttributeValue;
-
- break;
- case 'Color':
- $this->styles[$styleID]['font']['color']['rgb'] = substr($styleAttributeValue, 1);
-
- break;
- case 'Bold':
- $this->styles[$styleID]['font']['bold'] = true;
-
- break;
- case 'Italic':
- $this->styles[$styleID]['font']['italic'] = true;
-
- break;
- case 'Underline':
- $this->parseStyleFontUnderline($styleID, $styleAttributeValue);
-
- break;
- case 'VerticalAlign':
- $this->parseStyleFontVerticalAlign($styleID, $styleAttributeValue);
-
- break;
- }
- }
- }
-
- private function parseStyleInterior($styleID, SimpleXMLElement $styleAttributes): void
- {
- foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValuex) {
- $styleAttributeValue = (string) $styleAttributeValuex;
- switch ($styleAttributeKey) {
- case 'Color':
- $this->styles[$styleID]['fill']['endColor']['rgb'] = substr($styleAttributeValue, 1);
- $this->styles[$styleID]['fill']['startColor']['rgb'] = substr($styleAttributeValue, 1);
-
- break;
- case 'PatternColor':
- $this->styles[$styleID]['fill']['startColor']['rgb'] = substr($styleAttributeValue, 1);
-
- break;
- case 'Pattern':
- $lcStyleAttributeValue = strtolower((string) $styleAttributeValue);
- $this->styles[$styleID]['fill']['fillType'] = self::$mappings['fillType'][$lcStyleAttributeValue] ?? Fill::FILL_NONE;
-
- break;
- }
- }
- }
-
- private function parseStyleNumberFormat($styleID, SimpleXMLElement $styleAttributes): void
- {
- $fromFormats = ['\-', '\ '];
- $toFormats = ['-', ' '];
-
- foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
- $styleAttributeValue = str_replace($fromFormats, $toFormats, $styleAttributeValue);
- switch ($styleAttributeValue) {
- case 'Short Date':
- $styleAttributeValue = 'dd/mm/yyyy';
-
- break;
- }
-
- if ($styleAttributeValue > '') {
- $this->styles[$styleID]['numberFormat']['formatCode'] = $styleAttributeValue;
- }
- }
+ return ($simple === null)
+ ? new SimpleXMLElement('')
+ : ($simple->attributes($node) ?? new SimpleXMLElement(''));
}
}
diff --git a/src/PhpSpreadsheet/Reader/Xml/Properties.php b/src/PhpSpreadsheet/Reader/Xml/Properties.php
new file mode 100644
index 00000000..fa0d7957
--- /dev/null
+++ b/src/PhpSpreadsheet/Reader/Xml/Properties.php
@@ -0,0 +1,164 @@
+spreadsheet = $spreadsheet;
+ }
+
+ public function readProperties(SimpleXMLElement $xml, array $namespaces): void
+ {
+ $this->readStandardProperties($xml);
+ $this->readCustomProperties($xml, $namespaces);
+ }
+
+ protected function readStandardProperties(SimpleXMLElement $xml): void
+ {
+ if (isset($xml->DocumentProperties[0])) {
+ $docProps = $this->spreadsheet->getProperties();
+
+ foreach ($xml->DocumentProperties[0] as $propertyName => $propertyValue) {
+ $propertyValue = (string) $propertyValue;
+
+ $this->processStandardProperty($docProps, $propertyName, $propertyValue);
+ }
+ }
+ }
+
+ protected function readCustomProperties(SimpleXMLElement $xml, array $namespaces): void
+ {
+ if (isset($xml->CustomDocumentProperties)) {
+ $docProps = $this->spreadsheet->getProperties();
+
+ foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) {
+ $propertyAttributes = self::getAttributes($propertyValue, $namespaces['dt']);
+ $propertyName = preg_replace_callback('/_x([0-9a-f]{4})_/i', [$this, 'hex2str'], $propertyName);
+
+ $this->processCustomProperty($docProps, $propertyName, $propertyValue, $propertyAttributes);
+ }
+ }
+ }
+
+ protected function processStandardProperty(
+ DocumentProperties $docProps,
+ string $propertyName,
+ string $stringValue
+ ): void {
+ switch ($propertyName) {
+ case 'Title':
+ $docProps->setTitle($stringValue);
+
+ break;
+ case 'Subject':
+ $docProps->setSubject($stringValue);
+
+ break;
+ case 'Author':
+ $docProps->setCreator($stringValue);
+
+ break;
+ case 'Created':
+ $docProps->setCreated($this->processTimestampValue($stringValue));
+
+ break;
+ case 'LastAuthor':
+ $docProps->setLastModifiedBy($stringValue);
+
+ break;
+ case 'LastSaved':
+ $docProps->setModified($this->processTimestampValue($stringValue));
+
+ break;
+ case 'Company':
+ $docProps->setCompany($stringValue);
+
+ break;
+ case 'Category':
+ $docProps->setCategory($stringValue);
+
+ break;
+ case 'Manager':
+ $docProps->setManager($stringValue);
+
+ break;
+ case 'Keywords':
+ $docProps->setKeywords($stringValue);
+
+ break;
+ case 'Description':
+ $docProps->setDescription($stringValue);
+
+ break;
+ }
+ }
+
+ protected function processCustomProperty(
+ DocumentProperties $docProps,
+ string $propertyName,
+ ?SimpleXMLElement $propertyValue,
+ SimpleXMLElement $propertyAttributes
+ ): void {
+ $propertyType = DocumentProperties::PROPERTY_TYPE_UNKNOWN;
+
+ switch ((string) $propertyAttributes) {
+ case 'string':
+ $propertyType = DocumentProperties::PROPERTY_TYPE_STRING;
+ $propertyValue = trim((string) $propertyValue);
+
+ break;
+ case 'boolean':
+ $propertyType = DocumentProperties::PROPERTY_TYPE_BOOLEAN;
+ $propertyValue = (bool) $propertyValue;
+
+ break;
+ case 'integer':
+ $propertyType = DocumentProperties::PROPERTY_TYPE_INTEGER;
+ $propertyValue = (int) $propertyValue;
+
+ break;
+ case 'float':
+ $propertyType = DocumentProperties::PROPERTY_TYPE_FLOAT;
+ $propertyValue = (float) $propertyValue;
+
+ break;
+ case 'dateTime.tz':
+ $propertyType = DocumentProperties::PROPERTY_TYPE_DATE;
+ $propertyValue = $this->processTimestampValue(trim((string) $propertyValue));
+
+ break;
+ }
+
+ $docProps->setCustomProperty($propertyName, $propertyValue, $propertyType);
+ }
+
+ protected function hex2str(array $hex): string
+ {
+ return mb_chr((int) hexdec($hex[1]), 'UTF-8');
+ }
+
+ private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement
+ {
+ return ($simple === null)
+ ? new SimpleXMLElement('')
+ : ($simple->attributes($node) ?? new SimpleXMLElement(''));
+ }
+
+ protected function processTimestampValue(string $dateTimeValue): int
+ {
+ $dateTime = strtotime($dateTimeValue);
+
+ return $dateTime === false ? time() : $dateTime;
+ }
+}
diff --git a/src/PhpSpreadsheet/Reader/Xml/Style.php b/src/PhpSpreadsheet/Reader/Xml/Style.php
new file mode 100644
index 00000000..05b7c89d
--- /dev/null
+++ b/src/PhpSpreadsheet/Reader/Xml/Style.php
@@ -0,0 +1,74 @@
+Styles)) {
+ return [];
+ }
+
+ $alignmentStyleParser = new Style\Alignment();
+ $borderStyleParser = new Style\Border();
+ $fontStyleParser = new Style\Font();
+ $fillStyleParser = new Style\Fill();
+ $numberFormatStyleParser = new Style\NumberFormat();
+
+ foreach ($xml->Styles[0] as $style) {
+ $style_ss = self::getAttributes($style, $namespaces['ss']);
+ $styleID = (string) $style_ss['ID'];
+ $this->styles[$styleID] = $this->styles['Default'] ?? [];
+
+ $alignment = $border = $font = $fill = $numberFormat = [];
+
+ foreach ($style as $styleType => $styleDatax) {
+ $styleData = $styleDatax ?? new SimpleXMLElement('');
+ $styleAttributes = $styleData->attributes($namespaces['ss']);
+ switch ($styleType) {
+ case 'Alignment':
+ $alignment = $alignmentStyleParser->parseStyle($styleAttributes);
+
+ break;
+ case 'Borders':
+ $border = $borderStyleParser->parseStyle($styleData, $namespaces);
+
+ break;
+ case 'Font':
+ $font = $fontStyleParser->parseStyle($styleAttributes);
+
+ break;
+ case 'Interior':
+ $fill = $fillStyleParser->parseStyle($styleAttributes);
+
+ break;
+ case 'NumberFormat':
+ $numberFormat = $numberFormatStyleParser->parseStyle($styleAttributes);
+
+ break;
+ }
+ }
+
+ $this->styles[$styleID] = array_merge($alignment, $border, $font, $fill, $numberFormat);
+ }
+
+ return $this->styles;
+ }
+
+ protected static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement
+ {
+ return ($simple === null)
+ ? new SimpleXMLElement('')
+ : ($simple->attributes($node) ?? new SimpleXMLElement(''));
+ }
+}
diff --git a/src/PhpSpreadsheet/Reader/Xml/Style/Alignment.php b/src/PhpSpreadsheet/Reader/Xml/Style/Alignment.php
new file mode 100644
index 00000000..d1363548
--- /dev/null
+++ b/src/PhpSpreadsheet/Reader/Xml/Style/Alignment.php
@@ -0,0 +1,58 @@
+ $styleAttributeValue) {
+ $styleAttributeValue = (string) $styleAttributeValue;
+ switch ($styleAttributeKey) {
+ case 'Vertical':
+ if (self::identifyFixedStyleValue(self::VERTICAL_ALIGNMENT_STYLES, $styleAttributeValue)) {
+ $style['alignment']['vertical'] = $styleAttributeValue;
+ }
+
+ break;
+ case 'Horizontal':
+ if (self::identifyFixedStyleValue(self::HORIZONTAL_ALIGNMENT_STYLES, $styleAttributeValue)) {
+ $style['alignment']['horizontal'] = $styleAttributeValue;
+ }
+
+ break;
+ case 'WrapText':
+ $style['alignment']['wrapText'] = true;
+
+ break;
+ case 'Rotate':
+ $style['alignment']['textRotation'] = $styleAttributeValue;
+
+ break;
+ }
+ }
+
+ return $style;
+ }
+}
diff --git a/src/PhpSpreadsheet/Reader/Xml/Style/Border.php b/src/PhpSpreadsheet/Reader/Xml/Style/Border.php
new file mode 100644
index 00000000..8aefd9c9
--- /dev/null
+++ b/src/PhpSpreadsheet/Reader/Xml/Style/Border.php
@@ -0,0 +1,98 @@
+ [
+ '1continuous' => BorderStyle::BORDER_THIN,
+ '1dash' => BorderStyle::BORDER_DASHED,
+ '1dashdot' => BorderStyle::BORDER_DASHDOT,
+ '1dashdotdot' => BorderStyle::BORDER_DASHDOTDOT,
+ '1dot' => BorderStyle::BORDER_DOTTED,
+ '1double' => BorderStyle::BORDER_DOUBLE,
+ '2continuous' => BorderStyle::BORDER_MEDIUM,
+ '2dash' => BorderStyle::BORDER_MEDIUMDASHED,
+ '2dashdot' => BorderStyle::BORDER_MEDIUMDASHDOT,
+ '2dashdotdot' => BorderStyle::BORDER_MEDIUMDASHDOTDOT,
+ '2dot' => BorderStyle::BORDER_DOTTED,
+ '2double' => BorderStyle::BORDER_DOUBLE,
+ '3continuous' => BorderStyle::BORDER_THICK,
+ '3dash' => BorderStyle::BORDER_MEDIUMDASHED,
+ '3dashdot' => BorderStyle::BORDER_MEDIUMDASHDOT,
+ '3dashdotdot' => BorderStyle::BORDER_MEDIUMDASHDOTDOT,
+ '3dot' => BorderStyle::BORDER_DOTTED,
+ '3double' => BorderStyle::BORDER_DOUBLE,
+ ],
+ ];
+
+ public function parseStyle(SimpleXMLElement $styleData, array $namespaces): array
+ {
+ $style = [];
+
+ $diagonalDirection = '';
+ $borderPosition = '';
+ foreach ($styleData->Border as $borderStyle) {
+ $borderAttributes = self::getAttributes($borderStyle, $namespaces['ss']);
+ $thisBorder = [];
+ $styleType = (string) $borderAttributes->Weight;
+ $styleType .= strtolower((string) $borderAttributes->LineStyle);
+ $thisBorder['borderStyle'] = self::BORDER_MAPPINGS['borderStyle'][$styleType] ?? BorderStyle::BORDER_NONE;
+
+ foreach ($borderAttributes as $borderStyleKey => $borderStyleValuex) {
+ $borderStyleValue = (string) $borderStyleValuex;
+ switch ($borderStyleKey) {
+ case 'Position':
+ [$borderPosition, $diagonalDirection] =
+ $this->parsePosition($borderStyleValue, $diagonalDirection);
+
+ break;
+ case 'Color':
+ $borderColour = substr($borderStyleValue, 1);
+ $thisBorder['color']['rgb'] = $borderColour;
+
+ break;
+ }
+ }
+
+ if ($borderPosition) {
+ $style['borders'][$borderPosition] = $thisBorder;
+ } elseif ($diagonalDirection) {
+ $style['borders']['diagonalDirection'] = $diagonalDirection;
+ $style['borders']['diagonal'] = $thisBorder;
+ }
+ }
+
+ return $style;
+ }
+
+ protected function parsePosition(string $borderStyleValue, string $diagonalDirection): array
+ {
+ $borderStyleValue = strtolower($borderStyleValue);
+
+ if (in_array($borderStyleValue, self::BORDER_POSITIONS)) {
+ $borderPosition = $borderStyleValue;
+ } elseif ($borderStyleValue === 'diagonalleft') {
+ $diagonalDirection = $diagonalDirection ? Borders::DIAGONAL_BOTH : Borders::DIAGONAL_DOWN;
+ } elseif ($borderStyleValue === 'diagonalright') {
+ $diagonalDirection = $diagonalDirection ? Borders::DIAGONAL_BOTH : Borders::DIAGONAL_UP;
+ }
+
+ return [$borderPosition ?? null, $diagonalDirection];
+ }
+}
diff --git a/src/PhpSpreadsheet/Reader/Xml/Style/Fill.php b/src/PhpSpreadsheet/Reader/Xml/Style/Fill.php
new file mode 100644
index 00000000..9a612152
--- /dev/null
+++ b/src/PhpSpreadsheet/Reader/Xml/Style/Fill.php
@@ -0,0 +1,63 @@
+ [
+ 'solid' => FillStyles::FILL_SOLID,
+ 'gray75' => FillStyles::FILL_PATTERN_DARKGRAY,
+ 'gray50' => FillStyles::FILL_PATTERN_MEDIUMGRAY,
+ 'gray25' => FillStyles::FILL_PATTERN_LIGHTGRAY,
+ 'gray125' => FillStyles::FILL_PATTERN_GRAY125,
+ 'gray0625' => FillStyles::FILL_PATTERN_GRAY0625,
+ 'horzstripe' => FillStyles::FILL_PATTERN_DARKHORIZONTAL, // horizontal stripe
+ 'vertstripe' => FillStyles::FILL_PATTERN_DARKVERTICAL, // vertical stripe
+ 'reversediagstripe' => FillStyles::FILL_PATTERN_DARKUP, // reverse diagonal stripe
+ 'diagstripe' => FillStyles::FILL_PATTERN_DARKDOWN, // diagonal stripe
+ 'diagcross' => FillStyles::FILL_PATTERN_DARKGRID, // diagoanl crosshatch
+ 'thickdiagcross' => FillStyles::FILL_PATTERN_DARKTRELLIS, // thick diagonal crosshatch
+ 'thinhorzstripe' => FillStyles::FILL_PATTERN_LIGHTHORIZONTAL,
+ 'thinvertstripe' => FillStyles::FILL_PATTERN_LIGHTVERTICAL,
+ 'thinreversediagstripe' => FillStyles::FILL_PATTERN_LIGHTUP,
+ 'thindiagstripe' => FillStyles::FILL_PATTERN_LIGHTDOWN,
+ 'thinhorzcross' => FillStyles::FILL_PATTERN_LIGHTGRID, // thin horizontal crosshatch
+ 'thindiagcross' => FillStyles::FILL_PATTERN_LIGHTTRELLIS, // thin diagonal crosshatch
+ ],
+ ];
+
+ public function parseStyle(SimpleXMLElement $styleAttributes): array
+ {
+ $style = [];
+
+ foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValuex) {
+ $styleAttributeValue = (string) $styleAttributeValuex;
+ switch ($styleAttributeKey) {
+ case 'Color':
+ $style['fill']['endColor']['rgb'] = substr($styleAttributeValue, 1);
+ $style['fill']['startColor']['rgb'] = substr($styleAttributeValue, 1);
+
+ break;
+ case 'PatternColor':
+ $style['fill']['startColor']['rgb'] = substr($styleAttributeValue, 1);
+
+ break;
+ case 'Pattern':
+ $lcStyleAttributeValue = strtolower((string) $styleAttributeValue);
+ $style['fill']['fillType']
+ = self::FILL_MAPPINGS['fillType'][$lcStyleAttributeValue] ?? FillStyles::FILL_NONE;
+
+ break;
+ }
+ }
+
+ return $style;
+ }
+}
diff --git a/src/PhpSpreadsheet/Reader/Xml/Style/Font.php b/src/PhpSpreadsheet/Reader/Xml/Style/Font.php
new file mode 100644
index 00000000..16ab44d8
--- /dev/null
+++ b/src/PhpSpreadsheet/Reader/Xml/Style/Font.php
@@ -0,0 +1,79 @@
+ $styleAttributeValue) {
+ $styleAttributeValue = (string) $styleAttributeValue;
+ switch ($styleAttributeKey) {
+ case 'FontName':
+ $style['font']['name'] = $styleAttributeValue;
+
+ break;
+ case 'Size':
+ $style['font']['size'] = $styleAttributeValue;
+
+ break;
+ case 'Color':
+ $style['font']['color']['rgb'] = substr($styleAttributeValue, 1);
+
+ break;
+ case 'Bold':
+ $style['font']['bold'] = true;
+
+ break;
+ case 'Italic':
+ $style['font']['italic'] = true;
+
+ break;
+ case 'Underline':
+ $style = $this->parseUnderline($style, $styleAttributeValue);
+
+ break;
+ case 'VerticalAlign':
+ $style = $this->parseVerticalAlign($style, $styleAttributeValue);
+
+ break;
+ }
+ }
+
+ return $style;
+ }
+}
diff --git a/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php b/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php
new file mode 100644
index 00000000..a31aa9eb
--- /dev/null
+++ b/src/PhpSpreadsheet/Reader/Xml/Style/NumberFormat.php
@@ -0,0 +1,33 @@
+ $styleAttributeValue) {
+ $styleAttributeValue = str_replace($fromFormats, $toFormats, $styleAttributeValue);
+
+ switch ($styleAttributeValue) {
+ case 'Short Date':
+ $styleAttributeValue = 'dd/mm/yyyy';
+
+ break;
+ }
+
+ if ($styleAttributeValue > '') {
+ $style['numberFormat']['formatCode'] = $styleAttributeValue;
+ }
+ }
+
+ return $style;
+ }
+}
diff --git a/src/PhpSpreadsheet/Reader/Xml/Style/StyleBase.php b/src/PhpSpreadsheet/Reader/Xml/Style/StyleBase.php
new file mode 100644
index 00000000..fc9ace82
--- /dev/null
+++ b/src/PhpSpreadsheet/Reader/Xml/Style/StyleBase.php
@@ -0,0 +1,32 @@
+')
+ : ($simple->attributes($node) ?? new SimpleXMLElement(''));
+ }
+}
diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlTest.php
index bcc108c1..a0b05a56 100644
--- a/tests/PhpSpreadsheetTests/Reader/Xml/XmlTest.php
+++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlTest.php
@@ -45,4 +45,28 @@ class XmlTest extends TestCase
self::assertEquals('PhpSpreadsheet', $hyperlink->getValue());
self::assertEquals('https://phpspreadsheet.readthedocs.io', $hyperlink->getHyperlink()->getUrl());
}
+
+ public function testLoadCorruptedFile(): void
+ {
+ $this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class);
+
+ $xmlReader = new Xml();
+ $xmlReader->load('tests/data/Reader/Xml/CorruptedXmlFile.xml');
+ }
+
+ public function testListWorksheetNamesCorruptedFile(): void
+ {
+ $this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class);
+
+ $xmlReader = new Xml();
+ $xmlReader->listWorksheetNames('tests/data/Reader/Xml/CorruptedXmlFile.xml');
+ }
+
+ public function testListWorksheetInfoCorruptedFile(): void
+ {
+ $this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class);
+
+ $xmlReader = new Xml();
+ $xmlReader->listWorksheetInfo('tests/data/Reader/Xml/CorruptedXmlFile.xml');
+ }
}
diff --git a/tests/data/Reader/Xml/CorruptedXmlFile.xml b/tests/data/Reader/Xml/CorruptedXmlFile.xml
new file mode 100644
index 00000000..7678d229
--- /dev/null
+++ b/tests/data/Reader/Xml/CorruptedXmlFile.xml
@@ -0,0 +1,9 @@
+
+
+
+
+