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 @@ + + + + +