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

1,178 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.17 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 $v = htmlspecialchars($c['value'], ENT_QUOTES);
919 $v = preg_replace('/\R/', "<br>\r\n", $v);
920 $s .= '<'.$tag.' style="'.$css.'" nowrap>'
921 . ($v === '' ? '&nbsp' : $v) . '</'.$tag.'>';
922 $x++;
923 }
924 $s .= "</tr>\r\n";
925 $y++;
926 }
927 $s .= '</table>';
928
929 return $s;
930 }
931 public function rows($worksheetIndex = 0, $limit = 0)
932 {
933 return iterator_to_array($this->readRows($worksheetIndex, $limit), false);
934 }
935 // thx Gonzo
936 /**
937 * @param $worksheetIndex
938 * @param $limit
939 * @return \Generator
940 */
941 public function readRows($worksheetIndex = 0, $limit = 0)
942 {
943
944 if (($ws = $this->worksheet($worksheetIndex)) === false) {
945 return;
946 }
947 $dim = $this->dimension($worksheetIndex);
948 $numCols = $dim[0];
949 $numRows = $dim[1];
950
951 $emptyRow = [];
952 for ($i = 0; $i < $numCols; $i++) {
953 $emptyRow[] = '';
954 }
955
956 $curR = 0;
957 $_limit = $limit;
958 /* @var SimpleXMLElement $ws */
959 foreach ($ws->sheetData->row as $row) {
960 $r = $emptyRow;
961 $curC = 0;
962 foreach ($row->c as $c) {
963 // detect skipped cols
964 $idx = $this->getIndex((string)$c['r']);
965 $x = $idx[0];
966 $y = $idx[1];
967
968 if ($x > -1) {
969 $curC = $x;
970 while ($curR < $y) {
971 yield $emptyRow;
972 $curR++;
973 $_limit--;
974 if ($_limit === 0) {
975 return;
976 }
977 }
978 }
979 $r[$curC] = $this->value($c);
980 $curC++;
981 }
982 yield $r;
983
984 $curR++;
985 $_limit--;
986 if ($_limit === 0) {
987 return;
988 }
989 }
990 while ($curR < $numRows) {
991 yield $emptyRow;
992 $curR++;
993 $_limit--;
994 if ($_limit === 0) {
995 return;
996 }
997 }
998 }
999
1000 public function rowsEx($worksheetIndex = 0, $limit = 0)
1001 {
1002 return iterator_to_array($this->readRowsEx($worksheetIndex, $limit), false);
1003 }
1004 // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
1005 /**
1006 * @param $worksheetIndex
1007 * @param $limit
1008 * @return \Generator|null
1009 */
1010 public function readRowsEx($worksheetIndex = 0, $limit = 0)
1011 {
1012 if (!$this->rowsExReader) {
1013 require_once __DIR__ . '/SimpleXLSXEx.php';
1014 $this->rowsExReader = new SimpleXLSXEx($this);
1015 }
1016 return $this->rowsExReader->readRowsEx($worksheetIndex, $limit);
1017 }
1018
1019 /**
1020 * Returns cell value
1021 * VERY SLOW! Use ->rows() or ->rowsEx()
1022 *
1023 * @param int $worksheetIndex
1024 * @param string|array $cell ref or coords, D12 or [3,12]
1025 *
1026 * @return mixed Returns NULL if not found
1027 */
1028 public function getCell($worksheetIndex = 0, $cell = 'A1')
1029 {
1030
1031 if (($ws = $this->worksheet($worksheetIndex)) === false) {
1032 return false;
1033 }
1034 if (is_array($cell)) {
1035 $cell = self::num2name($cell[0]) . $cell[1];// [3,21] -> D21
1036 }
1037 if (is_string($cell)) {
1038 $result = $ws->sheetData->xpath("row/c[@r='" . $cell . "']");
1039 if (count($result)) {
1040 return $this->value($result[0]);
1041 }
1042 }
1043
1044 return null;
1045 }
1046
1047 public function getSheets()
1048 {
1049 return $this->sheets;
1050 }
1051
1052 public function sheetsCount()
1053 {
1054 return count($this->sheets);
1055 }
1056
1057 public function sheetName($worksheetIndex)
1058 {
1059 $sn = $this->sheetNames();
1060 if (isset($sn[$worksheetIndex])) {
1061 return $sn[$worksheetIndex];
1062 }
1063
1064 return false;
1065 }
1066
1067 public function sheetNames()
1068 {
1069 $a = [];
1070 foreach ($this->sheetMetaData as $k => $v) {
1071 $a[$k] = $v['name'];
1072 }
1073 return $a;
1074 }
1075 public function sheetMeta($worksheetIndex = null)
1076 {
1077 if ($worksheetIndex === null) {
1078 return $this->sheetMetaData;
1079 }
1080 return isset($this->sheetMetaData[$worksheetIndex]) ? $this->sheetMetaData[$worksheetIndex] : false;
1081 }
1082 public function isHiddenSheet($worksheetIndex)
1083 {
1084 return isset($this->sheetMetaData[$worksheetIndex]['state'])
1085 && $this->sheetMetaData[$worksheetIndex]['state'] === 'hidden';
1086 }
1087
1088 public function getStyles()
1089 {
1090 return $this->styles;
1091 }
1092
1093 public function getPackage()
1094 {
1095 return $this->package;
1096 }
1097
1098 public function setDateTimeFormat($value)
1099 {
1100 $this->datetimeFormat = is_string($value) ? $value : false;
1101 }
1102
1103 public static function getTarget($base, $target)
1104 {
1105 $target = trim($target);
1106 if (strpos($target, '/') === 0) {
1107 return self::substr($target, 1);
1108 }
1109 $target = ($base ? $base . '/' : '') . $target;
1110 // a/b/../c -> a/c
1111 $parts = explode('/', $target);
1112 $abs = [];
1113 foreach ($parts as $p) {
1114 if ('.' === $p) {
1115 continue;
1116 }
1117 if ('..' === $p) {
1118 array_pop($abs);
1119 } else {
1120 $abs[] = $p;
1121 }
1122 }
1123 return implode('/', $abs);
1124 }
1125
1126 public static function parseRichText($is = null)
1127 {
1128 $value = [];
1129
1130 if (isset($is->t)) {
1131 $value[] = (string)$is->t;
1132 } elseif (isset($is->r)) {
1133 foreach ($is->r as $run) {
1134 $value[] = (string)$run->t;
1135 }
1136 }
1137
1138 return implode('', $value);
1139 }
1140
1141 public static function num2name($num)
1142 {
1143 $numeric = ($num - 1) % 26;
1144 $letter = chr(65 + $numeric);
1145 $num2 = (int)(($num - 1) / 26);
1146 if ($num2 > 0) {
1147 return self::num2name($num2) . $letter;
1148 }
1149 return $letter;
1150 }
1151
1152 public static function strlen($str)
1153 {
1154 return (ini_get('mbstring.func_overload') & 2) ? mb_strlen($str, '8bit') : strlen($str);
1155 }
1156
1157 public static function substr($str, $start, $length = null)
1158 {
1159 return (ini_get('mbstring.func_overload') & 2) ?
1160 mb_substr($str, $start, ($length === null) ? mb_strlen($str, '8bit') : $length, '8bit')
1161 : substr($str, $start, ($length === null) ? strlen($str) : $length);
1162 }
1163
1164 public static function strpos($haystack, $needle, $offset = 0)
1165 {
1166 return (ini_get('mbstring.func_overload') & 2) ?
1167 mb_strpos($haystack, $needle, $offset, '8bit') : strpos($haystack, $needle, $offset);
1168 }
1169 public static function boolean($value)
1170 {
1171 if (is_numeric($value)) {
1172 return (bool) $value;
1173 }
1174
1175 return $value === 'true' || $value === 'TRUE';
1176 }
1177 }
1178