PluginProbe
TablePress – Tables in WordPress made easy / 3.3.1
TablePress – Tables in WordPress made easy v3.3.1
3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 2.4.4 All 44 releases
tablepress / libraries / simplexlsx.class.php

simplexlsx.class.php in TablePress – Tables in WordPress made easy 3.3.1, at libraries/simplexlsx.class.php

1,176 lines 28.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Excel 2007-2019/Office 365 Reader class
4 *
5 * Based on SimpleXLSX v1.1.16 by Sergey Shuchkin.
6 * @link https://github.com/shuchkin/simplexlsx/
7 *
8 * @package TablePress
9 * @subpackage Import
10 * @author Sergey Shuchkin, Tobias Bäthge
11 * @since 1.1.0
12 */
13
14 namespace Shuchkin;
15
16 use SimpleXMLElement;
17
18 /**
19 * PHP Excel 2007-2013/Office 365 Reader class
20 *
21 * @package TablePress
22 * @subpackage Import
23 * @author Sergey Shuchkin, Tobias Bäthge
24 * @since 1.1.0
25 */
26 class SimpleXLSX {
27 public static $CF = [ // Cell formats
28 0 => 'General',
29 1 => '0',
30 2 => '0.00',
31 3 => '#,##0',
32 4 => '#,##0.00',
33 9 => '0%',
34 10 => '0.00%',
35 11 => '0.00E+00',
36 12 => '# ?/?',
37 13 => '# ??/??',
38 14 => 'mm-dd-yy',
39 15 => 'd-mmm-yy',
40 16 => 'd-mmm',
41 17 => 'mmm-yy',
42 18 => 'h:mm AM/PM',
43 19 => 'h:mm:ss AM/PM',
44 20 => 'h:mm',
45 21 => 'h:mm:ss',
46 22 => 'm/d/yy h:mm',
47
48 37 => '#,##0 ;(#,##0)',
49 38 => '#,##0 ;[Red](#,##0)',
50 39 => '#,##0.00;(#,##0.00)',
51 40 => '#,##0.00;[Red](#,##0.00)',
52
53 44 => '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)',
54 45 => 'mm:ss',
55 46 => '[h]:mm:ss',
56 47 => 'mmss.0',
57 48 => '##0.0E+0',
58 49 => '@',
59
60 27 => '[$-404]e/m/d',
61 30 => 'm/d/yy',
62 36 => '[$-404]e/m/d',
63 50 => '[$-404]e/m/d',
64 57 => '[$-404]e/m/d',
65
66 59 => 't0',
67 60 => 't0.00',
68 61 => 't#,##0',
69 62 => 't#,##0.00',
70 67 => 't0%',
71 68 => 't0.00%',
72 69 => 't# ?/?',
73 70 => 't# ??/??',
74 ];
75 public $nf = []; // number formats
76 public $cellFormats = []; // cellXfs
77 public $datetimeFormat = 'Y-m-d H:i:s';
78 public $debug;
79 public $activeSheet = 0;
80 public $rowsExReader;
81
82 /* @var SimpleXMLElement[] $sheets */
83 public $sheets;
84 public $sheetFiles = [];
85 public $sheetMetaData = [];
86 public $sheetRels = [];
87 // scheme
88 public $styles;
89 /* @var array[] $package */
90 public $package;
91 public $sharedstrings;
92 public $date1904 = 0;
93
94
95 /*
96 private $date_formats = array(
97 0xe => "d/m/Y",
98 0xf => "d-M-Y",
99 0x10 => "d-M",
100 0x11 => "M-Y",
101 0x12 => "h:i a",
102 0x13 => "h:i:s a",
103 0x14 => "H:i",
104 0x15 => "H:i:s",
105 0x16 => "d/m/Y H:i",
106 0x2d => "i:s",
107 0x2e => "H:i:s",
108 0x2f => "i:s.S"
109 );
110 private $number_formats = array(
111 0x1 => "%1.0f", // "0"
112 0x2 => "%1.2f", // "0.00",
113 0x3 => "%1.0f", //"#,##0",
114 0x4 => "%1.2f", //"#,##0.00",
115 0x5 => "%1.0f", //"$#,##0;($#,##0)",
116 0x6 => '$%1.0f', //"$#,##0;($#,##0)",
117 0x7 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
118 0x8 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
119 0x9 => '%1.0f%%', //"0%"
120 0xa => '%1.2f%%', //"0.00%"
121 0xb => '%1.2f', //"0.00E00",
122 0x25 => '%1.0f', //"#,##0;(#,##0)",
123 0x26 => '%1.0f', //"#,##0;(#,##0)",
124 0x27 => '%1.2f', //"#,##0.00;(#,##0.00)",
125 0x28 => '%1.2f', //"#,##0.00;(#,##0.00)",
126 0x29 => '%1.0f', //"#,##0;(#,##0)",
127 0x2a => '$%1.0f', //"$#,##0;($#,##0)",
128 0x2b => '%1.2f', //"#,##0.00;(#,##0.00)",
129 0x2c => '$%1.2f', //"$#,##0.00;($#,##0.00)",
130 0x30 => '%1.0f'); //"##0.0E0";
131 // }}}
132 */
133 public $errno = 0;
134 public $error = false;
135 /**
136 * @var false|SimpleXMLElement
137 */
138 public $theme;
139
140
141 public function __construct($filename = null, $is_data = null, $debug = null)
142 {
143 if ($debug !== null) {
144 $this->debug = $debug;
145 }
146 $this->package = [
147 'filename' => '',
148 'mtime' => 0,
149 'size' => 0,
150 'comment' => '',
151 'entries' => []
152 ];
153 if ($filename && $this->unzip($filename, $is_data)) {
154 $this->parseEntries();
155 }
156 }
157
158 public function unzip($filename, $is_data = false)
159 {
160
161 if ($is_data) {
162 $this->package['filename'] = 'default.xlsx';
163 $this->package['mtime'] = time();
164 $this->package['size'] = self::strlen($filename);
165
166 $vZ = $filename;
167 } else {
168 if (!is_readable($filename)) {
169 $this->error(1, 'File not found ' . $filename);
170
171 return false;
172 }
173
174 // Package information
175 $this->package['filename'] = $filename;
176 $this->package['mtime'] = filemtime($filename);
177 $this->package['size'] = filesize($filename);
178
179 // Read file
180 $vZ = file_get_contents($filename);
181 }
182 // Cut end of central directory
183 /* $aE = explode("\x50\x4b\x05\x06", $vZ);
184
185 if (count($aE) == 1) {
186 $this->error('Unknown format');
187 return false;
188 }
189 */
190 // Explode to each part
191 $aE = explode("\x50\x4b\x03\x04", $vZ);
192 array_shift($aE);
193
194 $aEL = count($aE);
195 if ($aEL === 0) {
196 $this->error(2, 'Unknown archive format');
197
198 return false;
199 }
200 // Search central directory end record
201 $last = $aE[$aEL - 1];
202 $last = explode("\x50\x4b\x05\x06", $last);
203 if (count($last) !== 2) {
204 $this->error(2, 'Unknown archive format');
205
206 return false;
207 }
208 // Search central directory
209 $last = explode("\x50\x4b\x01\x02", $last[0]);
210 if (count($last) < 2) {
211 $this->error(2, 'Unknown archive format');
212
213 return false;
214 }
215 $aE[$aEL - 1] = $last[0];
216
217 // Loop through the entries
218 foreach ($aE as $vZ) {
219 $aI = [];
220 $aI['E'] = 0;
221 $aI['EM'] = '';
222 // Retrieving local file header information
223 // $aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL', $vZ);
224 $aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL/v1EFL', $vZ);
225
226 // Check if data is encrypted
227 // $bE = ($aP['GPF'] && 0x0001) ? TRUE : FALSE;
228 // $bE = false;
229 $nF = $aP['FNL'];
230 $mF = $aP['EFL'];
231
232 // Special case : value block after the compressed data
233 if ($aP['GPF'] & 0x0008) {
234 $aP1 = unpack('V1CRC/V1CS/V1UCS', self::substr($vZ, -12));
235
236 $aP['CRC'] = $aP1['CRC'];
237 $aP['CS'] = $aP1['CS'];
238 $aP['UCS'] = $aP1['UCS'];
239 // 2013-08-10
240 $vZ = self::substr($vZ, 0, -12);
241 if (self::substr($vZ, -4) === "\x50\x4b\x07\x08") {
242 $vZ = self::substr($vZ, 0, -4);
243 }
244 }
245
246 // Getting stored filename
247 $aI['N'] = self::substr($vZ, 26, $nF);
248 $aI['N'] = str_replace('\\', '/', $aI['N']);
249
250 if (self::substr($aI['N'], -1) === '/') {
251 // is a directory entry - will be skipped
252 continue;
253 }
254
255 // Truncate full filename in path and filename
256 $aI['P'] = dirname($aI['N']);
257 $aI['P'] = ($aI['P'] === '.') ? '' : $aI['P'];
258 $aI['N'] = basename($aI['N']);
259
260 $vZ = self::substr($vZ, 26 + $nF + $mF);
261
262 if ($aP['CS'] > 0 && (self::strlen($vZ) !== (int)$aP['CS'])) { // check only if availabled
263 $aI['E'] = 1;
264 $aI['EM'] = 'Compressed size is not equal with the value in header information.';
265 }
266 // } elseif ( $bE ) {
267 // $aI['E'] = 5;
268 // $aI['EM'] = 'File is encrypted, which is not supported from this class.';
269 /* } else {
270 switch ($aP['CM']) {
271 case 0: // Stored
272 // Here is nothing to do, the file ist flat.
273 break;
274 case 8: // Deflated
275 $vZ = gzinflate($vZ);
276 break;
277 case 12: // BZIP2
278 if (extension_loaded('bz2')) {
279 $vZ = bzdecompress($vZ);
280 } else {
281 $aI['E'] = 7;
282 $aI['EM'] = 'PHP BZIP2 extension not available.';
283 }
284 break;
285 default:
286 $aI['E'] = 6;
287 $aI['EM'] = "De-/Compression method {$aP['CM']} is not supported.";
288 }
289 if (!$aI['E']) {
290 if ($vZ === false) {
291 $aI['E'] = 2;
292 $aI['EM'] = 'Decompression of data failed.';
293 } elseif ($this->_strlen($vZ) !== (int)$aP['UCS']) {
294 $aI['E'] = 3;
295 $aI['EM'] = 'Uncompressed size is not equal with the value in header information.';
296 } elseif (crc32($vZ) !== $aP['CRC']) {
297 $aI['E'] = 4;
298 $aI['EM'] = 'CRC32 checksum is not equal with the value in header information.';
299 }
300 }
301 }
302 */
303
304 // DOS to UNIX timestamp
305 $aI['T'] = mktime(
306 ($aP['FT'] & 0xf800) >> 11,
307 ($aP['FT'] & 0x07e0) >> 5,
308 ($aP['FT'] & 0x001f) << 1,
309 ($aP['FD'] & 0x01e0) >> 5,
310 $aP['FD'] & 0x001f,
311 (($aP['FD'] & 0xfe00) >> 9) + 1980
312 );
313
314 $this->package['entries'][] = [
315 'data' => $vZ,
316 'ucs' => (int)$aP['UCS'], // ucompresses size
317 'cm' => $aP['CM'], // compressed method
318 'cs' => isset($aP['CS']) ? (int) $aP['CS'] : 0, // compresses size
319 'crc' => $aP['CRC'],
320 'error' => $aI['E'],
321 'error_msg' => $aI['EM'],
322 'name' => $aI['N'],
323 'path' => $aI['P'],
324 'time' => $aI['T']
325 ];
326 } // end for each entries
327
328 return true;
329 }
330
331
332 public function error($num = null, $str = null)
333 {
334 if ($num) {
335 $this->errno = $num;
336 $this->error = $str;
337 if ($this->debug) {
338 trigger_error(__CLASS__ . ': ' . $this->error, E_USER_WARNING);
339 }
340 }
341
342 return $this->error;
343 }
344
345 public function parseEntries()
346 {
347 // Document data holders
348 $this->sharedstrings = [];
349 $this->sheets = [];
350 // $this->styles = array();
351 // $m1 = 0; // memory_get_peak_usage( true );
352 // Read relations and search for officeDocument
353 if ($relations = $this->getEntryXML('_rels/.rels')) {
354 foreach ($relations->Relationship as $rel) {
355 $rel_type = basename(trim((string)$rel['Type'])); // officeDocument
356 $rel_target = self::getTarget('', (string)$rel['Target']); // /xl/workbook.xml or xl/workbook.xml
357
358 if ($rel_type === 'officeDocument'
359 && $workbook = $this->getEntryXML($rel_target)
360 ) {
361 $index_rId = []; // [0 => rId1]
362
363 $index = 0;
364 foreach ($workbook->sheets->sheet as $s) {
365 $a = [];
366 foreach ($s->attributes() as $k => $v) {
367 $a[(string)$k] = (string)$v;
368 }
369 $this->sheetMetaData[$index] = $a;
370 $index_rId[$index] = (string)$s['id'];
371 $index++;
372 }
373 if ((int)$workbook->workbookPr['date1904'] === 1) {
374 $this->date1904 = 1;
375 }
376
377
378 if ($workbookRelations = $this->getEntryXML(dirname($rel_target) . '/_rels/workbook.xml.rels')) {
379 // Loop relations for workbook and extract sheets...
380 foreach ($workbookRelations->Relationship as $workbookRelation) {
381 $wrel_type = basename(trim((string)$workbookRelation['Type'])); // worksheet
382 $wrel_target = self::getTarget(dirname($rel_target), (string)$workbookRelation['Target']);
383 if (!$this->entryExists($wrel_target)) {
384 continue;
385 }
386
387 if ($wrel_type === 'worksheet') { // Sheets
388 if ($sheet = $this->getEntryXML($wrel_target)) {
389 $index = array_search((string)$workbookRelation['Id'], $index_rId, true);
390 $this->sheets[$index] = $sheet;
391 $this->sheetFiles[$index] = $wrel_target;
392 $srel_d = dirname($wrel_target);
393 $srel_f = basename($wrel_target);
394 $srel_file = $srel_d . '/_rels/' . $srel_f . '.rels';
395 if ($this->entryExists($srel_file)) {
396 $this->sheetRels[$index] = $this->getEntryXML($srel_file);
397 }
398 }
399 } elseif ($wrel_type === 'sharedStrings') {
400 if ($sharedStrings = $this->getEntryXML($wrel_target)) {
401 foreach ($sharedStrings->si as $val) {
402 if (isset($val->t)) {
403 $this->sharedstrings[] = (string)$val->t;
404 } elseif (isset($val->r)) {
405 $this->sharedstrings[] = self::parseRichText($val);
406 }
407 }
408 }
409 } elseif ($wrel_type === 'styles') {
410 $this->styles = $this->getEntryXML($wrel_target);
411
412 // number formats
413 $this->nf = [];
414 if (isset($this->styles->numFmts->numFmt)) {
415 foreach ($this->styles->numFmts->numFmt as $v) {
416 $this->nf[(int)$v['numFmtId']] = (string)$v['formatCode'];
417 }
418 }
419
420 $this->cellFormats = [];
421 if (isset($this->styles->cellXfs->xf)) {
422 foreach ($this->styles->cellXfs->xf as $v) {
423 $x = [
424 'format' => null
425 ];
426 foreach ($v->attributes() as $k1 => $v1) {
427 $x[ $k1 ] = (int) $v1;
428 }
429 if (isset($x['numFmtId'])) {
430 if (isset($this->nf[$x['numFmtId']])) {
431 $x['format'] = $this->nf[$x['numFmtId']];
432 } elseif (isset(self::$CF[$x['numFmtId']])) {
433 $x['format'] = self::$CF[$x['numFmtId']];
434 }
435 }
436
437 $this->cellFormats[] = $x;
438 }
439 }
440 } elseif ($wrel_type === 'theme') {
441 $this->theme = $this->getEntryXML($wrel_target);
442 }
443 }
444
445 // break;
446 }
447 // reptile hack :: find active sheet from workbook.xml
448 if ($workbook->bookViews->workbookView) {
449 foreach ($workbook->bookViews->workbookView as $v) {
450 if (!empty($v['activeTab'])) {
451 $this->activeSheet = (int)$v['activeTab'];
452 }
453 }
454 }
455
456 break;
457 }
458 }
459 }
460
461 // $m2 = memory_get_peak_usage(true);
462 // echo __FUNCTION__.' M='.round( ($m2-$m1) / 1048576, 2).'MB'.PHP_EOL;
463
464 if (count($this->sheets)) {
465 // Sort sheets
466 ksort($this->sheets);
467
468 return true;
469 }
470
471 return false;
472 }
473
474 public function getEntryXML($name)
475 {
476 if ($entry_xml = $this->getEntryData($name)) {
477 $this->deleteEntry($name); // economy memory
478 // dirty remove namespace prefixes and empty rows
479 $entry_xml = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $entry_xml); // remove namespaces
480 $entry_xml .= ' '; // force run garbage collector
481 // remove namespaced attrs
482 $entry_xml = preg_replace('/[a-zA-Z0-9]+:([a-zA-Z0-9]+="[^"]+")/', '$1', $entry_xml);
483 $entry_xml .= ' ';
484 $entry_xml = preg_replace('/<[a-zA-Z0-9]+:([^>]+)>/', '<$1>', $entry_xml); // fix namespaced openned tags
485 $entry_xml .= ' ';
486 $entry_xml = preg_replace('/<\/[a-zA-Z0-9]+:([^>]+)>/', '</$1>', $entry_xml); // fix namespaced closed tags
487 $entry_xml .= ' ';
488
489 if (strpos($name, '/sheet')) { // dirty skip empty rows
490 // remove <row...> <c /><c /></row>
491 $cnt = $cnt2 = $cnt3 = null;
492 $entry_xml = preg_replace('/<row[^>]+>\s*(<c[^\/]+\/>\s*)+<\/row>/', '', $entry_xml, -1, $cnt);
493 $entry_xml .= ' ';
494 // remove <row />
495 $entry_xml = preg_replace('/<row[^\/>]*\/>/', '', $entry_xml, -1, $cnt2);
496 $entry_xml .= ' ';
497 // remove <row...></row>
498 $entry_xml = preg_replace('/<row[^>]*><\/row>/', '', $entry_xml, -1, $cnt3);
499 $entry_xml .= ' ';
500 if ($cnt || $cnt2 || $cnt3) {
501 $entry_xml = preg_replace('/<dimension[^\/]+\/>/', '', $entry_xml);
502 $entry_xml .= ' ';
503 }
504 // file_put_contents( basename( $name ), $entry_xml ); // @to do comment!!!
505 }
506 $entry_xml = trim($entry_xml);
507
508 // $m1 = memory_get_usage();
509 // XML External Entity (XXE) Prevention, libxml_disable_entity_loader deprecated in PHP 8
510 if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) {
511 $_old = libxml_disable_entity_loader();
512 }
513
514 $_old_uie = libxml_use_internal_errors(true);
515
516 $entry_xmlobj = simplexml_load_string($entry_xml, 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_PARSEHUGE);
517
518 libxml_use_internal_errors($_old_uie);
519
520 if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) {
521 /** @noinspection PhpUndefinedVariableInspection */
522 libxml_disable_entity_loader($_old);
523 }
524
525 // $m2 = memory_get_usage();
526 // echo round( ($m2-$m1) / (1024 * 1024), 2).' MB'.PHP_EOL;
527
528 if ($entry_xmlobj) {
529 return $entry_xmlobj;
530 }
531 $e = libxml_get_last_error();
532 if ($e) {
533 $this->error(3, 'XML-entry ' . $name . ' parser error ' . $e->message . ' line ' . $e->line);
534 }
535 } else {
536 $this->error(4, 'XML-entry not found ' . $name);
537 }
538
539 return false;
540 }
541
542 // sheets numeration: 1,2,3....
543
544 public function getEntryData($name)
545 {
546 $name = ltrim(str_replace('\\', '/', $name), '/');
547 $dir = self::strtoupper(dirname($name));
548 $name = self::strtoupper(basename($name));
549 foreach ($this->package['entries'] as &$entry) {
550 if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
551 if ($entry['error']) {
552 return false;
553 }
554 switch ($entry['cm']) {
555 case -1:
556 case 0: // Stored
557 // Here is nothing to do, the file ist flat.
558 break;
559 case 8: // Deflated
560 $entry['data'] = gzinflate($entry['data']);
561 break;
562 case 12: // BZIP2
563 if (extension_loaded('bz2')) {
564 $entry['data'] = bzdecompress($entry['data']);
565 } else {
566 $entry['error'] = 7;
567 $entry['error_message'] = 'PHP BZIP2 extension not available.';
568 }
569 break;
570 default:
571 $entry['error'] = 6;
572 $entry['error_msg'] = 'De-/Compression method '.$entry['cm'].' is not supported.';
573 }
574 if (!$entry['error'] && $entry['cm'] > -1) {
575 $entry['cm'] = -1;
576 if ($entry['data'] === false) {
577 $entry['error'] = 2;
578 $entry['error_msg'] = 'Decompression of data failed.';
579 } elseif ($entry['ucs'] > 0 && (self::strlen($entry['data']) !== (int)$entry['ucs'])) {
580 $entry['error'] = 3;
581 $entry['error_msg'] = 'Uncompressed size is not equal with the value in header information.';
582 } elseif (crc32($entry['data']) !== $entry['crc']) {
583 $entry['error'] = 4;
584 $entry['error_msg'] = 'CRC32 checksum is not equal with the value in header information.';
585 }
586 }
587
588 return $entry['data'];
589 }
590 }
591 unset($entry);
592 $this->error(5, 'Entry not found ' . ($dir ? $dir . '/' : '') . $name);
593
594 return false;
595 }
596 public function deleteEntry($name)
597 {
598 $name = ltrim(str_replace('\\', '/', $name), '/');
599 $dir = self::strtoupper(dirname($name));
600 $name = self::strtoupper(basename($name));
601 foreach ($this->package['entries'] as $k => $entry) {
602 if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
603 unset($this->package['entries'][$k]);
604 return true;
605 }
606 }
607 return false;
608 }
609
610 public static function strtoupper($str)
611 {
612 return (ini_get('mbstring.func_overload') & 2) ? mb_strtoupper($str, '8bit') : strtoupper($str);
613 }
614
615 /*
616 * @param string $name Filename in archive
617 * @return SimpleXMLElement|bool
618 */
619
620 public function entryExists($name)
621 {
622 // 0.6.6
623 $dir = self::strtoupper(dirname($name));
624 $name = self::strtoupper(basename($name));
625 foreach ($this->package['entries'] as $entry) {
626 if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
627 return true;
628 }
629 }
630
631 return false;
632 }
633
634 public static function parseFile($filename, $debug = false)
635 {
636 return self::parse($filename, false, $debug);
637 }
638
639 public static function parse($filename, $is_data = false, $debug = false)
640 {
641 $xlsx = new self();
642 $xlsx->debug = $debug;
643 if ($xlsx->unzip($filename, $is_data)) {
644 $xlsx->parseEntries();
645 }
646 if ($xlsx->success()) {
647 return $xlsx;
648 }
649 self::parseError($xlsx->error());
650 self::parseErrno($xlsx->errno());
651
652 return false;
653 }
654
655 public function success()
656 {
657 return !$this->error;
658 }
659
660 // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
661
662 public static function parseError($set = false)
663 {
664 static $error = false;
665
666 return $set ? $error = $set : $error;
667 }
668
669 public static function parseErrno($set = false)
670 {
671 static $errno = false;
672
673 return $set ? $errno = $set : $errno;
674 }
675
676 public function errno()
677 {
678 return $this->errno;
679 }
680
681 public static function parseData($data, $debug = false)
682 {
683 return self::parse($data, true, $debug);
684 }
685
686
687
688 public function worksheet($worksheetIndex = 0)
689 {
690 if (isset($this->sheets[$worksheetIndex])) {
691 return $this->sheets[$worksheetIndex];
692 }
693 $this->error(6, 'Worksheet not found ' . $worksheetIndex);
694
695 return false;
696 }
697
698 /**
699 * returns [numCols,numRows] of worksheet
700 *
701 * @param int $worksheetIndex
702 *
703 * @return array
704 */
705 public function dimension($worksheetIndex = 0)
706 {
707
708 if (($ws = $this->worksheet($worksheetIndex)) === false) {
709 return [0, 0];
710 }
711 /* @var SimpleXMLElement $ws */
712
713 $ref = (string)$ws->dimension['ref'];
714
715 if (self::strpos($ref, ':') !== false) {
716 $d = explode(':', $ref);
717 $idx = $this->getIndex($d[1]);
718
719 return [$idx[0] + 1, $idx[1] + 1];
720 }
721 /*
722 if ( $ref !== '' ) { // 0.6.8
723 $index = $this->getIndex( $ref );
724
725 return [ $index[0] + 1, $index[1] + 1 ];
726 }
727 */
728
729 // slow method
730 $maxC = $maxR = 0;
731 $iR = -1;
732 foreach ($ws->sheetData->row as $row) {
733 $iR++;
734 $iC = -1;
735 foreach ($row->c as $c) {
736 $iC++;
737 $idx = $this->getIndex((string)$c['r']);
738 $x = $idx[0];
739 $y = $idx[1];
740 if ($x > -1) {
741 if ($x > $maxC) {
742 $maxC = $x;
743 }
744 if ($y > $maxR) {
745 $maxR = $y;
746 }
747 } else {
748 if ($iC > $maxC) {
749 $maxC = $iC;
750 }
751 if ($iR > $maxR) {
752 $maxR = $iR;
753 }
754 }
755 }
756 }
757
758 return [$maxC + 1, $maxR + 1];
759 }
760
761 public function getIndex($cell = 'A1')
762 {
763 $m = null;
764
765 if (preg_match('/([A-Z]+)(\d+)/', $cell, $m)) {
766 $col = $m[1];
767 $row = $m[2];
768
769 $colLen = self::strlen($col);
770 $index = 0;
771
772 for ($i = $colLen - 1; $i >= 0; $i--) {
773 $index += (ord($col[$i]) - 64) * pow(26, $colLen - $i - 1);
774 }
775
776 return [$index - 1, $row - 1];
777 }
778
779 // $this->error( 'Invalid cell index ' . $cell );
780
781 return [-1, -1];
782 }
783
784 public function value($cell)
785 {
786 // Determine data type
787 $dataType = (string)$cell['t'];
788
789 if ($dataType === '' || $dataType === 'n') { // number
790 $s = (int)$cell['s'];
791 if ($s > 0 && isset($this->cellFormats[$s])) {
792 if (array_key_exists('format', $this->cellFormats[$s])) {
793 $format = $this->cellFormats[$s]['format'];
794 if ($format && preg_match('/[mM]/', preg_replace('/\"[^"]+\"/', '', $format))) { // [mm]onth,AM|PM
795 $dataType = 'D';
796 }
797 } else {
798 $dataType = 'n';
799 }
800 }
801 }
802
803 $value = '';
804
805 switch ($dataType) {
806 case 's':
807 // Value is a shared string
808 if ((string)$cell->v !== '') {
809 $value = $this->sharedstrings[(int)$cell->v];
810 }
811 break;
812
813 case 'str': // formula?
814 if ((string)$cell->v !== '') {
815 $value = (string)$cell->v;
816 }
817 break;
818
819 case 'b':
820 // Value is boolean
821 $value = self::boolean((string)$cell->v);
822
823 break;
824
825 case 'inlineStr':
826 // Value is rich text inline
827 $value = self::parseRichText($cell->is);
828
829 break;
830
831 case 'e':
832 // Value is an error message
833 if ((string)$cell->v !== '') {
834 $value = (string)$cell->v;
835 }
836 break;
837
838 case 'D':
839 // Date as float
840 if (!empty($cell->v)) {
841 $value = $this->datetimeFormat ?
842 gmdate($this->datetimeFormat, $this->unixstamp((float)$cell->v)) : (float)$cell->v;
843 }
844 break;
845
846 case 'd':
847 // Date as ISO YYYY-MM-DD
848 if ((string)$cell->v !== '') {
849 $value = (string)$cell->v;
850 }
851 break;
852
853 default:
854 // Value is a string
855 $value = (string)$cell->v;
856
857 // Check for numeric values
858 if (is_numeric($value)) {
859 /** @noinspection TypeUnsafeComparisonInspection */
860 if ($value == (int)$value) {
861 $value = (int)$value;
862 } /** @noinspection TypeUnsafeComparisonInspection */ elseif ($value == (float)$value) {
863 $value = (float)$value;
864 }
865 }
866 }
867
868 return $value;
869 }
870
871 public function unixstamp($excelDateTime)
872 {
873
874 $d = floor($excelDateTime); // days since 1900 or 1904
875 $t = $excelDateTime - $d;
876
877 if ($this->date1904) {
878 $d += 1462;
879 }
880
881 $t = (abs($d) > 0) ? ($d - 25569) * 86400 + round($t * 86400) : round($t * 86400);
882
883 return (int)$t;
884 }
885
886 public function toHTML($worksheetIndex = 0)
887 {
888 $s = '<table class=excel>';
889 foreach ($this->readRows($worksheetIndex) as $r) {
890 $s .= '<tr>';
891 foreach ($r as $c) {
892 $s .= '<td nowrap>' . ($c === '' ? '&nbsp' : htmlspecialchars($c, ENT_QUOTES)) . '</td>';
893 }
894 $s .= "</tr>\r\n";
895 }
896 $s .= '</table>';
897
898 return $s;
899 }
900 public function toHTMLEx($worksheetIndex = 0)
901 {
902 $s = '<table class=excel>';
903 $y = 0;
904 foreach ($this->readRowsEx($worksheetIndex) as $r) {
905 $s .= '<tr>';
906 $x = 0;
907 foreach ($r as $c) {
908 $tag = 'td';
909 $css = $c['css'];
910 if ($y === 0) {
911 $tag = 'th';
912 $css .= $c['width'] ? 'width: '.round($c['width'] * 0.47, 2).'em;' : '';
913 }
914
915 if ($x === 0 && $c['height']) {
916 $css .= 'height: '.round($c['height'] * 1.3333).'px;';
917 }
918 $s .= '<'.$tag.' style="'.$css.'" nowrap>'
919 . ($c['value'] === '' ? '&nbsp' : htmlspecialchars($c['value'], ENT_QUOTES)) . '</'.$tag.'>';
920 $x++;
921 }
922 $s .= "</tr>\r\n";
923 $y++;
924 }
925 $s .= '</table>';
926
927 return $s;
928 }
929 public function rows($worksheetIndex = 0, $limit = 0)
930 {
931 return iterator_to_array($this->readRows($worksheetIndex, $limit), false);
932 }
933 // thx Gonzo
934 /**
935 * @param $worksheetIndex
936 * @param $limit
937 * @return \Generator
938 */
939 public function readRows($worksheetIndex = 0, $limit = 0)
940 {
941
942 if (($ws = $this->worksheet($worksheetIndex)) === false) {
943 return;
944 }
945 $dim = $this->dimension($worksheetIndex);
946 $numCols = $dim[0];
947 $numRows = $dim[1];
948
949 $emptyRow = [];
950 for ($i = 0; $i < $numCols; $i++) {
951 $emptyRow[] = '';
952 }
953
954 $curR = 0;
955 $_limit = $limit;
956 /* @var SimpleXMLElement $ws */
957 foreach ($ws->sheetData->row as $row) {
958 $r = $emptyRow;
959 $curC = 0;
960 foreach ($row->c as $c) {
961 // detect skipped cols
962 $idx = $this->getIndex((string)$c['r']);
963 $x = $idx[0];
964 $y = $idx[1];
965
966 if ($x > -1) {
967 $curC = $x;
968 while ($curR < $y) {
969 yield $emptyRow;
970 $curR++;
971 $_limit--;
972 if ($_limit === 0) {
973 return;
974 }
975 }
976 }
977 $r[$curC] = $this->value($c);
978 $curC++;
979 }
980 yield $r;
981
982 $curR++;
983 $_limit--;
984 if ($_limit === 0) {
985 return;
986 }
987 }
988 while ($curR < $numRows) {
989 yield $emptyRow;
990 $curR++;
991 $_limit--;
992 if ($_limit === 0) {
993 return;
994 }
995 }
996 }
997
998 public function rowsEx($worksheetIndex = 0, $limit = 0)
999 {
1000 return iterator_to_array($this->readRowsEx($worksheetIndex, $limit), false);
1001 }
1002 // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
1003 /**
1004 * @param $worksheetIndex
1005 * @param $limit
1006 * @return \Generator|null
1007 */
1008 public function readRowsEx($worksheetIndex = 0, $limit = 0)
1009 {
1010 if (!$this->rowsExReader) {
1011 require_once __DIR__ . '/SimpleXLSXEx.php';
1012 $this->rowsExReader = new SimpleXLSXEx($this);
1013 }
1014 return $this->rowsExReader->readRowsEx($worksheetIndex, $limit);
1015 }
1016
1017 /**
1018 * Returns cell value
1019 * VERY SLOW! Use ->rows() or ->rowsEx()
1020 *
1021 * @param int $worksheetIndex
1022 * @param string|array $cell ref or coords, D12 or [3,12]
1023 *
1024 * @return mixed Returns NULL if not found
1025 */
1026 public function getCell($worksheetIndex = 0, $cell = 'A1')
1027 {
1028
1029 if (($ws = $this->worksheet($worksheetIndex)) === false) {
1030 return false;
1031 }
1032 if (is_array($cell)) {
1033 $cell = self::num2name($cell[0]) . $cell[1];// [3,21] -> D21
1034 }
1035 if (is_string($cell)) {
1036 $result = $ws->sheetData->xpath("row/c[@r='" . $cell . "']");
1037 if (count($result)) {
1038 return $this->value($result[0]);
1039 }
1040 }
1041
1042 return null;
1043 }
1044
1045 public function getSheets()
1046 {
1047 return $this->sheets;
1048 }
1049
1050 public function sheetsCount()
1051 {
1052 return count($this->sheets);
1053 }
1054
1055 public function sheetName($worksheetIndex)
1056 {
1057 $sn = $this->sheetNames();
1058 if (isset($sn[$worksheetIndex])) {
1059 return $sn[$worksheetIndex];
1060 }
1061
1062 return false;
1063 }
1064
1065 public function sheetNames()
1066 {
1067 $a = [];
1068 foreach ($this->sheetMetaData as $k => $v) {
1069 $a[$k] = $v['name'];
1070 }
1071 return $a;
1072 }
1073 public function sheetMeta($worksheetIndex = null)
1074 {
1075 if ($worksheetIndex === null) {
1076 return $this->sheetMetaData;
1077 }
1078 return isset($this->sheetMetaData[$worksheetIndex]) ? $this->sheetMetaData[$worksheetIndex] : false;
1079 }
1080 public function isHiddenSheet($worksheetIndex)
1081 {
1082 return isset($this->sheetMetaData[$worksheetIndex]['state'])
1083 && $this->sheetMetaData[$worksheetIndex]['state'] === 'hidden';
1084 }
1085
1086 public function getStyles()
1087 {
1088 return $this->styles;
1089 }
1090
1091 public function getPackage()
1092 {
1093 return $this->package;
1094 }
1095
1096 public function setDateTimeFormat($value)
1097 {
1098 $this->datetimeFormat = is_string($value) ? $value : false;
1099 }
1100
1101 public static function getTarget($base, $target)
1102 {
1103 $target = trim($target);
1104 if (strpos($target, '/') === 0) {
1105 return self::substr($target, 1);
1106 }
1107 $target = ($base ? $base . '/' : '') . $target;
1108 // a/b/../c -> a/c
1109 $parts = explode('/', $target);
1110 $abs = [];
1111 foreach ($parts as $p) {
1112 if ('.' === $p) {
1113 continue;
1114 }
1115 if ('..' === $p) {
1116 array_pop($abs);
1117 } else {
1118 $abs[] = $p;
1119 }
1120 }
1121 return implode('/', $abs);
1122 }
1123
1124 public static function parseRichText($is = null)
1125 {
1126 $value = [];
1127
1128 if (isset($is->t)) {
1129 $value[] = (string)$is->t;
1130 } elseif (isset($is->r)) {
1131 foreach ($is->r as $run) {
1132 $value[] = (string)$run->t;
1133 }
1134 }
1135
1136 return implode('', $value);
1137 }
1138
1139 public static function num2name($num)
1140 {
1141 $numeric = ($num - 1) % 26;
1142 $letter = chr(65 + $numeric);
1143 $num2 = (int)(($num - 1) / 26);
1144 if ($num2 > 0) {
1145 return self::num2name($num2) . $letter;
1146 }
1147 return $letter;
1148 }
1149
1150 public static function strlen($str)
1151 {
1152 return (ini_get('mbstring.func_overload') & 2) ? mb_strlen($str, '8bit') : strlen($str);
1153 }
1154
1155 public static function substr($str, $start, $length = null)
1156 {
1157 return (ini_get('mbstring.func_overload') & 2) ?
1158 mb_substr($str, $start, ($length === null) ? mb_strlen($str, '8bit') : $length, '8bit')
1159 : substr($str, $start, ($length === null) ? strlen($str) : $length);
1160 }
1161
1162 public static function strpos($haystack, $needle, $offset = 0)
1163 {
1164 return (ini_get('mbstring.func_overload') & 2) ?
1165 mb_strpos($haystack, $needle, $offset, '8bit') : strpos($haystack, $needle, $offset);
1166 }
1167 public static function boolean($value)
1168 {
1169 if (is_numeric($value)) {
1170 return (bool) $value;
1171 }
1172
1173 return $value === 'true' || $value === 'TRUE';
1174 }
1175 }
1176