Skip to content

Commit b74c476

Browse files
author
Owen Leibman
committed
WIP Namespacing Phase 2 - Styles
This is part 2 of a several-phase process to permit PhpSpreadsheet to handle input Xlsx files which use unexpected namespacing. The first phase, introduced as part of release 1.19.0, essentially handled the reading of data. This phase handles the reading of styles. More phases are planned. It is my intention to leave this in draft status for at least a month. This will give time for additional testing, by me and, I hope, others who might be interested. This fixes the same problem addressed by PR #2458, if it reaches mergeable status before I am ready to take this out of draft status. I do not anticipate any difficult merge conflicts if the other change is merged first. This change is more difficult than I'd hoped. I can't get xpath to work properly with the namespaced style file, even though I don't have difficulties with others. Normally we expect: ```xml <stylesheet xmlns="http://whatever" ... ``` In the namespaced files, we typically see: ```xml <x:stylesheet xmlns:x="http://whatever" ... ``` Simplexml_load_file specifying a namespace handles the two situations the same, as expected. But, for some reason that I cannot figure out, there are significant differences when xpath processes the result. However, I can manipulate the xml if necessary; I'm not proud of doing that, and will gladly accept any suggestions. In the meantime, it seems to work. My major non-standard unit test file had disabled any style-related tests when phase 1 was installed. These are now all enabled.
1 parent 443175e commit b74c476

File tree

4 files changed

+247
-81
lines changed

4 files changed

+247
-81
lines changed

src/PhpSpreadsheet/Reader/Xlsx.php

+40-3
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,42 @@ private function loadZip(string $filename, string $ns = ''): SimpleXMLElement
132132
return self::testSimpleXml($rels);
133133
}
134134

135+
private function loadStyleZip(string $filename, string $ns = ''): SimpleXMLElement
136+
{
137+
// With the following:
138+
// <x:styleSheet xmlns:x="whatever"...
139+
// simplexml_load_file specifying namespace works fine,
140+
// but xpath on the result does not. I can't figure out
141+
// how to make xpath work in this circumstance, but I can
142+
// manipulate the xml to the far more usual:
143+
// <stylesheet xmlns="whatever"...
144+
// Ugly, but arguably serviceable.
145+
$xml = $this->getFromZipArchive($this->zip, $filename);
146+
$xmlns = " xmlns=\"$ns\"";
147+
if (strpos($xml, $xmlns) === false) {
148+
$pattern = "~ xmlns:([A-Za-z0-9_]+)=\"$ns\"~";
149+
if (preg_match($pattern, $xml, $matches) === 1) {
150+
$pattern = "~ xmlns:${matches[1]}=~";
151+
$repl = preg_replace($pattern, ' xmlns=', $xml);
152+
if (is_string($repl)) {
153+
$pattern = "~<(/?)${matches[1]}:~";
154+
$repl = preg_replace($pattern, '<$1', $repl);
155+
}
156+
if (is_string($repl)) {
157+
$xml = $repl;
158+
}
159+
}
160+
}
161+
$rels = simplexml_load_string(
162+
$this->securityScanner->scan($xml),
163+
'SimpleXMLElement',
164+
0,
165+
$ns
166+
);
167+
168+
return self::testSimpleXml($rels);
169+
}
170+
135171
// This function is just to identify cases where I'm not sure
136172
// why empty namespace is required.
137173
private function loadZipNonamespace(string $filename, string $ns): SimpleXMLElement
@@ -538,11 +574,10 @@ public function load(string $filename, int $flags = 0): Spreadsheet
538574
if ($xpath === null) {
539575
$xmlStyles = self::testSimpleXml(null);
540576
} else {
541-
// I think Nonamespace is okay because I'm using xpath.
542-
$xmlStyles = $this->loadZipNonamespace("$dir/$xpath[Target]", $mainNS);
577+
$xmlStyles = $this->loadStyleZip("$dir/$xpath[Target]", $mainNS);
543578
}
544579

