PluginProbe
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin / 6.5.1.7
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin v6.5.1.7
6.5.1.7 6.5.1.6 6.5.1.5 6.5.1.4 6.5.1.3 6.5.1.2 6.5.1.1 6.5.0.9 6.5.0.8 6.5.0.7 6.5.0.6 trunk 3.4.2.40 3.4.2.41 3.4.2.42 3.4.2.43 3.4.2.44 3.4.2.45 3.4.2.46 3.4.2.47 3.4.2.48 3.4.2.49 3.4.2.50 6.3.2 6.3.3.1 All 47 releases
wpdatatables / lib / phpoffice / phpspreadsheet / src / PhpSpreadsheet / Reader / Gnumeric.php

Gnumeric.php in wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin 6.5.1.7, at lib/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php

605 lines 21.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace PhpOffice\PhpSpreadsheet\Reader;
4
5 use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6 use PhpOffice\PhpSpreadsheet\Cell\DataType;
7 use PhpOffice\PhpSpreadsheet\DefinedName;
8 use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\PageSetup;
9 use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\Properties;
10 use PhpOffice\PhpSpreadsheet\Reader\Gnumeric\Styles;
11 use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
12 use PhpOffice\PhpSpreadsheet\ReferenceHelper;
13 use PhpOffice\PhpSpreadsheet\RichText\RichText;
14 use PhpOffice\PhpSpreadsheet\Shared\File;
15 use PhpOffice\PhpSpreadsheet\Spreadsheet;
16 use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
17 use SimpleXMLElement;
18 use XMLReader;
19
20 class Gnumeric extends BaseReader
21 {
22 const NAMESPACE_GNM = 'http://www.gnumeric.org/v10.dtd'; // gmr in old sheets
23
24 const NAMESPACE_XSI = 'http://www.w3.org/2001/XMLSchema-instance';
25
26 const NAMESPACE_OFFICE = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0';
27
28 const NAMESPACE_XLINK = 'http://www.w3.org/1999/xlink';
29
30 const NAMESPACE_DC = 'http://purl.org/dc/elements/1.1/';
31
32 const NAMESPACE_META = 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0';
33
34 const NAMESPACE_OOO = 'http://openoffice.org/2004/office';
35
36 /**
37 * Shared Expressions.
38 *
39 * @var array
40 */
41 private $expressions = [];
42
43 /**
44 * Spreadsheet shared across all functions.
45 *
46 * @var Spreadsheet
47 */
48 private $spreadsheet;
49
50 /** @var ReferenceHelper */
51 private $referenceHelper;
52
53 /** @var array */
54 public static $mappings = [
55 'dataType' => [
56 '10' => DataType::TYPE_NULL,
57 '20' => DataType::TYPE_BOOL,
58 '30' => DataType::TYPE_NUMERIC, // Integer doesn't exist in Excel
59 '40' => DataType::TYPE_NUMERIC, // Float
60 '50' => DataType::TYPE_ERROR,
61 '60' => DataType::TYPE_STRING,
62 //'70': // Cell Range
63 //'80': // Array
64 ],
65 ];
66
67 /**
68 * Create a new Gnumeric.
69 */
70 public function __construct()
71 {
72 parent::__construct();
73 $this->referenceHelper = ReferenceHelper::getInstance();
74 $this->securityScanner = XmlScanner::getInstance($this);
75 }
76
77 /**
78 * Can the current IReader read the file?
79 */
80 public function canRead(string $filename): bool
81 {
82 $data = null;
83 if (File::testFileNoThrow($filename)) {
84 $data = $this->gzfileGetContents($filename);
85 if (strpos($data, self::NAMESPACE_GNM) === false) {
86 $data = '';
87 }
88 }
89
90 return !empty($data);
91 }
92
93 private static function matchXml(XMLReader $xml, string $expectedLocalName): bool
94 {
95 return $xml->namespaceURI === self::NAMESPACE_GNM
96 && $xml->localName === $expectedLocalName
97 && $xml->nodeType === XMLReader::ELEMENT;
98 }
99
100 /**
101 * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
102 *
103 * @param string $filename
104 *
105 * @return array
106 */
107 public function listWorksheetNames($filename)
108 {
109 File::assertFile($filename);
110 if (!$this->canRead($filename)) {
111 throw new Exception($filename . ' is an invalid Gnumeric file.');
112 }
113
114 $xml = new XMLReader();
115 $contents = $this->gzfileGetContents($filename);
116 $xml->xml($contents);
117 $xml->setParserProperty(2, true);
118
119 $worksheetNames = [];
120 while ($xml->read()) {
121 if (self::matchXml($xml, 'SheetName')) {
122 $xml->read(); // Move onto the value node
123 $worksheetNames[] = (string) $xml->value;
124 } elseif (self::matchXml($xml, 'Sheets')) {
125 // break out of the loop once we've got our sheet names rather than parse the entire file
126 break;
127 }
128 }
129
130 return $worksheetNames;
131 }
132
133 /**
134 * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
135 *
136 * @param string $filename
137 *
138 * @return array
139 */
140 public function listWorksheetInfo($filename)
141 {
142 File::assertFile($filename);
143 if (!$this->canRead($filename)) {
144 throw new Exception($filename . ' is an invalid Gnumeric file.');
145 }
146
147 $xml = new XMLReader();
148 $contents = $this->gzfileGetContents($filename);
149 $xml->xml($contents);
150 $xml->setParserProperty(2, true);
151
152 $worksheetInfo = [];
153 while ($xml->read()) {
154 if (self::matchXml($xml, 'Sheet')) {
155 $tmpInfo = [
156 'worksheetName' => '',
157 'lastColumnLetter' => 'A',
158 'lastColumnIndex' => 0,
159 'totalRows' => 0,
160 'totalColumns' => 0,
161 ];
162
163 while ($xml->read()) {
164 if (self::matchXml($xml, 'Name')) {
165 $xml->read(); // Move onto the value node
166 $tmpInfo['worksheetName'] = (string) $xml->value;
167 } elseif (self::matchXml($xml, 'MaxCol')) {
168 $xml->read(); // Move onto the value node
169 $tmpInfo['lastColumnIndex'] = (int) $xml->value;
170 $tmpInfo['totalColumns'] = (int) $xml->value + 1;
171 } elseif (self::matchXml($xml, 'MaxRow')) {
172 $xml->read(); // Move onto the value node
173 $tmpInfo['totalRows'] = (int) $xml->value + 1;
174
175 break;
176 }
177 }
178 $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
179 $worksheetInfo[] = $tmpInfo;
180 }
181 }
182
183 return $worksheetInfo;
184 }
185
186 /**
187 * @param string $filename
188 *
189 * @return string
190 */
191 private function gzfileGetContents($filename)
192 {
193 $data = '';
194 $contents = @file_get_contents($filename);
195 if ($contents !== false) {
196 if (substr($contents, 0, 2) === "\x1f\x8b") {
197 // Check if gzlib functions are available
198 if (function_exists('gzdecode')) {
199 $contents = @gzdecode($contents);
200 if ($contents !== false) {
201 $data = $contents;
202 }
203 }
204 } else {
205 $data = $contents;
206 }
207 }
208 if ($data !== '') {
209 $data = $this->getSecurityScannerOrThrow()->scan($data);
210 }
211
212 return $data;
213 }
214
215 public static function gnumericMappings(): array
216 {
217 return array_merge(self::$mappings, Styles::$mappings);
218 }
219
220 private function processComments(SimpleXMLElement $sheet): void
221 {
222 if ((!$this->readDataOnly) && (isset($sheet->Objects))) {
223 foreach ($sheet->Objects->children(self::NAMESPACE_GNM) as $key => $comment) {
224 $commentAttributes = $comment->attributes();
225 // Only comment objects are handled at the moment
226 if ($commentAttributes && $commentAttributes->Text) {
227 $this->spreadsheet->getActiveSheet()->getComment((string) $commentAttributes->ObjectBound)
228 ->setAuthor((string) $commentAttributes->Author)
229 ->setText($this->parseRichText((string) $commentAttributes->Text));
230 }
231 }
232 }
233 }
234
235 /**
236 * @param mixed $value
237 */
238 private static function testSimpleXml($value): SimpleXMLElement
239 {
240 return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><root></root>');
241 }
242
243 /**
244 * Loads Spreadsheet from file.
245 */
246 protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
247 {
248 // Create new Spreadsheet
249 $spreadsheet = new Spreadsheet();
250 $spreadsheet->removeSheetByIndex(0);
251
252 // Load into this instance
253 return $this->loadIntoExisting($filename, $spreadsheet);
254 }
255
256 /**
257 * Loads from file into Spreadsheet instance.
258 */
259 public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
260 {
261 $this->spreadsheet = $spreadsheet;
262 File::assertFile($filename);
263 if (!$this->canRead($filename)) {
264 throw new Exception($filename . ' is an invalid Gnumeric file.');
265 }
266
267 $gFileData = $this->gzfileGetContents($filename);
268
269 $xml2 = simplexml_load_string($gFileData);
270 $xml = self::testSimpleXml($xml2);
271
272 $gnmXML = $xml->children(self::NAMESPACE_GNM);
273 (new Properties($this->spreadsheet))->readProperties($xml, $gnmXML);
274
275 $worksheetID = 0;
276 foreach ($gnmXML->Sheets->Sheet as $sheetOrNull) {
277 $sheet = self::testSimpleXml($sheetOrNull);
278 $worksheetName = (string) $sheet->Name;
279 if (is_array($this->loadSheetsOnly) && !in_array($worksheetName, $this->loadSheetsOnly, true)) {
280 continue;
281 }
282
283 $maxRow = $maxCol = 0;
284
285 // Create new Worksheet
286 $this->spreadsheet->createSheet();
287 $this->spreadsheet->setActiveSheetIndex($worksheetID);
288 // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula
289 // cells... during the load, all formulae should be correct, and we're simply bringing the worksheet
290 // name in line with the formula, not the reverse
291 $this->spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false);
292
293 $visibility = $sheet->attributes()['Visibility'] ?? 'GNM_SHEET_VISIBILITY_VISIBLE';
294 if ((string) $visibility !== 'GNM_SHEET_VISIBILITY_VISIBLE') {
295 $this->spreadsheet->getActiveSheet()->setSheetState(Worksheet::SHEETSTATE_HIDDEN);
296 }
297
298 if (!$this->readDataOnly) {
299 (new PageSetup($this->spreadsheet))
300 ->printInformation($sheet)
301 ->sheetMargins($sheet);
302 }
303
304 foreach ($sheet->Cells->Cell as $cellOrNull) {
305 $cell = self::testSimpleXml($cellOrNull);
306 $cellAttributes = self::testSimpleXml($cell->attributes());
307 $row = (int) $cellAttributes->Row + 1;
308 $column = (int) $cellAttributes->Col;
309
310 $maxRow = max($maxRow, $row);
311 $maxCol = max($maxCol, $column);
312
313 $column = Coordinate::stringFromColumnIndex($column + 1);
314
315 // Read cell?
316 if ($this->getReadFilter() !== null) {
317 if (!$this->getReadFilter()->readCell($column, $row, $worksheetName)) {
318 continue;
319 }
320 }
321
322 $this->loadCell($cell, $worksheetName, $cellAttributes, $column, $row);
323 }
324
325 if ($sheet->Styles !== null) {
326 (new Styles($this->spreadsheet, $this->readDataOnly))->read($sheet, $maxRow, $maxCol);
327 }
328
329 $this->processComments($sheet);
330 $this->processColumnWidths($sheet, $maxCol);
331 $this->processRowHeights($sheet, $maxRow);
332 $this->processMergedCells($sheet);
333 $this->processAutofilter($sheet);
334
335 $this->setSelectedCells($sheet);
336 ++$worksheetID;
337 }
338
339 $this->processDefinedNames($gnmXML);
340
341 $this->setSelectedSheet($gnmXML);
342
343 // Return
344 return $this->spreadsheet;
345 }
346
347 private function setSelectedSheet(SimpleXMLElement $gnmXML): void
348 {
349 if (isset($gnmXML->UIData)) {
350 $attributes = self::testSimpleXml($gnmXML->UIData->attributes());
351 $selectedSheet = (int) $attributes['SelectedTab'];
352 $this->spreadsheet->setActiveSheetIndex($selectedSheet);
353 }
354 }
355
356 private function setSelectedCells(?SimpleXMLElement $sheet): void
357 {
358 if ($sheet !== null && isset($sheet->Selections)) {
359 foreach ($sheet->Selections as $selection) {
360 $startCol = (int) ($selection->StartCol ?? 0);
361 $startRow = (int) ($selection->StartRow ?? 0) + 1;
362 $endCol = (int) ($selection->EndCol ?? $startCol);
363 $endRow = (int) ($selection->endRow ?? 0) + 1;
364
365 $startColumn = Coordinate::stringFromColumnIndex($startCol + 1);
366 $endColumn = Coordinate::stringFromColumnIndex($endCol + 1);
367
368 $startCell = "{$startColumn}{$startRow}";
369 $endCell = "{$endColumn}{$endRow}";
370 $selectedRange = $startCell . (($endCell !== $startCell) ? ':' . $endCell : '');
371 $this->spreadsheet->getActiveSheet()->setSelectedCell($selectedRange);
372
373 break;
374 }
375 }
376 }
377
378 private function processMergedCells(?SimpleXMLElement $sheet): void
379 {
380 // Handle Merged Cells in this worksheet
381 if ($sheet !== null && isset($sheet->MergedRegions)) {
382 foreach ($sheet->MergedRegions->Merge as $mergeCells) {
383 if (strpos((string) $mergeCells, ':') !== false) {
384 $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells, Worksheet::MERGE_CELL_CONTENT_HIDE);
385 }
386 }
387 }
388 }
389
390 private function processAutofilter(?SimpleXMLElement $sheet): void
391 {
392 if ($sheet !== null && isset($sheet->Filters)) {
393 foreach ($sheet->Filters->Filter as $autofilter) {
394 if ($autofilter !== null) {
395 $attributes = $autofilter->attributes();
396 if (isset($attributes['Area'])) {
397 $this->spreadsheet->getActiveSheet()->setAutoFilter((string) $attributes['Area']);
398 }
399 }
400 }
401 }
402 }
403
404 private function setColumnWidth(int $whichColumn, float $defaultWidth): void
405 {
406 $columnDimension = $this->spreadsheet->getActiveSheet()
407 ->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1));
408 if ($columnDimension !== null) {
409 $columnDimension->setWidth($defaultWidth);
410 }
411 }
412
413 private function setColumnInvisible(int $whichColumn): void
414 {
415 $columnDimension = $this->spreadsheet->getActiveSheet()
416 ->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1));
417 if ($columnDimension !== null) {
418 $columnDimension->setVisible(false);
419 }
420 }
421
422 private function processColumnLoop(int $whichColumn, int $maxCol, ?SimpleXMLElement $columnOverride, float $defaultWidth): int
423 {
424 $columnOverride = self::testSimpleXml($columnOverride);
425 $columnAttributes = self::testSimpleXml($columnOverride->attributes());
426 $column = $columnAttributes['No'];
427 $columnWidth = ((float) $columnAttributes['Unit']) / 5.4;
428 $hidden = (isset($columnAttributes['Hidden'])) && ((string) $columnAttributes['Hidden'] == '1');
429 $columnCount = (int) ($columnAttributes['Count'] ?? 1);
430 while ($whichColumn < $column) {
431 $this->setColumnWidth($whichColumn, $defaultWidth);
432 ++$whichColumn;
433 }
434 while (($whichColumn < ($column + $columnCount)) && ($whichColumn <= $maxCol)) {
435 $this->setColumnWidth($whichColumn, $columnWidth);
436 if ($hidden) {
437 $this->setColumnInvisible($whichColumn);
438 }
439 ++$whichColumn;
440 }
441
442 return $whichColumn;
443 }
444
445 private function processColumnWidths(?SimpleXMLElement $sheet, int $maxCol): void
446 {
447 if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Cols))) {
448 // Column Widths
449 $defaultWidth = 0;
450 $columnAttributes = $sheet->Cols->attributes();
451 if ($columnAttributes !== null) {
452 $defaultWidth = $columnAttributes['DefaultSizePts'] / 5.4;
453 }
454 $whichColumn = 0;
455 foreach ($sheet->Cols->ColInfo as $columnOverride) {
456 $whichColumn = $this->processColumnLoop($whichColumn, $maxCol, $columnOverride, $defaultWidth);
457 }
458 while ($whichColumn <= $maxCol) {
459 $this->setColumnWidth($whichColumn, $defaultWidth);
460 ++$whichColumn;
461 }
462 }
463 }
464
465 private function setRowHeight(int $whichRow, float $defaultHeight): void
466 {
467 $rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow);
468 if ($rowDimension !== null) {
469 $rowDimension->setRowHeight($defaultHeight);
470 }
471 }
472
473 private function setRowInvisible(int $whichRow): void
474 {
475 $rowDimension = $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow);
476 if ($rowDimension !== null) {
477 $rowDimension->setVisible(false);
478 }
479 }
480
481 private function processRowLoop(int $whichRow, int $maxRow, ?SimpleXMLElement $rowOverride, float $defaultHeight): int
482 {
483 $rowOverride = self::testSimpleXml($rowOverride);
484 $rowAttributes = self::testSimpleXml($rowOverride->attributes());
485 $row = $rowAttributes['No'];
486 $rowHeight = (float) $rowAttributes['Unit'];
487 $hidden = (isset($rowAttributes['Hidden'])) && ((string) $rowAttributes['Hidden'] == '1');
488 $rowCount = (int) ($rowAttributes['Count'] ?? 1);
489 while ($whichRow < $row) {
490 ++$whichRow;
491 $this->setRowHeight($whichRow, $defaultHeight);
492 }
493 while (($whichRow < ($row + $rowCount)) && ($whichRow < $maxRow)) {
494 ++$whichRow;
495 $this->setRowHeight($whichRow, $rowHeight);
496 if ($hidden) {
497 $this->setRowInvisible($whichRow);
498 }
499 }
500
501 return $whichRow;
502 }
503
504 private function processRowHeights(?SimpleXMLElement $sheet, int $maxRow): void
505 {
506 if ((!$this->readDataOnly) && $sheet !== null && (isset($sheet->Rows))) {
507 // Row Heights
508 $defaultHeight = 0;
509 $rowAttributes = $sheet->Rows->attributes();
510 if ($rowAttributes !== null) {
511 $defaultHeight = (float) $rowAttributes['DefaultSizePts'];
512 }
513 $whichRow = 0;
514
515 foreach ($sheet->Rows->RowInfo as $rowOverride) {
516 $whichRow = $this->processRowLoop($whichRow, $maxRow, $rowOverride, $defaultHeight);
517 }
518 // never executed, I can't figure out any circumstances
519 // under which it would be executed, and, even if
520 // such exist, I'm not convinced this is needed.
521 //while ($whichRow < $maxRow) {
522 // ++$whichRow;
523 // $this->spreadsheet->getActiveSheet()->getRowDimension($whichRow)->setRowHeight($defaultHeight);
524 //}
525 }
526 }
527
528 private function processDefinedNames(?SimpleXMLElement $gnmXML): void
529 {
530 // Loop through definedNames (global named ranges)
531 if ($gnmXML !== null && isset($gnmXML->Names)) {
532 foreach ($gnmXML->Names->Name as $definedName) {
533 $name = (string) $definedName->name;
534 $value = (string) $definedName->value;
535 if (stripos($value, '#REF!') !== false) {
536 continue;
537 }
538
539 [$worksheetName] = Worksheet::extractSheetTitle($value, true);
540 $worksheetName = trim($worksheetName, "'");
541 $worksheet = $this->spreadsheet->getSheetByName($worksheetName);
542 // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet
543 if ($worksheet !== null) {
544 $this->spreadsheet->addDefinedName(DefinedName::createInstance($name, $worksheet, $value));
545 }
546 }
547 }
548 }
549
550 private function parseRichText(string $is): RichText
551 {
552 $value = new RichText();
553 $value->createText($is);
554
555 return $value;
556 }
557
558 private function loadCell(
559 SimpleXMLElement $cell,
560 string $worksheetName,
561 SimpleXMLElement $cellAttributes,
562 string $column,
563 int $row
564 ): void {
565 $ValueType = $cellAttributes->ValueType;
566 $ExprID = (string) $cellAttributes->ExprID;
567 $type = DataType::TYPE_FORMULA;
568 if ($ExprID > '') {
569 if (((string) $cell) > '') {
570 $this->expressions[$ExprID] = [
571 'column' => $cellAttributes->Col,
572 'row' => $cellAttributes->Row,
573 'formula' => (string) $cell,
574 ];
575 } else {
576 $expression = $this->expressions[$ExprID];
577
578 $cell = $this->referenceHelper->updateFormulaReferences(
579 $expression['formula'],
580 'A1',
581 $cellAttributes->Col - $expression['column'],
582 $cellAttributes->Row - $expression['row'],
583 $worksheetName
584 );
585 }
586 $type = DataType::TYPE_FORMULA;
587 } else {
588 $vtype = (string) $ValueType;
589 if (array_key_exists($vtype, self::$mappings['dataType'])) {
590 $type = self::$mappings['dataType'][$vtype];
591 }
592 if ($vtype === '20') { // Boolean
593 $cell = $cell == 'TRUE';
594 }
595 }
596
597 $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type);
598 if (isset($cellAttributes->ValueFormat)) {
599 $this->spreadsheet->getActiveSheet()->getCell($column . $row)
600 ->getStyle()->getNumberFormat()
601 ->setFormatCode((string) $cellAttributes->ValueFormat);
602 }
603 }
604 }
605