PluginProbe
TablePress – Tables in WordPress made easy / 3.2
TablePress – Tables in WordPress made easy v3.2
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.2, at libraries/simplexlsx.class.php

1,174 lines 28.4 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.14 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 $entry_xml = preg_replace('/<row[^>]+>\s*(<c[^\/]+\/>\s*)+<\/row>/', '', $entry_xml, -1, $cnt);
492 $entry_xml .= ' ';
493 // remove <row />
494 $entry_xml = preg_replace('/<row[^\/>]*\/>/', '', $entry_xml, -1, $cnt2);
495 $entry_xml .= ' ';
496 // remove <row...></row>
497 $entry_xml = preg_replace('/<row[^>]*><\/row>/', '', $entry_xml, -1, $cnt3);
498 $entry_xml .= ' ';
499 if ($cnt || $cnt2 || $cnt3) {
500 $entry_xml = preg_replace('/<dimension[^\/]+\/>/', '', $entry_xml);
501 $entry_xml .= ' ';
502 }
503 // file_put_contents( basename( $name ), $entry_xml ); // @to do comment!!!
504 }
505 $entry_xml = trim($entry_xml);
506
507 // $m1 = memory_get_usage();
508 // XML External Entity (XXE) Prevention, libxml_disable_entity_loader deprecated in PHP 8
509 if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) {
510 $_old = libxml_disable_entity_loader();
511 }
512
513 $_old_uie = libxml_use_internal_errors(true);
514
515 $entry_xmlobj = simplexml_load_string($entry_xml, 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_PARSEHUGE);
516
517 libxml_use_internal_errors($_old_uie);
518
519 if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) {
520 /** @noinspection PhpUndefinedVariableInspection */
521 libxml_disable_entity_loader($_old);
522 }
523
524 // $m2 = memory_get_usage();
525 // echo round( ($m2-$m1) / (1024 * 1024), 2).' MB'.PHP_EOL;
526
527 if ($entry_xmlobj) {
528 return $entry_xmlobj;
529 }
530 $e = libxml_get_last_error();
531 if ($e) {
532 $this->error(3, 'XML-entry ' . $name . ' parser error ' . $e->message . ' line ' . $e->line);
533 }
534 } else {
535 $this->error(4, 'XML-entry not found ' . $name);
536 }
537
538 return false;
539 }
540
541 // sheets numeration: 1,2,3....
542
543 public function getEntryData($name)
544 {
545 $name = ltrim(str_replace('\\', '/', $name), '/');
546 $dir = self::strtoupper(dirname($name));
547 $name = self::strtoupper(basename($name));
548 foreach ($this->package['entries'] as &$entry) {
549 if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
550 if ($entry['error']) {
551 return false;
552 }
553 switch ($entry['cm']) {
554 case -1:
555 case 0: // Stored
556 // Here is nothing to do, the file ist flat.
557 break;
558 case 8: // Deflated
559 $entry['data'] = gzinflate($entry['data']);
560 break;
561 case 12: // BZIP2
562 if (extension_loaded('bz2')) {
563 $entry['data'] = bzdecompress($entry['data']);
564 } else {
565 $entry['error'] = 7;
566 $entry['error_message'] = 'PHP BZIP2 extension not available.';
567 }
568 break;
569 default:
570 $entry['error'] = 6;
571 $entry['error_msg'] = 'De-/Compression method '.$entry['cm'].' is not supported.';
572 }
573 if (!$entry['error'] && $entry['cm'] > -1) {
574 $entry['cm'] = -1;
575 if ($entry['data'] === false) {
576 $entry['error'] = 2;
577 $entry['error_msg'] = 'Decompression of data failed.';
578 } elseif ($entry['ucs'] > 0 && (self::strlen($entry['data']) !== (int)$entry['ucs'])) {
579 $entry['error'] = 3;
580 $entry['error_msg'] = 'Uncompressed size is not equal with the value in header information.';
581 } elseif (crc32($entry['data']) !== $entry['crc']) {
582 $entry['error'] = 4;
583 $entry['error_msg'] = 'CRC32 checksum is not equal with the value in header information.';
584 }
585 }
586
587 return $entry['data'];
588 }
589 }
590 unset($entry);
591 $this->error(5, 'Entry not found ' . ($dir ? $dir . '/' : '') . $name);
592
593 return false;
594 }
595 public function deleteEntry($name)
596 {
597 $name = ltrim(str_replace('\\', '/', $name), '/');
598 $dir = self::strtoupper(dirname($name));
599 $name = self::strtoupper(basename($name));
600 foreach ($this->package['entries'] as $k => $entry) {
601 if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
602 unset($this->package['entries'][$k]);
603 return true;
604 }
605 }
606 return false;
607 }
608
609 public static function strtoupper($str)
610 {
611 return (ini_get('mbstring.func_overload') & 2) ? mb_strtoupper($str, '8bit') : strtoupper($str);
612 }
613
614 /*
615 * @param string $name Filename in archive
616 * @return SimpleXMLElement|bool
617 */
618
619 public function entryExists($name)
620 {
621 // 0.6.6
622 $dir = self::strtoupper(dirname($name));
623 $name = self::strtoupper(basename($name));
624 foreach ($this->package['entries'] as $entry) {
625 if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
626 return true;
627 }
628 }
629
630 return false;
631 }
632
633 public static function parseFile($filename, $debug = false)
634 {
635 return self::parse($filename, false, $debug);
636 }
637
638 public static function parse($filename, $is_data = false, $debug = false)
639 {
640 $xlsx = new self();
641 $xlsx->debug = $debug;
642 if ($xlsx->unzip($filename, $is_data)) {
643 $xlsx->parseEntries();
644 }
645 if ($xlsx->success()) {
646 return $xlsx;
647 }
648 self::parseError($xlsx->error());
649 self::parseErrno($xlsx->errno());
650
651 return false;
652 }
653
654 public function success()
655 {
656 return !$this->error;
657 }
658
659 // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
660
661 public static function parseError($set = false)
662 {
663 static $error = false;
664
665 return $set ? $error = $set : $error;
666 }
667
668 public static function parseErrno($set = false)
669 {
670 static $errno = false;
671
672 return $set ? $errno = $set : $errno;
673 }
674
675 public function errno()
676 {
677 return $this->errno;
678 }
679
680 public static function parseData($data, $debug = false)
681 {
682 return self::parse($data, true, $debug);
683 }
684
685
686
687 public function worksheet($worksheetIndex = 0)
688 {
689 if (isset($this->sheets[$worksheetIndex])) {
690 return $this->sheets[$worksheetIndex];
691 }
692 $this->error(6, 'Worksheet not found ' . $worksheetIndex);
693
694 return false;
695 }
696
697 /**
698 * returns [numCols,numRows] of worksheet
699 *
700 * @param int $worksheetIndex
701 *
702 * @return array
703 */
704 public function dimension($worksheetIndex = 0)
705 {
706
707 if (($ws = $this->worksheet($worksheetIndex)) === false) {
708 return [0, 0];
709 }
710 /* @var SimpleXMLElement $ws */
711
712 $ref = (string)$ws->dimension['ref'];
713
714 if (self::strpos($ref, ':') !== false) {
715 $d = explode(':', $ref);
716 $idx = $this->getIndex($d[1]);
717
718 return [$idx[0] + 1, $idx[1] + 1];
719 }
720 /*
721 if ( $ref !== '' ) { // 0.6.8
722 $index = $this->getIndex( $ref );
723
724 return [ $index[0] + 1, $index[1] + 1 ];
725 }
726 */
727
728 // slow method
729 $maxC = $maxR = 0;
730 $iR = -1;
731 foreach ($ws->sheetData->row as $row) {
732 $iR++;
733 $iC = -1;
734 foreach ($row->c as $c) {
735 $iC++;
736 $idx = $this->getIndex((string)$c['r']);
737 $x = $idx[0];
738 $y = $idx[1];
739 if ($x > -1) {
740 if ($x > $maxC) {
741 $maxC = $x;
742 }
743 if ($y > $maxR) {
744 $maxR = $y;
745 }
746 } else {
747 if ($iC > $maxC) {
748 $maxC = $iC;
749 }
750 if ($iR > $maxR) {
751 $maxR = $iR;
752 }
753 }
754 }
755 }
756
757 return [$maxC + 1, $maxR + 1];
758 }
759
760 public function getIndex($cell = 'A1')
761 {
762
763 if (preg_match('/([A-Z]+)(\d+)/', $cell, $m)) {
764 $col = $m[1];
765 $row = $m[2];
766
767 $colLen = self::strlen($col);
768 $index = 0;
769
770 for ($i = $colLen - 1; $i >= 0; $i--) {
771 $index += (ord($col[$i]) - 64) * pow(26, $colLen - $i - 1);
772 }
773
774 return [$index - 1, $row - 1];
775 }
776
777 // $this->error( 'Invalid cell index ' . $cell );
778
779 return [-1, -1];
780 }
781
782 public function value($cell)
783 {
784 // Determine data type
785 $dataType = (string)$cell['t'];
786
787 if ($dataType === '' || $dataType === 'n') { // number
788 $s = (int)$cell['s'];
789 if ($s > 0 && isset($this->cellFormats[$s])) {
790 if (array_key_exists('format', $this->cellFormats[$s])) {
791 $format = $this->cellFormats[$s]['format'];
792 if ($format && preg_match('/[mM]/', preg_replace('/\"[^"]+\"/', '', $format))) { // [mm]onth,AM|PM
793 $dataType = 'D';
794 }
795 } else {
796 $dataType = 'n';
797 }
798 }
799 }
800
801 $value = '';
802
803 switch ($dataType) {
804 case 's':
805 // Value is a shared string
806 if ((string)$cell->v !== '') {
807 $value = $this->sharedstrings[(int)$cell->v];
808 }
809 break;
810
811 case 'str': // formula?
812 if ((string)$cell->v !== '') {
813 $value = (string)$cell->v;
814 }
815 break;
816
817 case 'b':
818 // Value is boolean
819 $value = self::boolean((string)$cell->v);
820
821 break;
822
823 case 'inlineStr':
824 // Value is rich text inline
825 $value = self::parseRichText($cell->is);
826
827 break;
828
829 case 'e':
830 // Value is an error message
831 if ((string)$cell->v !== '') {
832 $value = (string)$cell->v;
833 }
834 break;
835
836 case 'D':
837 // Date as float
838 if (!empty($cell->v)) {
839 $value = $this->datetimeFormat ?
840 gmdate($this->datetimeFormat, $this->unixstamp((float)$cell->v)) : (float)$cell->v;
841 }
842 break;
843
844 case 'd':
845 // Date as ISO YYYY-MM-DD
846 if ((string)$cell->v !== '') {
847 $value = (string)$cell->v;
848 }
849 break;
850
851 default:
852 // Value is a string
853 $value = (string)$cell->v;
854
855 // Check for numeric values
856 if (is_numeric($value)) {
857 /** @noinspection TypeUnsafeComparisonInspection */
858 if ($value == (int)$value) {
859 $value = (int)$value;
860 } /** @noinspection TypeUnsafeComparisonInspection */ elseif ($value == (float)$value) {
861 $value = (float)$value;
862 }
863 }
864 }
865
866 return $value;
867 }
868
869 public function unixstamp($excelDateTime)
870 {
871
872 $d = floor($excelDateTime); // days since 1900 or 1904
873 $t = $excelDateTime - $d;
874
875 if ($this->date1904) {
876 $d += 1462;
877 }
878
879 $t = (abs($d) > 0) ? ($d - 25569) * 86400 + round($t * 86400) : round($t * 86400);
880
881 return (int)$t;
882 }
883
884 public function toHTML($worksheetIndex = 0)
885 {
886 $s = '<table class=excel>';
887 foreach ($this->readRows($worksheetIndex) as $r) {
888 $s .= '<tr>';
889 foreach ($r as $c) {
890 $s .= '<td nowrap>' . ($c === '' ? '&nbsp' : htmlspecialchars($c, ENT_QUOTES)) . '</td>';
891 }
892 $s .= "</tr>\r\n";
893 }
894 $s .= '</table>';
895
896 return $s;
897 }
898 public function toHTMLEx($worksheetIndex = 0)
899 {
900 $s = '<table class=excel>';
901 $y = 0;
902 foreach ($this->readRowsEx($worksheetIndex) as $r) {
903 $s .= '<tr>';
904 $x = 0;
905 foreach ($r as $c) {
906 $tag = 'td';
907 $css = $c['css'];
908 if ($y === 0) {
909 $tag = 'th';
910 $css .= $c['width'] ? 'width: '.round($c['width'] * 0.47, 2).'em;' : '';
911 }
912
913 if ($x === 0 && $c['height']) {
914 $css .= 'height: '.round($c['height'] * 1.3333).'px;';
915 }
916 $s .= '<'.$tag.' style="'.$css.'" nowrap>'
917 . ($c['value'] === '' ? '&nbsp' : htmlspecialchars($c['value'], ENT_QUOTES)) . '</'.$tag.'>';
918 $x++;
919 }
920 $s .= "</tr>\r\n";
921 $y++;
922 }
923 $s .= '</table>';
924
925 return $s;
926 }
927 public function rows($worksheetIndex = 0, $limit = 0)
928 {
929 return iterator_to_array($this->readRows($worksheetIndex, $limit), false);
930 }
931 // thx Gonzo
932 /**
933 * @param $worksheetIndex
934 * @param $limit
935 * @return \Generator
936 */
937 public function readRows($worksheetIndex = 0, $limit = 0)
938 {
939
940 if (($ws = $this->worksheet($worksheetIndex)) === false) {
941 return;
942 }
943 $dim = $this->dimension($worksheetIndex);
944 $numCols = $dim[0];
945 $numRows = $dim[1];
946
947 $emptyRow = [];
948 for ($i = 0; $i < $numCols; $i++) {
949 $emptyRow[] = '';
950 }
951
952 $curR = 0;
953 $_limit = $limit;
954 /* @var SimpleXMLElement $ws */
955 foreach ($ws->sheetData->row as $row) {
956 $r = $emptyRow;
957 $curC = 0;
958 foreach ($row->c as $c) {
959 // detect skipped cols
960 $idx = $this->getIndex((string)$c['r']);
961 $x = $idx[0];
962 $y = $idx[1];
963
964 if ($x > -1) {
965 $curC = $x;
966 while ($curR < $y) {
967 yield $emptyRow;
968 $curR++;
969 $_limit--;
970 if ($_limit === 0) {
971 return;
972 }
973 }
974 }
975 $r[$curC] = $this->value($c);
976 $curC++;
977 }
978 yield $r;
979
980 $curR++;
981 $_limit--;
982 if ($_limit === 0) {
983 return;
984 }
985 }
986 while ($curR < $numRows) {
987 yield $emptyRow;
988 $curR++;
989 $_limit--;
990 if ($_limit === 0) {
991 return;
992 }
993 }
994 }
995
996 public function rowsEx($worksheetIndex = 0, $limit = 0)
997 {
998 return iterator_to_array($this->readRowsEx($worksheetIndex, $limit), false);
999 }
1000 // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
1001 /**
1002 * @param $worksheetIndex
1003 * @param $limit
1004 * @return \Generator|null
1005 */
1006 public function readRowsEx($worksheetIndex = 0, $limit = 0)
1007 {
1008 if (!$this->rowsExReader) {
1009 require_once __DIR__ . '/SimpleXLSXEx.php';
1010 $this->rowsExReader = new SimpleXLSXEx($this);
1011 }
1012 return $this->rowsExReader->readRowsEx($worksheetIndex, $limit);
1013 }
1014
1015 /**
1016 * Returns cell value
1017 * VERY SLOW! Use ->rows() or ->rowsEx()
1018 *
1019 * @param int $worksheetIndex
1020 * @param string|array $cell ref or coords, D12 or [3,12]
1021 *
1022 * @return mixed Returns NULL if not found
1023 */
1024 public function getCell($worksheetIndex = 0, $cell = 'A1')
1025 {
1026
1027 if (($ws = $this->worksheet($worksheetIndex)) === false) {
1028 return false;
1029 }
1030 if (is_array($cell)) {
1031 $cell = self::num2name($cell[0]) . $cell[1];// [3,21] -> D21
1032 }
1033 if (is_string($cell)) {
1034 $result = $ws->sheetData->xpath("row/c[@r='" . $cell . "']");
1035 if (count($result)) {
1036 return $this->value($result[0]);
1037 }
1038 }
1039
1040 return null;
1041 }
1042
1043 public function getSheets()
1044 {
1045 return $this->sheets;
1046 }
1047
1048 public function sheetsCount()
1049 {
1050 return count($this->sheets);
1051 }
1052
1053 public function sheetName($worksheetIndex)
1054 {
1055 $sn = $this->sheetNames();
1056 if (isset($sn[$worksheetIndex])) {
1057 return $sn[$worksheetIndex];
1058 }
1059
1060 return false;
1061 }
1062
1063 public function sheetNames()
1064 {
1065 $a = [];
1066 foreach ($this->sheetMetaData as $k => $v) {
1067 $a[$k] = $v['name'];
1068 }
1069 return $a;
1070 }
1071 public function sheetMeta($worksheetIndex = null)
1072 {
1073 if ($worksheetIndex === null) {
1074 return $this->sheetMetaData;
1075 }
1076 return isset($this->sheetMetaData[$worksheetIndex]) ? $this->sheetMetaData[$worksheetIndex] : false;
1077 }
1078 public function isHiddenSheet($worksheetIndex)
1079 {
1080 return isset($this->sheetMetaData[$worksheetIndex]['state'])
1081 && $this->sheetMetaData[$worksheetIndex]['state'] === 'hidden';
1082 }
1083
1084 public function getStyles()
1085 {
1086 return $this->styles;
1087 }
1088
1089 public function getPackage()
1090 {
1091 return $this->package;
1092 }
1093
1094 public function setDateTimeFormat($value)
1095 {
1096 $this->datetimeFormat = is_string($value) ? $value : false;
1097 }
1098
1099 public static function getTarget($base, $target)
1100 {
1101 $target = trim($target);
1102 if (strpos($target, '/') === 0) {
1103 return self::substr($target, 1);
1104 }
1105 $target = ($base ? $base . '/' : '') . $target;
1106 // a/b/../c -> a/c
1107 $parts = explode('/', $target);
1108 $abs = [];
1109 foreach ($parts as $p) {
1110 if ('.' === $p) {
1111 continue;
1112 }
1113 if ('..' === $p) {
1114 array_pop($abs);
1115 } else {
1116 $abs[] = $p;
1117 }
1118 }
1119 return implode('/', $abs);
1120 }
1121
1122 public static function parseRichText($is = null)
1123 {
1124 $value = [];
1125
1126 if (isset($is->t)) {
1127 $value[] = (string)$is->t;
1128 } elseif (isset($is->r)) {
1129 foreach ($is->r as $run) {
1130 $value[] = (string)$run->t;
1131 }
1132 }
1133
1134 return implode('', $value);
1135 }
1136
1137 public static function num2name($num)
1138 {
1139 $numeric = ($num - 1) % 26;
1140 $letter = chr(65 + $numeric);
1141 $num2 = (int)(($num - 1) / 26);
1142 if ($num2 > 0) {
1143 return self::num2name($num2) . $letter;
1144 }
1145 return $letter;
1146 }
1147
1148 public static function strlen($str)
1149 {
1150 return (ini_get('mbstring.func_overload') & 2) ? mb_strlen($str, '8bit') : strlen($str);
1151 }
1152
1153 public static function substr($str, $start, $length = null)
1154 {
1155 return (ini_get('mbstring.func_overload') & 2) ?
1156 mb_substr($str, $start, ($length === null) ? mb_strlen($str, '8bit') : $length, '8bit')
1157 : substr($str, $start, ($length === null) ? strlen($str) : $length);
1158 }
1159
1160 public static function strpos($haystack, $needle, $offset = 0)
1161 {
1162 return (ini_get('mbstring.func_overload') & 2) ?
1163 mb_strpos($haystack, $needle, $offset, '8bit') : strpos($haystack, $needle, $offset);
1164 }
1165 public static function boolean($value)
1166 {
1167 if (is_numeric($value)) {
1168 return (bool) $value;
1169 }
1170
1171 return $value === 'true' || $value === 'TRUE';
1172 }
1173 }
1174