545-
$xmlStyles->registerXPathNamespace('smm', Namespaces::MAIN);
580+
$xmlStyles->registerXPathNamespace('smm', $mainNS);
546581
$fills = self::xpathNoFalse($xmlStyles, 'smm:fills/smm:fill');
547582
$fonts = self::xpathNoFalse($xmlStyles, 'smm:fonts/smm:font');
548583
$borders = self::xpathNoFalse($xmlStyles, 'smm:borders/smm:border');
@@ -558,6 +593,7 @@ public function load(string $filename, int $flags = 0): Spreadsheet
558593
if (isset($numFmts) && ($numFmts !== null)) {
559594
$numFmts->registerXPathNamespace('sml', $mainNS);
560595
}
596+
$this->styleReader->setNamespace($mainNS);
561597
if (!$this->readDataOnly/* && $xmlStyles*/) {
562598
foreach ($xfTags as $xfTag) {
563599
$xf = self::getAttributes($xfTag);
@@ -642,6 +678,7 @@ public function load(string $filename, int $flags = 0): Spreadsheet
642678
}
643679
}
644680
$this->styleReader->setStyleXml($xmlStyles);
681+
$this->styleReader->setNamespace($mainNS);
645682
$this->styleReader->setStyleBaseData($theme, $styles, $cellStyles);
646683
$dxfs = $this->styleReader->dxfs($this->readDataOnly);
647684
$styles = $this->styleReader->styles();

src/PhpSpreadsheet/Reader/Xlsx/Styles.php

+148-72
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,27 @@ class Styles extends BaseParserClass
3333
/** @var SimpleXMLElement */
3434
private $styleXml;
3535

