1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace PhpOffice\PhpSpreadsheet\Reader; |
4
|
|
|
|
5
|
|
|
use DOMAttr; |
6
|
|
|
use DOMDocument; |
7
|
|
|
use DOMElement; |
8
|
|
|
use DOMNode; |
9
|
|
|
use DOMText; |
10
|
|
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; |
11
|
|
|
use PhpOffice\PhpSpreadsheet\Cell\DataType; |
12
|
|
|
use PhpOffice\PhpSpreadsheet\Helper\Dimension as HelperDimension; |
13
|
|
|
use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter; |
14
|
|
|
use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames; |
15
|
|
|
use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator; |
16
|
|
|
use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings; |
17
|
|
|
use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties; |
18
|
|
|
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; |
19
|
|
|
use PhpOffice\PhpSpreadsheet\RichText\RichText; |
20
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\Date; |
21
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\File; |
22
|
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
23
|
|
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat; |
24
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
25
|
|
|
use Throwable; |
26
|
|
|
use XMLReader; |
27
|
|
|
use ZipArchive; |
28
|
|
|
|
29
|
|
|
class Ods extends BaseReader |
30
|
|
|
{ |
31
|
|
|
const INITIAL_FILE = 'content.xml'; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Create a new Ods Reader instance. |
35
|
|
|
*/ |
36
|
111 |
|
public function __construct() |
37
|
|
|
{ |
38
|
111 |
|
parent::__construct(); |
39
|
111 |
|
$this->securityScanner = XmlScanner::getInstance($this); |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Can the current IReader read the file? |
44
|
|
|
*/ |
45
|
19 |
|
public function canRead(string $filename): bool |
46
|
|
|
{ |
47
|
19 |
|
$mimeType = 'UNKNOWN'; |
48
|
|
|
|
49
|
|
|
// Load file |
50
|
|
|
|
51
|
19 |
|
if (File::testFileNoThrow($filename, '')) { |
52
|
3 |
|
$zip = new ZipArchive(); |
53
|
3 |
|
if ($zip->open($filename) === true) { |
54
|
|
|
// check if it is an OOXML archive |
55
|
3 |
|
$stat = $zip->statName('mimetype'); |
56
|
3 |
|
if (!empty($stat) && ($stat['size'] <= 255)) { |
57
|
2 |
|
$mimeType = $zip->getFromName($stat['name']); |
58
|
1 |
|
} elseif ($zip->statName('META-INF/manifest.xml')) { |
59
|
1 |
|
$xml = simplexml_load_string( |
60
|
1 |
|
$this->getSecurityScannerOrThrow() |
61
|
1 |
|
->scan( |
62
|
1 |
|
$zip->getFromName( |
63
|
1 |
|
'META-INF/manifest.xml' |
64
|
1 |
|
) |
65
|
1 |
|
) |
66
|
1 |
|
); |
67
|
1 |
|
if ($xml !== false) { |
68
|
1 |
|
$namespacesContent = $xml->getNamespaces(true); |
69
|
1 |
|
if (isset($namespacesContent['manifest'])) { |
70
|
1 |
|
$manifest = $xml->children($namespacesContent['manifest']); |
71
|
1 |
|
foreach ($manifest as $manifestDataSet) { |
72
|
1 |
|
$manifestAttributes = $manifestDataSet->attributes($namespacesContent['manifest']); |
73
|
1 |
|
if ($manifestAttributes && $manifestAttributes->{'full-path'} == '/') { |
74
|
1 |
|
$mimeType = (string) $manifestAttributes->{'media-type'}; |
75
|
|
|
|
76
|
1 |
|
break; |
77
|
|
|
} |
78
|
|
|
} |
79
|
|
|
} |
80
|
|
|
} |
81
|
|
|
} |
82
|
|
|
|
83
|
3 |
|
$zip->close(); |
84
|
|
|
} |
85
|
|
|
} |
86
|
|
|
|
87
|
19 |
|
return $mimeType === 'application/vnd.oasis.opendocument.spreadsheet'; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object. |
92
|
|
|
* |
93
|
|
|
* @return string[] |
94
|
|
|
*/ |
95
|
6 |
|
public function listWorksheetNames(string $filename): array |
96
|
|
|
{ |
97
|
6 |
|
File::assertFile($filename, self::INITIAL_FILE); |
98
|
|
|
|
99
|
2 |
|
$worksheetNames = []; |
100
|
|
|
|
101
|
2 |
|
$xml = new XMLReader(); |
102
|
2 |
|
$xml->xml( |
103
|
2 |
|
$this->getSecurityScannerOrThrow() |
104
|
2 |
|
->scanFile( |
105
|
2 |
|
'zip://' . realpath($filename) . '#' . self::INITIAL_FILE |
106
|
2 |
|
) |
107
|
2 |
|
); |
108
|
2 |
|
$xml->setParserProperty(2, true); |
109
|
|
|
|
110
|
|
|
// Step into the first level of content of the XML |
111
|
2 |
|
$xml->read(); |
112
|
2 |
|
while ($xml->read()) { |
113
|
|
|
// Quickly jump through to the office:body node |
114
|
2 |
|
while ($xml->name !== 'office:body') { |
115
|
2 |
|
if ($xml->isEmptyElement) { |
116
|
2 |
|
$xml->read(); |
117
|
|
|
} else { |
118
|
2 |
|
$xml->next(); |
119
|
|
|
} |
120
|
|
|
} |
121
|
|
|
// Now read each node until we find our first table:table node |
122
|
2 |
|
while ($xml->read()) { |
123
|
2 |
|
$xmlName = $xml->name; |
124
|
2 |
|
if ($xmlName == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { |
125
|
|
|
// Loop through each table:table node reading the table:name attribute for each worksheet name |
126
|
|
|
do { |
127
|
2 |
|
$worksheetName = $xml->getAttribute('table:name'); |
128
|
2 |
|
if (!empty($worksheetName)) { |
129
|
2 |
|
$worksheetNames[] = $worksheetName; |
130
|
|
|
} |
131
|
2 |
|
$xml->next(); |
132
|
2 |
|
} while ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT); |
133
|
|
|
} |
134
|
|
|
} |
135
|
|
|
} |
136
|
|
|
|
137
|
2 |
|
return $worksheetNames; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). |
142
|
|
|
* |
143
|
|
|
* @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}> |
144
|
|
|
*/ |
145
|
6 |
|
public function listWorksheetInfo(string $filename): array |
146
|
|
|
{ |
147
|
6 |
|
File::assertFile($filename, self::INITIAL_FILE); |
148
|
|
|
|
149
|
2 |
|
$worksheetInfo = []; |
150
|
|
|
|
151
|
2 |
|
$xml = new XMLReader(); |
152
|
2 |
|
$xml->xml( |
153
|
2 |
|
$this->getSecurityScannerOrThrow() |
154
|
2 |
|
->scanFile( |
155
|
2 |
|
'zip://' . realpath($filename) . '#' . self::INITIAL_FILE |
156
|
2 |
|
) |
157
|
2 |
|
); |
158
|
2 |
|
$xml->setParserProperty(2, true); |
159
|
|
|
|
160
|
|
|
// Step into the first level of content of the XML |
161
|
2 |
|
$xml->read(); |
162
|
2 |
|
$tableVisibility = []; |
163
|
2 |
|
$lastTableStyle = ''; |
164
|
|
|
|
165
|
2 |
|
while ($xml->read()) { |
166
|
2 |
|
if ($xml->name === 'style:style') { |
167
|
2 |
|
$styleType = $xml->getAttribute('style:family'); |
168
|
2 |
|
if ($styleType === 'table') { |
169
|
2 |
|
$lastTableStyle = $xml->getAttribute('style:name'); |
170
|
|
|
} |
171
|
2 |
|
} elseif ($xml->name === 'style:table-properties') { |
172
|
2 |
|
$visibility = $xml->getAttribute('table:display'); |
173
|
2 |
|
$tableVisibility[$lastTableStyle] = ($visibility === 'false') ? Worksheet::SHEETSTATE_HIDDEN : Worksheet::SHEETSTATE_VISIBLE; |
174
|
2 |
|
} elseif ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { |
175
|
2 |
|
$worksheetNames[] = $xml->getAttribute('table:name'); |
176
|
|
|
|
177
|
2 |
|
$styleName = $xml->getAttribute('table:style-name') ?? ''; |
178
|
2 |
|
$visibility = $tableVisibility[$styleName] ?? ''; |
179
|
2 |
|
$tmpInfo = [ |
180
|
2 |
|
'worksheetName' => (string) $xml->getAttribute('table:name'), |
181
|
2 |
|
'lastColumnLetter' => 'A', |
182
|
2 |
|
'lastColumnIndex' => 0, |
183
|
2 |
|
'totalRows' => 0, |
184
|
2 |
|
'totalColumns' => 0, |
185
|
2 |
|
'sheetState' => $visibility, |
186
|
2 |
|
]; |
187
|
|
|
|
188
|
|
|
// Loop through each child node of the table:table element reading |
189
|
2 |
|
$currCells = 0; |
190
|
|
|
do { |
191
|
2 |
|
$xml->read(); |
192
|
2 |
|
if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) { |
193
|
2 |
|
$rowspan = $xml->getAttribute('table:number-rows-repeated'); |
194
|
2 |
|
$rowspan = empty($rowspan) ? 1 : (int) $rowspan; |
195
|
2 |
|
$tmpInfo['totalRows'] += $rowspan; |
196
|
2 |
|
$tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); |
197
|
2 |
|
$currCells = 0; |
198
|
|
|
// Step into the row |
199
|
2 |
|
$xml->read(); |
200
|
|
|
do { |
201
|
2 |
|
$doread = true; |
202
|
2 |
|
if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) { |
203
|
2 |
|
if (!$xml->isEmptyElement) { |
204
|
2 |
|
++$currCells; |
205
|
2 |
|
$xml->next(); |
206
|
2 |
|
$doread = false; |
207
|
|
|
} |
208
|
1 |
|
} elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) { |
209
|
1 |
|
$mergeSize = $xml->getAttribute('table:number-columns-repeated'); |
210
|
1 |
|
$currCells += (int) $mergeSize; |
211
|
|
|
} |
212
|
2 |
|
if ($doread) { |
213
|
1 |
|
$xml->read(); |
214
|
|
|
} |
215
|
2 |
|
} while ($xml->name != 'table:table-row'); |
216
|
|
|
} |
217
|
2 |
|
} while ($xml->name != 'table:table'); |
218
|
|
|
|
219
|
2 |
|
$tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); |
220
|
2 |
|
$tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1; |
221
|
2 |
|
$tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1); |
222
|
2 |
|
$worksheetInfo[] = $tmpInfo; |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
|
226
|
2 |
|
return $worksheetInfo; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* Loads PhpSpreadsheet from file. |
231
|
|
|
*/ |
232
|
78 |
|
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet |
233
|
|
|
{ |
234
|
78 |
|
$spreadsheet = $this->newSpreadsheet(); |
235
|
78 |
|
$spreadsheet->setValueBinder($this->valueBinder); |
236
|
78 |
|
$spreadsheet->removeSheetByIndex(0); |
237
|
|
|
|
238
|
|
|
// Load into this instance |
239
|
78 |
|
return $this->loadIntoExisting($filename, $spreadsheet); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Loads PhpSpreadsheet from file into PhpSpreadsheet instance. |
244
|
|
|
*/ |
245
|
82 |
|
public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet |
246
|
|
|
{ |
247
|
82 |
|
File::assertFile($filename, self::INITIAL_FILE); |
248
|
|
|
|
249
|
78 |
|
$zip = new ZipArchive(); |
250
|
78 |
|
$zip->open($filename); |
251
|
|
|
|
252
|
|
|
// Meta |
253
|
|
|
|
254
|
78 |
|
$xml = @simplexml_load_string( |
255
|
78 |
|
$this->getSecurityScannerOrThrow() |
256
|
78 |
|
->scan($zip->getFromName('meta.xml')) |
257
|
78 |
|
); |
258
|
78 |
|
if ($xml === false) { |
259
|
1 |
|
throw new Exception('Unable to read data from {$pFilename}'); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** @var array{meta?: string, office?: string, dc?: string} */ |
263
|
77 |
|
$namespacesMeta = $xml->getNamespaces(true); |
264
|
|
|
|
265
|
77 |
|
(new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta); |
266
|
|
|
|
267
|
|
|
// Styles |
268
|
|
|
|
269
|
77 |
|
$dom = new DOMDocument('1.01', 'UTF-8'); |
270
|
77 |
|
$dom->loadXML( |
271
|
77 |
|
$this->getSecurityScannerOrThrow() |
272
|
77 |
|
->scan($zip->getFromName('styles.xml')) |
273
|
77 |
|
); |
274
|
|
|
|
275
|
77 |
|
$pageSettings = new PageSettings($dom); |
276
|
|
|
|
277
|
|
|
// Main Content |
278
|
|
|
|
279
|
77 |
|
$dom = new DOMDocument('1.01', 'UTF-8'); |
280
|
77 |
|
$dom->loadXML( |
281
|
77 |
|
$this->getSecurityScannerOrThrow() |
282
|
77 |
|
->scan($zip->getFromName(self::INITIAL_FILE)) |
283
|
77 |
|
); |
284
|
|
|
|
285
|
77 |
|
$officeNs = (string) $dom->lookupNamespaceUri('office'); |
286
|
77 |
|
$tableNs = (string) $dom->lookupNamespaceUri('table'); |
287
|
77 |
|
$textNs = (string) $dom->lookupNamespaceUri('text'); |
288
|
77 |
|
$xlinkNs = (string) $dom->lookupNamespaceUri('xlink'); |
289
|
77 |
|
$styleNs = (string) $dom->lookupNamespaceUri('style'); |
290
|
|
|
|
291
|
77 |
|
$pageSettings->readStyleCrossReferences($dom); |
292
|
|
|
|
293
|
77 |
|
$autoFilterReader = new AutoFilter($spreadsheet, $tableNs); |
294
|
77 |
|
$definedNameReader = new DefinedNames($spreadsheet, $tableNs); |
295
|
77 |
|
$columnWidths = []; |
296
|
77 |
|
$automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0); |
297
|
77 |
|
$automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style'); |
298
|
77 |
|
foreach ($automaticStyles as $automaticStyle) { |
299
|
77 |
|
$styleName = $automaticStyle->getAttributeNS($styleNs, 'name'); |
300
|
77 |
|
$styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family'); |
301
|
77 |
|
if ($styleFamily === 'table-column') { |
302
|
46 |
|
$tcprops = $automaticStyle->getElementsByTagNameNS($styleNs, 'table-column-properties'); |
303
|
46 |
|
$tcprop = $tcprops->item(0); |
304
|
46 |
|
if ($tcprop !== null) { |
305
|
46 |
|
$columnWidth = $tcprop->getAttributeNs($styleNs, 'column-width'); |
306
|
46 |
|
$columnWidths[$styleName] = $columnWidth; |
307
|
|
|
} |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
// Content |
312
|
77 |
|
$item0 = $dom->getElementsByTagNameNS($officeNs, 'body')->item(0); |
313
|
77 |
|
$spreadsheets = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($officeNs, 'spreadsheet'); |
314
|
|
|
|
315
|
77 |
|
foreach ($spreadsheets as $workbookData) { |
316
|
|
|
/** @var DOMElement $workbookData */ |
317
|
77 |
|
$tables = $workbookData->getElementsByTagNameNS($tableNs, 'table'); |
318
|
|
|
|
319
|
77 |
|
$worksheetID = 0; |
320
|
77 |
|
foreach ($tables as $worksheetDataSet) { |
321
|
|
|
/** @var DOMElement $worksheetDataSet */ |
322
|
77 |
|
$worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name'); |
323
|
|
|
|
324
|
|
|
// Check loadSheetsOnly |
325
|
|
|
if ( |
326
|
77 |
|
$this->loadSheetsOnly !== null |
327
|
|
|
&& $worksheetName |
328
|
77 |
|
&& !in_array($worksheetName, $this->loadSheetsOnly) |
329
|
|
|
) { |
330
|
3 |
|
continue; |
331
|
|
|
} |
332
|
|
|
|
333
|
76 |
|
$worksheetStyleName = $worksheetDataSet->getAttributeNS($tableNs, 'style-name'); |
334
|
|
|
|
335
|
|
|
// Create sheet |
336
|
76 |
|
$spreadsheet->createSheet(); |
337
|
76 |
|
$spreadsheet->setActiveSheetIndex($worksheetID); |
338
|
|
|
|
339
|
76 |
|
if ($worksheetName || is_numeric($worksheetName)) { |
340
|
|
|
// Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in |
341
|
|
|
// formula cells... during the load, all formulae should be correct, and we're simply |
342
|
|
|
// bringing the worksheet name in line with the formula, not the reverse |
343
|
76 |
|
$spreadsheet->getActiveSheet()->setTitle((string) $worksheetName, false, false); |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
// Go through every child of table element |
347
|
76 |
|
$rowID = 1; |
348
|
76 |
|
$tableColumnIndex = 1; |
349
|
76 |
|
foreach ($worksheetDataSet->childNodes as $childNode) { |
350
|
|
|
/** @var DOMElement $childNode */ |
351
|
|
|
|
352
|
|
|
// Filter elements which are not under the "table" ns |
353
|
76 |
|
if ($childNode->namespaceURI != $tableNs) { |
354
|
45 |
|
continue; |
355
|
|
|
} |
356
|
|
|
|
357
|
76 |
|
$key = self::extractNodeName($childNode->nodeName); |
358
|
|
|
|
359
|
|
|
switch ($key) { |
360
|
76 |
|
case 'table-header-rows': |
361
|
76 |
|
case 'table-rows': |
362
|
1 |
|
$this->processTableHeaderRows( |
363
|
1 |
|
$childNode, |
364
|
1 |
|
$tableNs, |
365
|
1 |
|
$rowID, |
366
|
1 |
|
$worksheetName, |
367
|
1 |
|
$officeNs, |
368
|
1 |
|
$textNs, |
369
|
1 |
|
$xlinkNs, |
370
|
1 |
|
$spreadsheet |
371
|
1 |
|
); |
372
|
|
|
|
373
|
1 |
|
break; |
374
|
76 |
|
case 'table-row-group': |
375
|
1 |
|
$this->processTableRowGroup( |
376
|
1 |
|
$childNode, |
377
|
1 |
|
$tableNs, |
378
|
1 |
|
$rowID, |
379
|
1 |
|
$worksheetName, |
380
|
1 |
|
$officeNs, |
381
|
1 |
|
$textNs, |
382
|
1 |
|
$xlinkNs, |
383
|
1 |
|
$spreadsheet |
384
|
1 |
|
); |
385
|
|
|
|
386
|
1 |
|
break; |
387
|
76 |
|
case 'table-header-columns': |
388
|
76 |
|
case 'table-columns': |
389
|
|
|
$this->processTableHeaderColumns( |
390
|
|
|
$childNode, |
391
|
|
|
$tableNs, |
392
|
|
|
$columnWidths, |
393
|
|
|
$tableColumnIndex, |
394
|
|
|
$spreadsheet |
395
|
|
|
); |
396
|
|
|
|
397
|
|
|
break; |
398
|
76 |
|
case 'table-column-group': |
399
|
|
|
$this->processTableColumnGroup( |
400
|
|
|
$childNode, |
401
|
|
|
$tableNs, |
402
|
|
|
$columnWidths, |
403
|
|
|
$tableColumnIndex, |
404
|
|
|
$spreadsheet |
405
|
|
|
); |
406
|
|
|
|
407
|
|
|
break; |
408
|
76 |
|
case 'table-column': |
409
|
45 |
|
$this->processTableColumn( |
410
|
45 |
|
$childNode, |
411
|
45 |
|
$tableNs, |
412
|
45 |
|
$columnWidths, |
413
|
45 |
|
$tableColumnIndex, |
414
|
45 |
|
$spreadsheet |
415
|
45 |
|
); |
416
|
|
|
|
417
|
45 |
|
break; |
418
|
75 |
|
case 'table-row': |
419
|
75 |
|
$this->processTableRow( |
420
|
75 |
|
$childNode, |
421
|
75 |
|
$tableNs, |
422
|
75 |
|
$rowID, |
423
|
75 |
|
$worksheetName, |
424
|
75 |
|
$officeNs, |
425
|
75 |
|
$textNs, |
426
|
75 |
|
$xlinkNs, |
427
|
75 |
|
$spreadsheet |
428
|
75 |
|
); |
429
|
|
|
|
430
|
75 |
|
break; |
431
|
|
|
} |
432
|
|
|
} |
433
|
76 |
|
$pageSettings->setVisibilityForWorksheet( |
434
|
76 |
|
$spreadsheet->getActiveSheet(), |
435
|
76 |
|
$worksheetStyleName |
436
|
76 |
|
); |
437
|
76 |
|
$pageSettings->setPrintSettingsForWorksheet( |
438
|
76 |
|
$spreadsheet->getActiveSheet(), |
439
|
76 |
|
$worksheetStyleName |
440
|
76 |
|
); |
441
|
76 |
|
++$worksheetID; |
442
|
|
|
} |
443
|
|
|
|
444
|
77 |
|
$autoFilterReader->read($workbookData); |
445
|
77 |
|
$definedNameReader->read($workbookData); |
446
|
|
|
} |
447
|
77 |
|
$spreadsheet->setActiveSheetIndex(0); |
448
|
|
|
|
449
|
76 |
|
if ($zip->locateName('settings.xml') !== false) { |
450
|
69 |
|
$this->processSettings($zip, $spreadsheet); |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
// Return |
454
|
76 |
|
return $spreadsheet; |
455
|
|
|
} |
456
|
|
|
|
457
|
1 |
|
private function processTableHeaderRows( |
458
|
|
|
DOMElement $childNode, |
459
|
|
|
string $tableNs, |
460
|
|
|
int &$rowID, |
461
|
|
|
string $worksheetName, |
462
|
|
|
string $officeNs, |
463
|
|
|
string $textNs, |
464
|
|
|
string $xlinkNs, |
465
|
|
|
Spreadsheet $spreadsheet |
466
|
|
|
): void { |
467
|
1 |
|
foreach ($childNode->childNodes as $grandchildNode) { |
468
|
|
|
/** @var DOMElement $grandchildNode */ |
469
|
1 |
|
$grandkey = self::extractNodeName($grandchildNode->nodeName); |
470
|
|
|
switch ($grandkey) { |
471
|
1 |
|
case 'table-row': |
472
|
1 |
|
$this->processTableRow( |
473
|
1 |
|
$grandchildNode, |
474
|
1 |
|
$tableNs, |
475
|
1 |
|
$rowID, |
476
|
1 |
|
$worksheetName, |
477
|
1 |
|
$officeNs, |
478
|
1 |
|
$textNs, |
479
|
1 |
|
$xlinkNs, |
480
|
1 |
|
$spreadsheet |
481
|
1 |
|
); |
482
|
|
|
|
483
|
1 |
|
break; |
484
|
|
|
} |
485
|
|
|
} |
486
|
|
|
} |
487
|
|
|
|
488
|
1 |
|
private function processTableRowGroup( |
489
|
|
|
DOMElement $childNode, |
490
|
|
|
string $tableNs, |
491
|
|
|
int &$rowID, |
492
|
|
|
string $worksheetName, |
493
|
|
|
string $officeNs, |
494
|
|
|
string $textNs, |
495
|
|
|
string $xlinkNs, |
496
|
|
|
Spreadsheet $spreadsheet |
497
|
|
|
): void { |
498
|
1 |
|
foreach ($childNode->childNodes as $grandchildNode) { |
499
|
|
|
/** @var DOMElement $grandchildNode */ |
500
|
1 |
|
$grandkey = self::extractNodeName($grandchildNode->nodeName); |
501
|
|
|
switch ($grandkey) { |
502
|
1 |
|
case 'table-row': |
503
|
1 |
|
$this->processTableRow( |
504
|
1 |
|
$grandchildNode, |
505
|
1 |
|
$tableNs, |
506
|
1 |
|
$rowID, |
507
|
1 |
|
$worksheetName, |
508
|
1 |
|
$officeNs, |
509
|
1 |
|
$textNs, |
510
|
1 |
|
$xlinkNs, |
511
|
1 |
|
$spreadsheet |
512
|
1 |
|
); |
513
|
|
|
|
514
|
1 |
|
break; |
515
|
|
|
case 'table-header-rows': |
516
|
|
|
case 'table-rows': |
517
|
|
|
$this->processTableHeaderRows( |
518
|
|
|
$grandchildNode, |
519
|
|
|
$tableNs, |
520
|
|
|
$rowID, |
521
|
|
|
$worksheetName, |
522
|
|
|
$officeNs, |
523
|
|
|
$textNs, |
524
|
|
|
$xlinkNs, |
525
|
|
|
$spreadsheet |
526
|
|
|
); |
527
|
|
|
|
528
|
|
|
break; |
529
|
|
|
case 'table-row-group': |
530
|
|
|
$this->processTableRowGroup( |
531
|
|
|
$grandchildNode, |
532
|
|
|
$tableNs, |
533
|
|
|
$rowID, |
534
|
|
|
$worksheetName, |
535
|
|
|
$officeNs, |
536
|
|
|
$textNs, |
537
|
|
|
$xlinkNs, |
538
|
|
|
$spreadsheet |
539
|
|
|
); |
540
|
|
|
|
541
|
|
|
break; |
542
|
|
|
} |
543
|
|
|
} |
544
|
|
|
} |
545
|
|
|
|
546
|
75 |
|
private function processTableRow( |
547
|
|
|
DOMElement $childNode, |
548
|
|
|
string $tableNs, |
549
|
|
|
int &$rowID, |
550
|
|
|
string $worksheetName, |
551
|
|
|
string $officeNs, |
552
|
|
|
string $textNs, |
553
|
|
|
string $xlinkNs, |
554
|
|
|
Spreadsheet $spreadsheet |
555
|
|
|
): void { |
556
|
75 |
|
if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) { |
557
|
26 |
|
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated'); |
558
|
|
|
} else { |
559
|
75 |
|
$rowRepeats = 1; |
560
|
|
|
} |
561
|
|
|
|
562
|
75 |
|
$columnID = 'A'; |
563
|
|
|
/** @var DOMElement|DOMText $cellData */ |
564
|
75 |
|
foreach ($childNode->childNodes as $cellData) { |
565
|
75 |
|
if ($cellData instanceof DOMText) { |
566
|
2 |
|
continue; // should just be whitespace |
567
|
|
|
} |
568
|
75 |
|
if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { |
569
|
2 |
|
if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { |
570
|
1 |
|
$colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); |
571
|
|
|
} else { |
572
|
2 |
|
$colRepeats = 1; |
573
|
|
|
} |
574
|
|
|
|
575
|
2 |
|
for ($i = 0; $i < $colRepeats; ++$i) { |
576
|
2 |
|
++$columnID; |
577
|
|
|
} |
578
|
|
|
|
579
|
2 |
|
continue; |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
// Initialize variables |
583
|
75 |
|
$formatting = $hyperlink = null; |
584
|
75 |
|
$hasCalculatedValue = false; |
585
|
75 |
|
$cellDataFormula = ''; |
586
|
75 |
|
$cellDataType = ''; |
587
|
75 |
|
$cellDataRef = ''; |
588
|
|
|
|
589
|
75 |
|
if ($cellData->hasAttributeNS($tableNs, 'formula')) { |
590
|
29 |
|
$cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula'); |
591
|
29 |
|
$hasCalculatedValue = true; |
592
|
|
|
} |
593
|
75 |
|
if ($cellData->hasAttributeNS($tableNs, 'number-matrix-columns-spanned')) { |
594
|
12 |
|
if ($cellData->hasAttributeNS($tableNs, 'number-matrix-rows-spanned')) { |
595
|
12 |
|
$cellDataType = 'array'; |
596
|
12 |
|
$arrayRow = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned'); |
597
|
12 |
|
$arrayCol = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned'); |
598
|
12 |
|
$lastRow = $rowID + $arrayRow - 1; |
599
|
12 |
|
$lastCol = $columnID; |
600
|
12 |
|
while ($arrayCol > 1) { |
601
|
7 |
|
++$lastCol; |
602
|
7 |
|
--$arrayCol; |
603
|
|
|
} |
604
|
12 |
|
$cellDataRef = "$columnID$rowID:$lastCol$lastRow"; |
605
|
|
|
} |
606
|
|
|
} |
607
|
|
|
|
608
|
|
|
// Annotations |
609
|
75 |
|
$annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation'); |
610
|
|
|
|
611
|
75 |
|
if ($annotation->length > 0 && $annotation->item(0) !== null) { |
612
|
11 |
|
$textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p'); |
613
|
11 |
|
$textNodeLength = $textNode->length; |
614
|
11 |
|
$newLineOwed = false; |
615
|
11 |
|
for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) { |
616
|
11 |
|
$textNodeItem = $textNode->item($textNodeIndex); |
617
|
11 |
|
if ($textNodeItem !== null) { |
618
|
11 |
|
$text = $this->scanElementForText($textNodeItem); |
619
|
11 |
|
if ($newLineOwed) { |
620
|
1 |
|
$spreadsheet->getActiveSheet() |
621
|
1 |
|
->getComment($columnID . $rowID) |
622
|
1 |
|
->getText() |
623
|
1 |
|
->createText("\n"); |
624
|
|
|
} |
625
|
11 |
|
$newLineOwed = true; |
626
|
|
|
|
627
|
11 |
|
$spreadsheet->getActiveSheet() |
628
|
11 |
|
->getComment($columnID . $rowID) |
629
|
11 |
|
->getText() |
630
|
11 |
|
->createText( |
631
|
11 |
|
$this->parseRichText($text) |
632
|
11 |
|
); |
633
|
|
|
} |
634
|
|
|
} |
635
|
|
|
} |
636
|
|
|
|
637
|
|
|
// Content |
638
|
|
|
|
639
|
|
|
/** @var DOMElement[] $paragraphs */ |
640
|
75 |
|
$paragraphs = []; |
641
|
|
|
|
642
|
75 |
|
foreach ($cellData->childNodes as $item) { |
643
|
|
|
/** @var DOMElement $item */ |
644
|
|
|
|
645
|
|
|
// Filter text:p elements |
646
|
75 |
|
if ($item->nodeName == 'text:p') { |
647
|
75 |
|
$paragraphs[] = $item; |
648
|
|
|
} |
649
|
|
|
} |
650
|
|
|
|
651
|
75 |
|
if (count($paragraphs) > 0) { |
652
|
|
|
// Consolidate if there are multiple p records (maybe with spans as well) |
653
|
75 |
|
$dataArray = []; |
654
|
|
|
|
655
|
|
|
// Text can have multiple text:p and within those, multiple text:span. |
656
|
|
|
// text:p newlines, but text:span does not. |
657
|
|
|
// Also, here we assume there is no text data is span fields are specified, since |
658
|
|
|
// we have no way of knowing proper positioning anyway. |
659
|
|
|
|
660
|
75 |
|
foreach ($paragraphs as $pData) { |
661
|
75 |
|
$dataArray[] = $this->scanElementForText($pData); |
662
|
|
|
} |
663
|
75 |
|
$allCellDataText = implode("\n", $dataArray); |
664
|
|
|
|
665
|
75 |
|
$type = $cellData->getAttributeNS($officeNs, 'value-type'); |
666
|
|
|
|
667
|
|
|
switch ($type) { |
668
|
75 |
|
case 'string': |
669
|
49 |
|
$type = DataType::TYPE_STRING; |
670
|
49 |
|
$dataValue = $allCellDataText; |
671
|
|
|
|
672
|
49 |
|
foreach ($paragraphs as $paragraph) { |
673
|
49 |
|
$link = $paragraph->getElementsByTagNameNS($textNs, 'a'); |
674
|
49 |
|
if ($link->length > 0 && $link->item(0) !== null) { |
675
|
7 |
|
$hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href'); |
676
|
|
|
} |
677
|
|
|
} |
678
|
|
|
|
679
|
49 |
|
break; |
680
|
51 |
|
case 'boolean': |
681
|
9 |
|
$type = DataType::TYPE_BOOL; |
682
|
9 |
|
$dataValue = ($cellData->getAttributeNS($officeNs, 'boolean-value') === 'true') ? true : false; |
683
|
|
|
|
684
|
9 |
|
break; |
685
|
49 |
|
case 'percentage': |
686
|
4 |
|
$type = DataType::TYPE_NUMERIC; |
687
|
4 |
|
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); |
688
|
|
|
|
689
|
|
|
// percentage should always be float |
690
|
|
|
//if (floor($dataValue) == $dataValue) { |
691
|
|
|
// $dataValue = (int) $dataValue; |
692
|
|
|
//} |
693
|
4 |
|
$formatting = NumberFormat::FORMAT_PERCENTAGE_00; |
694
|
|
|
|
695
|
4 |
|
break; |
696
|
49 |
|
case 'currency': |
697
|
4 |
|
$type = DataType::TYPE_NUMERIC; |
698
|
4 |
|
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); |
699
|
|
|
|
700
|
4 |
|
if (floor($dataValue) == $dataValue) { |
701
|
4 |
|
$dataValue = (int) $dataValue; |
702
|
|
|
} |
703
|
4 |
|
$formatting = NumberFormat::FORMAT_CURRENCY_USD_INTEGER; |
704
|
|
|
|
705
|
4 |
|
break; |
706
|
45 |
|
case 'float': |
707
|
45 |
|
$type = DataType::TYPE_NUMERIC; |
708
|
45 |
|
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); |
709
|
|
|
|
710
|
45 |
|
if (floor($dataValue) == $dataValue) { |
711
|
41 |
|
if ($dataValue == (int) $dataValue) { |
712
|
41 |
|
$dataValue = (int) $dataValue; |
713
|
|
|
} |
714
|
|
|
} |
715
|
|
|
|
716
|
45 |
|
break; |
717
|
10 |
|
case 'date': |
718
|
9 |
|
$type = DataType::TYPE_NUMERIC; |
719
|
9 |
|
$value = $cellData->getAttributeNS($officeNs, 'date-value'); |
720
|
9 |
|
$dataValue = Date::convertIsoDate($value); |
721
|
|
|
|
722
|
9 |
|
if ($dataValue != floor($dataValue)) { |
723
|
6 |
|
$formatting = NumberFormat::FORMAT_DATE_XLSX15 |
724
|
6 |
|
. ' ' |
725
|
6 |
|
. NumberFormat::FORMAT_DATE_TIME4; |
726
|
|
|
} else { |
727
|
9 |
|
$formatting = NumberFormat::FORMAT_DATE_XLSX15; |
728
|
|
|
} |
729
|
|
|
|
730
|
9 |
|
break; |
731
|
7 |
|
case 'time': |
732
|
6 |
|
$type = DataType::TYPE_NUMERIC; |
733
|
|
|
|
734
|
6 |
|
$timeValue = $cellData->getAttributeNS($officeNs, 'time-value'); |
735
|
|
|
|
736
|
6 |
|
$dataValue = Date::PHPToExcel( |
737
|
6 |
|
strtotime( |
738
|
6 |
|
'01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? []) |
739
|
6 |
|
) |
740
|
6 |
|
); |
741
|
6 |
|
$formatting = NumberFormat::FORMAT_DATE_TIME4; |
742
|
|
|
|
743
|
6 |
|
break; |
744
|
|
|
default: |
745
|
1 |
|
$dataValue = null; |
746
|
|
|
} |
747
|
|
|
} else { |
748
|
43 |
|
$type = DataType::TYPE_NULL; |
749
|
43 |
|
$dataValue = null; |
750
|
|
|
} |
751
|
|
|
|
752
|
75 |
|
if ($hasCalculatedValue) { |
753
|
29 |
|
$type = DataType::TYPE_FORMULA; |
754
|
29 |
|
$cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1); |
755
|
29 |
|
$cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula); |
756
|
|
|
} |
757
|
|
|
|
758
|
75 |
|
if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { |
759
|
38 |
|
$colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); |
760
|
|
|
} else { |
761
|
75 |
|
$colRepeats = 1; |
762
|
|
|
} |
763
|
|
|
|
764
|
75 |
|
if ($type !== null) { // @phpstan-ignore-line |
765
|
75 |
|
for ($i = 0; $i < $colRepeats; ++$i) { |
766
|
75 |
|
if ($i > 0) { |
767
|
38 |
|
++$columnID; |
768
|
|
|
} |
769
|
|
|
|
770
|
75 |
|
if ($type !== DataType::TYPE_NULL) { |
771
|
75 |
|
for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) { |
772
|
75 |
|
$rID = $rowID + $rowAdjust; |
773
|
|
|
|
774
|
75 |
|
$cell = $spreadsheet->getActiveSheet() |
775
|
75 |
|
->getCell($columnID . $rID); |
776
|
|
|
|
777
|
|
|
// Set value |
778
|
75 |
|
if ($hasCalculatedValue) { |
779
|
29 |
|
$cell->setValueExplicit($cellDataFormula, $type); |
780
|
29 |
|
if ($cellDataType === 'array') { |
781
|
12 |
|
$cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]); |
782
|
|
|
} |
783
|
71 |
|
} elseif ($type !== '' || $dataValue !== null) { |
784
|
71 |
|
$cell->setValueExplicit($dataValue, $type); |
785
|
|
|
} |
786
|
|
|
|
787
|
75 |
|
if ($hasCalculatedValue) { |
788
|
29 |
|
$cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC); |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
// Set other properties |
792
|
75 |
|
if ($formatting !== null) { |
793
|
13 |
|
$spreadsheet->getActiveSheet() |
794
|
13 |
|
->getStyle($columnID . $rID) |
795
|
13 |
|
->getNumberFormat() |
796
|
13 |
|
->setFormatCode($formatting); |
797
|
|
|
} else { |
798
|
75 |
|
$spreadsheet->getActiveSheet() |
799
|
75 |
|
->getStyle($columnID . $rID) |
800
|
75 |
|
->getNumberFormat() |
801
|
75 |
|
->setFormatCode(NumberFormat::FORMAT_GENERAL); |
802
|
|
|
} |
803
|
|
|
|
804
|
75 |
|
if ($hyperlink !== null) { |
805
|
7 |
|
if ($hyperlink[0] === '#') { |
806
|
1 |
|
$hyperlink = 'sheet://' . substr($hyperlink, 1); |
807
|
|
|
} |
808
|
7 |
|
$cell->getHyperlink() |
809
|
7 |
|
->setUrl($hyperlink); |
810
|
|
|
} |
811
|
|
|
} |
812
|
|
|
} |
813
|
|
|
} |
814
|
|
|
} |
815
|
|
|
|
816
|
|
|
// Merged cells |
817
|
75 |
|
$this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet); |
818
|
|
|
|
819
|
75 |
|
++$columnID; |
820
|
|
|
} |
821
|
75 |
|
$rowID += $rowRepeats; |
822
|
|
|
} |
823
|
|
|
|
824
|
76 |
|
private static function extractNodeName(string $key): string |
825
|
|
|
{ |
826
|
|
|
// Remove ns from node name |
827
|
76 |
|
if (str_contains($key, ':')) { |
828
|
76 |
|
$keyChunks = explode(':', $key); |
829
|
76 |
|
$key = array_pop($keyChunks); |
830
|
|
|
} |
831
|
|
|
|
832
|
76 |
|
return $key; |
833
|
|
|
} |
834
|
|
|
|
835
|
|
|
/** |
836
|
|
|
* @param string[] $columnWidths |
837
|
|
|
*/ |
838
|
|
|
private function processTableHeaderColumns( |
839
|
|
|
DOMElement $childNode, |
840
|
|
|
string $tableNs, |
841
|
|
|
array $columnWidths, |
842
|
|
|
int &$tableColumnIndex, |
843
|
|
|
Spreadsheet $spreadsheet |
844
|
|
|
): void { |
845
|
|
|
foreach ($childNode->childNodes as $grandchildNode) { |
846
|
|
|
/** @var DOMElement $grandchildNode */ |
847
|
|
|
$grandkey = self::extractNodeName($grandchildNode->nodeName); |
848
|
|
|
switch ($grandkey) { |
849
|
|
|
case 'table-column': |
850
|
|
|
$this->processTableColumn( |
851
|
|
|
$grandchildNode, |
852
|
|
|
$tableNs, |
853
|
|
|
$columnWidths, |
854
|
|
|
$tableColumnIndex, |
855
|
|
|
$spreadsheet |
856
|
|
|
); |
857
|
|
|
|
858
|
|
|
break; |
859
|
|
|
} |
860
|
|
|
} |
861
|
|
|
} |
862
|
|
|
|
863
|
|
|
/** |
864
|
|
|
* @param string[] $columnWidths |
865
|
|
|
*/ |
866
|
|
|
private function processTableColumnGroup( |
867
|
|
|
DOMElement $childNode, |
868
|
|
|
string $tableNs, |
869
|
|
|
array $columnWidths, |
870
|
|
|
int &$tableColumnIndex, |
871
|
|
|
Spreadsheet $spreadsheet |
872
|
|
|
): void { |
873
|
|
|
foreach ($childNode->childNodes as $grandchildNode) { |
874
|
|
|
/** @var DOMElement $grandchildNode */ |
875
|
|
|
$grandkey = self::extractNodeName($grandchildNode->nodeName); |
876
|
|
|
switch ($grandkey) { |
877
|
|
|
case 'table-column': |
878
|
|
|
$this->processTableColumn( |
879
|
|
|
$grandchildNode, |
880
|
|
|
$tableNs, |
881
|
|
|
$columnWidths, |
882
|
|
|
$tableColumnIndex, |
883
|
|
|
$spreadsheet |
884
|
|
|
); |
885
|
|
|
|
886
|
|
|
break; |
887
|
|
|
case 'table-header-columns': |
888
|
|
|
case 'table-columns': |
889
|
|
|
$this->processTableHeaderColumns( |
890
|
|
|
$grandchildNode, |
891
|
|
|
$tableNs, |
892
|
|
|
$columnWidths, |
893
|
|
|
$tableColumnIndex, |
894
|
|
|
$spreadsheet |
895
|
|
|
); |
896
|
|
|
|
897
|
|
|
break; |
898
|
|
|
case 'table-column-group': |
899
|
|
|
$this->processTableColumnGroup( |
900
|
|
|
$grandchildNode, |
901
|
|
|
$tableNs, |
902
|
|
|
$columnWidths, |
903
|
|
|
$tableColumnIndex, |
904
|
|
|
$spreadsheet |
905
|
|
|
); |
906
|
|
|
|
907
|
|
|
break; |
908
|
|
|
} |
909
|
|
|
} |
910
|
|
|
} |
911
|
|
|
|
912
|
|
|
/** |
913
|
|
|
* @param string[] $columnWidths |
914
|
|
|
*/ |
915
|
45 |
|
private function processTableColumn( |
916
|
|
|
DOMElement $childNode, |
917
|
|
|
string $tableNs, |
918
|
|
|
array $columnWidths, |
919
|
|
|
int &$tableColumnIndex, |
920
|
|
|
Spreadsheet $spreadsheet |
921
|
|
|
): void { |
922
|
45 |
|
if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) { |
923
|
40 |
|
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated'); |
924
|
|
|
} else { |
925
|
19 |
|
$rowRepeats = 1; |
926
|
|
|
} |
927
|
45 |
|
$tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name'); |
928
|
45 |
|
if (isset($columnWidths[$tableStyleName])) { |
929
|
45 |
|
$columnWidth = new HelperDimension($columnWidths[$tableStyleName]); |
930
|
45 |
|
$tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex); |
931
|
45 |
|
for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) { |
932
|
|
|
/** @var string $tableColumnString */ |
933
|
45 |
|
$spreadsheet->getActiveSheet() |
934
|
45 |
|
->getColumnDimension($tableColumnString) |
935
|
45 |
|
->setWidth($columnWidth->toUnit('cm'), 'cm'); |
936
|
45 |
|
++$tableColumnString; |
937
|
|
|
} |
938
|
|
|
} |
939
|
45 |
|
$tableColumnIndex += $rowRepeats; |
940
|
|
|
} |
941
|
|
|
|
942
|
69 |
|
private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void |
943
|
|
|
{ |
944
|
69 |
|
$dom = new DOMDocument('1.01', 'UTF-8'); |
945
|
69 |
|
$dom->loadXML( |
946
|
69 |
|
$this->getSecurityScannerOrThrow() |
947
|
69 |
|
->scan($zip->getFromName('settings.xml')) |
948
|
69 |
|
); |
949
|
69 |
|
$configNs = (string) $dom->lookupNamespaceUri('config'); |
950
|
69 |
|
$officeNs = (string) $dom->lookupNamespaceUri('office'); |
951
|
69 |
|
$settings = $dom->getElementsByTagNameNS($officeNs, 'settings') |
952
|
69 |
|
->item(0); |
953
|
69 |
|
if ($settings !== null) { |
954
|
69 |
|
$this->lookForActiveSheet($settings, $spreadsheet, $configNs); |
955
|
69 |
|
$this->lookForSelectedCells($settings, $spreadsheet, $configNs); |
956
|
|
|
} |
957
|
|
|
} |
958
|
|
|
|
959
|
69 |
|
private function lookForActiveSheet(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void |
960
|
|
|
{ |
961
|
|
|
/** @var DOMElement $t */ |
962
|
69 |
|
foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) { |
963
|
68 |
|
if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') { |
964
|
|
|
try { |
965
|
68 |
|
$spreadsheet->setActiveSheetIndexByName($t->nodeValue ?? ''); |
966
|
1 |
|
} catch (Throwable) { |
967
|
|
|
// do nothing |
968
|
|
|
} |
969
|
|
|
|
970
|
68 |
|
break; |
971
|
|
|
} |
972
|
|
|
} |
973
|
|
|
} |
974
|
|
|
|
975
|
69 |
|
private function lookForSelectedCells(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void |
976
|
|
|
{ |
977
|
|
|
/** @var DOMElement $t */ |
978
|
69 |
|
foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) { |
979
|
68 |
|
if ($t->getAttributeNs($configNs, 'name') === 'Tables') { |
980
|
68 |
|
foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) { |
981
|
68 |
|
$setRow = $setCol = ''; |
982
|
68 |
|
$wsname = $ws->getAttributeNs($configNs, 'name'); |
983
|
68 |
|
foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) { |
984
|
68 |
|
$attrName = $configItem->getAttributeNs($configNs, 'name'); |
985
|
68 |
|
if ($attrName === 'CursorPositionX') { |
986
|
68 |
|
$setCol = $configItem->nodeValue; |
987
|
|
|
} |
988
|
68 |
|
if ($attrName === 'CursorPositionY') { |
989
|
68 |
|
$setRow = $configItem->nodeValue; |
990
|
|
|
} |
991
|
|
|
} |
992
|
68 |
|
$this->setSelected($spreadsheet, $wsname, "$setCol", "$setRow"); |
993
|
|
|
} |
994
|
|
|
|
995
|
68 |
|
break; |
996
|
|
|
} |
997
|
|
|
} |
998
|
|
|
} |
999
|
|
|
|
1000
|
68 |
|
private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void |
1001
|
|
|
{ |
1002
|
68 |
|
if (is_numeric($setCol) && is_numeric($setRow)) { |
1003
|
68 |
|
$sheet = $spreadsheet->getSheetByName($wsname); |
1004
|
68 |
|
if ($sheet !== null) { |
1005
|
68 |
|
$sheet->setSelectedCells([(int) $setCol + 1, (int) $setRow + 1]); |
1006
|
|
|
} |
1007
|
|
|
} |
1008
|
|
|
} |
1009
|
|
|
|
1010
|
|
|
/** |
1011
|
|
|
* Recursively scan element. |
1012
|
|
|
*/ |
1013
|
75 |
|
protected function scanElementForText(DOMNode $element): string |
1014
|
|
|
{ |
1015
|
75 |
|
$str = ''; |
1016
|
75 |
|
foreach ($element->childNodes as $child) { |
1017
|
|
|
/** @var DOMNode $child */ |
1018
|
75 |
|
if ($child->nodeType == XML_TEXT_NODE) { |
1019
|
75 |
|
$str .= $child->nodeValue; |
1020
|
18 |
|
} elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:line-break') { |
1021
|
1 |
|
$str .= "\n"; |
1022
|
18 |
|
} elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') { |
1023
|
|
|
// It's a space |
1024
|
|
|
|
1025
|
|
|
// Multiple spaces? |
1026
|
6 |
|
$attributes = $child->attributes; |
1027
|
|
|
/** @var ?DOMAttr $cAttr */ |
1028
|
6 |
|
$cAttr = ($attributes === null) ? null : $attributes->getNamedItem('c'); |
1029
|
6 |
|
$multiplier = self::getMultiplier($cAttr); |
1030
|
6 |
|
$str .= str_repeat(' ', $multiplier); |
1031
|
|
|
} |
1032
|
|
|
|
1033
|
75 |
|
if ($child->hasChildNodes()) { |
1034
|
16 |
|
$str .= $this->scanElementForText($child); |
1035
|
|
|
} |
1036
|
|
|
} |
1037
|
|
|
|
1038
|
75 |
|
return $str; |
1039
|
|
|
} |
1040
|
|
|
|
1041
|
6 |
|
private static function getMultiplier(?DOMAttr $cAttr): int |
1042
|
|
|
{ |
1043
|
6 |
|
if ($cAttr) { |
1044
|
6 |
|
$multiplier = (int) $cAttr->nodeValue; |
1045
|
|
|
} else { |
1046
|
6 |
|
$multiplier = 1; |
1047
|
|
|
} |
1048
|
|
|
|
1049
|
6 |
|
return $multiplier; |
1050
|
|
|
} |
1051
|
|
|
|
1052
|
11 |
|
private function parseRichText(string $is): RichText |
1053
|
|
|
{ |
1054
|
11 |
|
$value = new RichText(); |
1055
|
11 |
|
$value->createText($is); |
1056
|
|
|
|
1057
|
11 |
|
return $value; |
1058
|
|
|
} |
1059
|
|
|
|
1060
|
75 |
|
private function processMergedCells( |
1061
|
|
|
DOMElement $cellData, |
1062
|
|
|
string $tableNs, |
1063
|
|
|
string $type, |
1064
|
|
|
string $columnID, |
1065
|
|
|
int $rowID, |
1066
|
|
|
Spreadsheet $spreadsheet |
1067
|
|
|
): void { |
1068
|
|
|
if ( |
1069
|
75 |
|
$cellData->hasAttributeNS($tableNs, 'number-columns-spanned') |
1070
|
75 |
|
|| $cellData->hasAttributeNS($tableNs, 'number-rows-spanned') |
1071
|
|
|
) { |
1072
|
18 |
|
if (($type !== DataType::TYPE_NULL) || ($this->readDataOnly === false)) { |
1073
|
18 |
|
$columnTo = $columnID; |
1074
|
|
|
|
1075
|
18 |
|
if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) { |
1076
|
17 |
|
$columnIndex = Coordinate::columnIndexFromString($columnID); |
1077
|
17 |
|
$columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned'); |
1078
|
17 |
|
$columnIndex -= 2; |
1079
|
|
|
|
1080
|
17 |
|
$columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1); |
1081
|
|
|
} |
1082
|
|
|
|
1083
|
18 |
|
$rowTo = $rowID; |
1084
|
|
|
|
1085
|
18 |
|
if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) { |
1086
|
18 |
|
$rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1; |
1087
|
|
|
} |
1088
|
|
|
|
1089
|
18 |
|
$cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo; |
1090
|
18 |
|
$spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); |
1091
|
|
|
} |
1092
|
|
|
} |
1093
|
|
|
} |
1094
|
|
|
} |
1095
|
|
|
|