36+
/** @var string */
37+
private $namespace = '';
38+
39+
public function setNamespace(string $namespace): void
40+
{
41+
$this->namespace = $namespace;
42+
}
43+
44+
private function getStyleAttributes(SimpleXMLElement $value): SimpleXMLElement
45+
{
46+
$attr = null;
47+
if ($value) {
48+
$attr = $value->attributes('');
49+
if ($attr === null || count($attr) === 0) {
50+
$attr = $value->attributes($this->namespace);
51+
}
52+
}
53+
54+
return Xlsx::testSimpleXml($attr);
55+
}
56+
3657
public function setStyleXml(SimpleXmlElement $styleXml): void
3758
{
3859
$this->styleXml = $styleXml;
@@ -52,48 +73,62 @@ public function setStyleBaseData(?Theme $theme = null, array $styles = [], array
5273

5374
public function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml): void
5475
{
55-
if (isset($fontStyleXml->name, $fontStyleXml->name['val'])) {
56-
$fontStyle->setName((string) $fontStyleXml->name['val']);
76+
if (isset($fontStyleXml->name)) {
77+
$attr = $this->getStyleAttributes($fontStyleXml->name);
78+
if (isset($attr['val'])) {
79+
$fontStyle->setName((string) $attr['val']);
80+
}
5781
}
58-
if (isset($fontStyleXml->sz, $fontStyleXml->sz['val'])) {
59-
$fontStyle->setSize((float) $fontStyleXml->sz['val']);
82+
if (isset($fontStyleXml->sz)) {
83+
$attr = $this->getStyleAttributes($fontStyleXml->sz);
84+
if (isset($attr['val'])) {
85+
$fontStyle->setSize((float) $attr['val']);
86+
}
6087
}
6188
if (isset($fontStyleXml->b)) {
62-
$fontStyle->setBold(!isset($fontStyleXml->b['val']) || self::boolean((string) $fontStyleXml->b['val']));
89+
$attr = $this->getStyleAttributes($fontStyleXml->b);
90+
$fontStyle->setBold(!isset($attr['val']) || self::boolean((string) $attr['val']));
6391
}
6492
if (isset($fontStyleXml->i)) {
65-
$fontStyle->setItalic(!isset($fontStyleXml->i['val']) || self::boolean((string) $fontStyleXml->i['val']));
93+
$attr = $this->getStyleAttributes($fontStyleXml->i);
94+
$fontStyle->setItalic(!isset($attr['val']) || self::boolean((string) $attr['val']));
6695
}
6796
if (isset($fontStyleXml->strike)) {
68-
$fontStyle->setStrikethrough(
69-
!isset($fontStyleXml->strike['val']) || self::boolean((string) $fontStyleXml->strike['val'])
70-
);
97+
$attr = $this->getStyleAttributes($fontStyleXml->strike);
98+
$fontStyle->setStrikethrough(!isset($attr['val']) || self::boolean((string) $attr['val']));
7199
}
72100
$fontStyle->getColor()->setARGB($this->readColor($fontStyleXml->color));
73101

74-
if (isset($fontStyleXml->u) && !isset($fontStyleXml->u['val'])) {
75-
$fontStyle->setUnderline(Font::UNDERLINE_SINGLE);
76-
} elseif (isset($fontStyleXml->u, $fontStyleXml->u['val'])) {
77-
$fontStyle->setUnderline((string) $fontStyleXml->u['val']);
102+
if (isset($fontStyleXml->u)) {
103+
$attr = $this->getStyleAttributes($fontStyleXml->u);
104+
if (!isset($attr['val'])) {
105+
$fontStyle->setUnderline(Font::UNDERLINE_SINGLE);
106+
} else {
107+
$fontStyle->setUnderline((string) $attr['val']);
108+
}
78109
}
79-
80-
if (isset($fontStyleXml->vertAlign, $fontStyleXml->vertAlign['val'])) {
81-
$verticalAlign = strtolower((string) $fontStyleXml->vertAlign['val']);
82-
if ($verticalAlign === 'superscript') {
83-
$fontStyle->setSuperscript(true);
84-
} elseif ($verticalAlign === 'subscript') {
85-
$fontStyle->setSubscript(true);
110+
if (isset($fontStyleXml->vertAlign)) {
111+
$attr = $this->getStyleAttributes($fontStyleXml->vertAlign);
112+
if (!isset($attr['val'])) {
113+
$verticalAlign = strtolower((string) $attr['val']);
114+
if ($verticalAlign === 'superscript') {
115+
$fontStyle->setSuperscript(true);
116+
} elseif ($verticalAlign === 'subscript') {
117+
$fontStyle->setSubscript(true);
118+
}
86119
}
87120
}
88121
}
89122

90123
private function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void
91124
{
92-
if ($numfmtStyleXml->count() === 0) {
125+
if ((string) $numfmtStyleXml['formatCode'] !== '') {
126+
$numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmtStyleXml['formatCode']));
127+
93128
return;
94129
}
95-
$numfmt = Xlsx::getAttributes($numfmtStyleXml);
96-
if ($numfmt->count() > 0 && isset($numfmt['formatCode'])) {
130+
$numfmt = $this->getStyleAttributes($numfmtStyleXml);
131+
if (isset($numfmt['formatCode'])) {
97132
$numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmt['formatCode']));
98133
}
99134
}
@@ -103,10 +138,11 @@ public function readFillStyle(Fill $fillStyle, SimpleXMLElement $fillStyleXml):
103138
if ($fillStyleXml->gradientFill) {
104139
/** @var SimpleXMLElement $gradientFill */
105140
$gradientFill = $fillStyleXml->gradientFill[0];
106-
if (!empty($gradientFill['type'])) {
107-
$fillStyle->setFillType((string) $gradientFill['type']);
141+
$attr = $this->getStyleAttributes($gradientFill);
142+
if (!empty($attr['type'])) {
143+
$fillStyle->setFillType((string) $attr['type']);
108144
}
109-
$fillStyle->setRotation((float) ($gradientFill['degree']));
145+
$fillStyle->setRotation((float) ($attr['degree']));
110146
$gradientFill->registerXPathNamespace('sml', Namespaces::MAIN);
111147
$fillStyle->getStartColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=0]'))->color));
112148
$fillStyle->getEndColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color));
@@ -121,18 +157,25 @@ public function readFillStyle(Fill $fillStyle, SimpleXMLElement $fillStyleXml):
121157
$defaultFillStyle = Fill::FILL_SOLID;
122158
}
123159

124-
$patternType = (string) $fillStyleXml->patternFill['patternType'] != ''
125-
? (string) $fillStyleXml->patternFill['patternType']
126-
: $defaultFillStyle;
160+
$type = '';
161+
if ((string) $fillStyleXml->patternFill['patternType'] !== '') {
162+
$type = (string) $fillStyleXml->patternFill['patternType'];
163+
} else {
164+
$attr = $this->getStyleAttributes($fillStyleXml->patternFill);
165+
$type = (string) $attr['patternType'];
166+
}
167+
$patternType = ($type === '') ? $defaultFillStyle : $type;
127168

128169
$fillStyle->setFillType($patternType);
129170
}
130171
}
131172

132173
public function readBorderStyle(Borders $borderStyle, SimpleXMLElement $borderStyleXml): void
133174
{
134-
$diagonalUp = self::boolean((string) $borderStyleXml['diagonalUp']);
135-
$diagonalDown = self::boolean((string) $borderStyleXml['diagonalDown']);
175+
$diagonalUp = $this->getAttribute($borderStyleXml, 'diagonalUp');
176+
$diagonalUp = self::boolean($diagonalUp);
177+
$diagonalDown = $this->getAttribute($borderStyleXml, 'diagonalDown');
178+
$diagonalDown = self::boolean($diagonalDown);
136179
if (!$diagonalUp && !$diagonalDown) {
137180
$borderStyle->setDiagonalDirection(Borders::DIAGONAL_NONE);
138181
} elseif ($diagonalUp && !$diagonalDown) {
@@ -150,10 +193,26 @@ public function readBorderStyle(Borders $borderStyle, SimpleXMLElement $borderSt
150193
$this->readBorder($borderStyle->getDiagonal(), $borderStyleXml->diagonal);
151194
}
152195

196+
private function getAttribute(SimpleXMLElement $xml, string $attribute): string
197+
{
198+
$style = '';
199+
if ((string) $xml[$attribute] !== '') {
200+
$style = (string) $xml[$attribute];
201+
} else {
202+
$attr = $this->getStyleAttributes($xml);
203+
if (isset($attr[$attribute])) {
204+
$style = (string) $attr[$attribute];
205+
}
206+
}
207+
208+
return $style;
209+
}
210+
153211
private function readBorder(Border $border, SimpleXMLElement $borderXml): void
154212
{
155-
if (isset($borderXml['style'])) {
156-
$border->setBorderStyle((string) $borderXml['style']);
213+
$style = $this->getAttribute($borderXml, 'style');
214+
if ($style !== '') {
215+
$border->setBorderStyle((string) $style);
157216
}
158217
if (isset($borderXml->color)) {
159218
$border->getColor()->setARGB($this->readColor($borderXml->color));
@@ -162,25 +221,25 @@ private function readBorder(Border $border, SimpleXMLElement $borderXml): void
162221

163222
public function readAlignmentStyle(Alignment $alignment, SimpleXMLElement $alignmentXml): void
164223
{
165-
$alignment->setHorizontal((string) $alignmentXml['horizontal']);
166-
$alignment->setVertical((string) $alignmentXml['vertical']);
167-
168-
$textRotation = 0;
169-
if ((int) $alignmentXml['textRotation'] <= 90) {
170-
$textRotation = (int) $alignmentXml['textRotation'];
171-
} elseif ((int) $alignmentXml['textRotation'] > 90) {
172-
$textRotation = 90 - (int) $alignmentXml['textRotation'];
173-
}
174-
175-
$alignment->setTextRotation((int) $textRotation);
176-
$alignment->setWrapText(self::boolean((string) $alignmentXml['wrapText']));
177-
$alignment->setShrinkToFit(self::boolean((string) $alignmentXml['shrinkToFit']));
178-
$alignment->setIndent(
179-
(int) ((string) $alignmentXml['indent']) > 0 ? (int) ((string) $alignmentXml['indent']) : 0
180-
);
181-
$alignment->setReadOrder(
182-
(int) ((string) $alignmentXml['readingOrder']) > 0 ? (int) ((string) $alignmentXml['readingOrder']) : 0
183-
);
224+
$horizontal = $this->getAttribute($alignmentXml, 'horizontal');
225+
$alignment->setHorizontal($horizontal);
226+
$vertical = $this->getAttribute($alignmentXml, 'vertical');
227+
$alignment->setVertical((string) $vertical);
228+
229+
$textRotation = (int) $this->getAttribute($alignmentXml, 'textRotation');
230+
if ($textRotation > 90) {
231+
$textRotation = 90 - $textRotation;
232+
}
233+
$alignment->setTextRotation($textRotation);
234+
235+
$wrapText = $this->getAttribute($alignmentXml, 'wrapText');
236+
$alignment->setWrapText(self::boolean((string) $wrapText));
237+
$shrinkToFit = $this->getAttribute($alignmentXml, 'shrinkToFit');
238+
$alignment->setShrinkToFit(self::boolean((string) $shrinkToFit));
239+
$indent = (int) $this->getAttribute($alignmentXml, 'indent');
240+
$alignment->setIndent(max($indent, 0));
241+
$readingOrder = (int) $this->getAttribute($alignmentXml, 'readingOrder');
242+
$alignment->setReadOrder(max($readingOrder, 0));
184243
}
185244

186245
private static function formatGeneral(string $formatString): string
@@ -223,8 +282,8 @@ public function readStyle(Style $docStyle, $style): void
223282

224283
// protection
225284
if (isset($style->protection)) {
226-
$this->readProtectionLocked($docStyle, $style);
227-
$this->readProtectionHidden($docStyle, $style);
285+
$this->readProtectionLocked($docStyle, $style->protection);
286+
$this->readProtectionHidden($docStyle, $style->protection);
228287
}
229288

230289
// top-level style settings
@@ -235,13 +294,20 @@ public function readStyle(Style $docStyle, $style): void
235294

236295
/**
237296
* Read protection locked attribute.
238-
*
239-
* @param SimpleXMLElement|stdClass $style
240297
*/
241-
public function readProtectionLocked(Style $docStyle, $style): void
298+
public function readProtectionLocked(Style $docStyle, SimpleXMLElement $style): void
242299
{
243-
if (isset($style->protection['locked'])) {
244-
if (self::boolean((string) $style->protection['locked'])) {
300+
$locked = '';
301+
if ((string) $style['locked'] !== '') {
302+
$locked = (string) $style['locked'];
303+
} else {
304+
$attr = $this->getStyleAttributes($style);
305+
if (isset($attr['locked'])) {
306+
$locked = (string) $attr['locked'];
307+
}
308+
}
309+
if ($locked !== '') {
310+
if (self::boolean($locked)) {
245311
$docStyle->getProtection()->setLocked(Protection::PROTECTION_PROTECTED);
246312
} else {
247313
$docStyle->getProtection()->setLocked(Protection::PROTECTION_UNPROTECTED);
@@ -251,13 +317,20 @@ public function readProtectionLocked(Style $docStyle, $style): void
251317

252318
/**
253319
* Read protection hidden attribute.
254-
*
255-
* @param SimpleXMLElement|stdClass $style
256320
*/
257-
public function readProtectionHidden(Style $docStyle, $style): void
321+
public function readProtectionHidden(Style $docStyle, SimpleXMLElement $style): void
258322
{
259-
if (isset($style->protection['hidden'])) {
260-
if (self::boolean((string) $style->protection['hidden'])) {
323+
$hidden = '';
324+
if ((string) $style['hidden'] !== '') {
325+
$hidden = (string) $style['hidden'];
326+
} else {
327+
$attr = $this->getStyleAttributes($style);
328+
if (isset($attr['hidden'])) {
329+
$hidden = (string) $attr['hidden'];
330+
}
331+
}
332+
if ($hidden !== '') {
333+
if (self::boolean((string) $hidden)) {
261334
$docStyle->getProtection()->setHidden(Protection::PROTECTION_PROTECTED);
262335
} else {
263336
$docStyle->getProtection()->setHidden(Protection::PROTECTION_UNPROTECTED);
@@ -267,15 +340,18 @@ public function readProtectionHidden(Style $docStyle, $style): void
267340

268341
public function readColor(SimpleXMLElement $color, bool $background = false): string
269342
{
270-
if (isset($color['rgb'])) {
271-
return (string) $color['rgb'];
272-
} elseif (isset($color['indexed'])) {
273-
return Color::indexedColor((int) ($color['indexed'] - 7), $background)->getARGB() ?? '';
274-
} elseif (isset($color['theme'])) {
343+
$attr = $this->getStyleAttributes($color);
344+
if (isset($attr['rgb'])) {
345+
return (string) $attr['rgb'];
346+
}
347+
if (isset($attr['indexed'])) {
348+
return Color::indexedColor((int) ($attr['indexed'] - 7), $background)->getARGB() ?? '';
349+
}
350+
if (isset($attr['theme'])) {
275351
if ($this->theme !== null) {
276-
$returnColour = $this->theme->getColourByIndex((int) $color['theme']);
277-
if (isset($color['tint'])) {
278-
$tintAdjust = (float) $color['tint'];
352+
$returnColour = $this->theme->getColourByIndex((int) $attr['theme']);
353+
if (isset($attr['tint'])) {
354+
$tintAdjust = (float) $attr['tint'];
279355
$returnColour = Color::changeBrightness($returnColour ?? '', $tintAdjust);
280356
}
281357

0 commit comments

Comments
 (0)