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

1,166 lines 28.6 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.13 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'] = SimpleXLSX::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', SimpleXLSX::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 = SimpleXLSX::substr($vZ, 0, -12);
241 if (SimpleXLSX::substr($vZ, -4) === "\x50\x4b\x07\x08") {
242 $vZ = SimpleXLSX::substr($vZ, 0, -4);
243 }
244 }
245
246 // Getting stored filename
247 $aI['N'] = SimpleXLSX::substr($vZ, 26, $nF);
248 $aI['N'] = str_replace('\\', '/', $aI['N']);
249
250 if (SimpleXLSX::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 = SimpleXLSX::substr($vZ, 26 + $nF + $mF);
261
262 if ($aP['CS'] > 0 && (SimpleXLSX::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 = SimpleXLSX::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 = SimpleXLSX::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[] = SimpleXLSX::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 $entry_xml = preg_replace('/[a-zA-Z0-9]+:([a-zA-Z0-9]+="[^"]+")/', '$1', $entry_xml); // remove namespaced attrs
482 $entry_xml .= ' ';
483 $entry_xml = preg_replace('/<[a-zA-Z0-9]+:([^>]+)>/', '<$1>', $entry_xml); // fix namespaced openned tags
484 $entry_xml .= ' ';
485 $entry_xml = preg_replace('/<\/[a-zA-Z0-9]+:([^>]+)>/', '</$1>', $entry_xml); // fix namespaced closed tags
486 $entry_xml .= ' ';
487
488 if (strpos($name, '/sheet')) { // dirty skip empty rows
489 // remove <row...> <c /><c /></row>
490 $entry_xml = preg_replace('/<row[^>]+>\s*(<c[^\/]+\/>\s*)+<\/row>/', '', $entry_xml, -1, $cnt);
491 $entry_xml .= ' ';
492 // remove <row />
493 $entry_xml = preg_replace('/<row[^\/>]*\/>/', '', $entry_xml, -1, $cnt2);
494 $entry_xml .= ' ';
495 // remove <row...></row>
496 $entry_xml = preg_replace('/<row[^>]*><\/row>/', '', $entry_xml, -1, $cnt3);
497 $entry_xml .= ' ';
498 if ($cnt || $cnt2 || $cnt3) {
499 $entry_xml = preg_replace('/<dimension[^\/]+\/>/', '', $entry_xml);
500 $entry_xml .= ' ';
501 }
502 // file_put_contents( basename( $name ), $entry_xml ); // @to do comment!!!
503 }
504 $entry_xml = trim($entry_xml);
505
506 // $m1 = memory_get_usage();
507 // XML External Entity (XXE) Prevention, libxml_disable_entity_loader deprecated in PHP 8
508 if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) {
509 $_old = libxml_disable_entity_loader();
510 }
511
512 $_old_uie = libxml_use_internal_errors(true);
513
514 $entry_xmlobj = simplexml_load_string($entry_xml, 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_PARSEHUGE);
515
516 libxml_use_internal_errors($_old_uie);
517
518 if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) {
519 /** @noinspection PhpUndefinedVariableInspection */
520 libxml_disable_entity_loader($_old);
521 }
522
523 // $m2 = memory_get_usage();
524 // echo round( ($m2-$m1) / (1024 * 1024), 2).' MB'.PHP_EOL;
525
526 if ($entry_xmlobj) {
527 return $entry_xmlobj;
528 }
529 $e = libxml_get_last_error();
530 if ($e) {
531 $this->error(3, 'XML-entry ' . $name . ' parser error ' . $e->message . ' line ' . $e->line);
532 }
533 } else {
534 $this->error(4, 'XML-entry not found ' . $name);
535 }
536
537 return false;
538 }
539
540 // sheets numeration: 1,2,3....
541
542 public function getEntryData($name)
543 {
544 $name = ltrim(str_replace('\\', '/', $name), '/');
545 $dir = SimpleXLSX::strtoupper(dirname($name));
546 $name = SimpleXLSX::strtoupper(basename($name));
547 foreach ($this->package['entries'] as &$entry) {
548 if (SimpleXLSX::strtoupper($entry['path']) === $dir && SimpleXLSX::strtoupper($entry['name']) === $name) {
549 if ($entry['error']) {
550 return false;
551 }
552 switch ($entry['cm']) {
553 case -1:
554 case 0: // Stored
555 // Here is nothing to do, the file ist flat.
556 break;
557 case 8: // Deflated
558 $entry['data'] = gzinflate($entry['data']);
559 break;
560 case 12: // BZIP2
561 if (extension_loaded('bz2')) {
562 $entry['data'] = bzdecompress($entry['data']);
563 } else {
564 $entry['error'] = 7;
565 $entry['error_message'] = 'PHP BZIP2 extension not available.';
566 }
567 break;
568 default:
569 $entry['error'] = 6;
570 $entry['error_msg'] = 'De-/Compression method '.$entry['cm'].' is not supported.';
571 }
572 if (!$entry['error'] && $entry['cm'] > -1) {
573 $entry['cm'] = -1;
574 if ($entry['data'] === false) {
575 $entry['error'] = 2;
576 $entry['error_msg'] = 'Decompression of data failed.';
577 } elseif ($entry['ucs'] > 0 && (SimpleXLSX::strlen($entry['data']) !== (int)$entry['ucs'])) {
578 $entry['error'] = 3;
579 $entry['error_msg'] = 'Uncompressed size is not equal with the value in header information.';
580 } elseif (crc32($entry['data']) !== $entry['crc']) {
581 $entry['error'] = 4;
582 $entry['error_msg'] = 'CRC32 checksum is not equal with the value in header information.';
583 }
584 }
585
586 return $entry['data'];
587 }
588 }
589 unset($entry);
590 $this->error(5, 'Entry not found ' . ($dir ? $dir . '/' : '') . $name);
591
592 return false;
593 }
594 public function deleteEntry($name)
595 {
596 $name = ltrim(str_replace('\\', '/', $name), '/');
597 $dir = SimpleXLSX::strtoupper(dirname($name));
598 $name = SimpleXLSX::strtoupper(basename($name));
599 foreach ($this->package['entries'] as $k => $entry) {
600 if (SimpleXLSX::strtoupper($entry['path']) === $dir && SimpleXLSX::strtoupper($entry['name']) === $name) {
601 unset($this->package['entries'][$k]);
602 return true;
603 }
604 }
605 return false;
606 }
607
608 public static function strtoupper($str)
609 {
610 return (ini_get('mbstring.func_overload') & 2) ? mb_strtoupper($str, '8bit') : strtoupper($str);
611 }
612
613 /*
614 * @param string $name Filename in archive
615 * @return SimpleXMLElement|bool
616 */
617
618 public function entryExists($name)
619 {
620 // 0.6.6
621 $dir = SimpleXLSX::strtoupper(dirname($name));
622 $name = SimpleXLSX::strtoupper(basename($name));
623 foreach ($this->package['entries'] as $entry) {
624 if (SimpleXLSX::strtoupper($entry['path']) === $dir && SimpleXLSX::strtoupper($entry['name']) === $name) {
625 return true;
626 }
627 }
628
629 return false;
630 }
631
632 public static function parseFile($filename, $debug = false)
633 {
634 return self::parse($filename, false, $debug);
635 }
636
637 public static function parse($filename, $is_data = false, $debug = false)
638 {
639 $xlsx = new self();
640 $xlsx->debug = $debug;
641 if ($xlsx->unzip($filename, $is_data)) {
642 $xlsx->parseEntries();
643 }
644 if ($xlsx->success()) {
645 return $xlsx;
646 }
647 self::parseError($xlsx->error());
648 self::parseErrno($xlsx->errno());
649
650 return false;
651 }
652
653 public function success()
654 {
655 return !$this->error;
656 }
657
658 // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
659
660 public static function parseError($set = false)
661 {
662 static $error = false;
663
664 return $set ? $error = $set : $error;
665 }
666
667 public static function parseErrno($set = false)
668 {
669 static $errno = false;
670
671 return $set ? $errno = $set : $errno;
672 }
673
674 public function errno()
675 {
676 return $this->errno;
677 }
678
679 public static function parseData($data, $debug = false)
680 {
681 return self::parse($data, true, $debug);
682 }
683
684
685
686 public function worksheet($worksheetIndex = 0)
687 {
688 if (isset($this->sheets[$worksheetIndex])) {
689 return $this->sheets[$worksheetIndex];
690 }
691 $this->error(6, 'Worksheet not found ' . $worksheetIndex);
692
693 return false;
694 }
695
696 /**
697 * returns [numCols,numRows] of worksheet
698 *
699 * @param int $worksheetIndex
700 *
701 * @return array
702 */
703 public function dimension($worksheetIndex = 0)
704 {
705
706 if (($ws = $this->worksheet($worksheetIndex)) === false) {
707 return [0, 0];
708 }
709 /* @var SimpleXMLElement $ws */
710
711 $ref = (string)$ws->dimension['ref'];
712
713 if (SimpleXLSX::strpos($ref, ':') !== false) {
714 $d = explode(':', $ref);
715 $idx = $this->getIndex($d[1]);
716
717 return [$idx[0] + 1, $idx[1] + 1];
718 }
719 /*
720 if ( $ref !== '' ) { // 0.6.8
721 $index = $this->getIndex( $ref );
722
723 return [ $index[0] + 1, $index[1] + 1 ];
724 }
725 */
726
727 // slow method
728 $maxC = $maxR = 0;
729 $iR = -1;
730 foreach ($ws->sheetData->row as $row) {
731 $iR++;
732 $iC = -1;
733 foreach ($row->c as $c) {
734 $iC++;
735 $idx = $this->getIndex((string)$c['r']);
736 $x = $idx[0];
737 $y = $idx[1];
738 if ($x > -1) {
739 if ($x > $maxC) {
740 $maxC = $x;
741 }
742 if ($y > $maxR) {
743 $maxR = $y;
744 }
745 } else {
746 if ($iC > $maxC) {
747 $maxC = $iC;
748 }
749 if ($iR > $maxR) {
750 $maxR = $iR;
751 }
752 }
753 }
754 }
755
756 return [$maxC + 1, $maxR + 1];
757 }
758
759 public function getIndex($cell = 'A1')
760 {
761
762 if (preg_match('/([A-Z]+)(\d+)/', $cell, $m)) {
763 $col = $m[1];
764 $row = $m[2];
765
766 $colLen = SimpleXLSX::strlen($col);
767 $index = 0;
768
769 for ($i = $colLen - 1; $i >= 0; $i--) {
770 $index += (ord($col[$i]) - 64) * pow(26, $colLen - $i - 1);
771 }
772
773 return [$index - 1, $row - 1];
774 }
775
776 // $this->error( 'Invalid cell index ' . $cell );
777
778 return [-1, -1];
779 }
780
781 public function value($cell)
782 {
783 // Determine data type
784 $dataType = (string)$cell['t'];
785
786 if ($dataType === '' || $dataType === 'n') { // number
787 $s = (int)$cell['s'];
788 if ($s > 0 && isset($this->cellFormats[$s])) {
789 if (array_key_exists('format', $this->cellFormats[$s])) {
790 $format = $this->cellFormats[$s]['format'];
791 if ($format && preg_match('/[mM]/', preg_replace('/\"[^"]+\"/', '', $format))) { // [mm]onth,AM|PM
792 $dataType = 'D';
793 }
794 } else {
795 $dataType = 'n';
796 }
797 }
798 }
799
800 $value = '';
801
802 switch ($dataType) {
803 case 's':
804 // Value is a shared string
805 if ((string)$cell->v !== '') {
806 $value = $this->sharedstrings[(int)$cell->v];
807 }
808 break;
809
810 case 'str': // formula?
811 if ((string)$cell->v !== '') {
812 $value = (string)$cell->v;
813 }
814 break;
815
816 case 'b':
817 // Value is boolean
818 $value = (string)$cell->v;
819 if ($value === '0') {
820 $value = false;
821 } elseif ($value === '1') {
822 $value = true;
823 } else {
824 $value = (bool)$cell->v;
825 }
826
827 break;
828
829 case 'inlineStr':
830 // Value is rich text inline
831 $value = SimpleXLSX::parseRichText($cell->is);
832
833 break;
834
835 case 'e':
836 // Value is an error message
837 if ((string)$cell->v !== '') {
838 $value = (string)$cell->v;
839 }
840 break;
841
842 case 'D':
843 // Date as float
844 if (!empty($cell->v)) {
845 $value = $this->datetimeFormat ? gmdate($this->datetimeFormat, $this->unixstamp((float)$cell->v)) : (float)$cell->v;
846 }
847 break;
848
849 case 'd':
850 // Date as ISO YYYY-MM-DD
851 if ((string)$cell->v !== '') {
852 $value = (string)$cell->v;
853 }
854 break;
855
856 default:
857 // Value is a string
858 $value = (string)$cell->v;
859
860 // Check for numeric values
861 if (is_numeric($value)) {
862 /** @noinspection TypeUnsafeComparisonInspection */
863 if ($value == (int)$value) {
864 $value = (int)$value;
865 } /** @noinspection TypeUnsafeComparisonInspection */ elseif ($value == (float)$value) {
866 $value = (float)$value;
867 }
868 }
869 }
870
871 return $value;
872 }
873
874 public function unixstamp($excelDateTime)
875 {
876
877 $d = floor($excelDateTime); // days since 1900 or 1904
878 $t = $excelDateTime - $d;
879
880 if ($this->date1904) {
881 $d += 1462;
882 }
883
884 $t = (abs($d) > 0) ? ($d - 25569) * 86400 + round($t * 86400) : round($t * 86400);
885
886 return (int)$t;
887 }
888
889 public function toHTML($worksheetIndex = 0)
890 {
891 $s = '<table class=excel>';
892 foreach ($this->readRows($worksheetIndex) as $r) {
893 $s .= '<tr>';
894 foreach ($r as $c) {
895 $s .= '<td nowrap>' . ($c === '' ? '&nbsp' : htmlspecialchars($c, ENT_QUOTES)) . '</td>';
896 }
897 $s .= "</tr>\r\n";
898 }
899 $s .= '</table>';
900
901 return $s;
902 }
903 public function toHTMLEx($worksheetIndex = 0)
904 {
905 $s = '<table class=excel>';
906 $y = 0;
907 foreach ($this->readRowsEx($worksheetIndex) as $r) {
908 $s .= '<tr>';
909 $x = 0;
910 foreach ($r as $c) {
911 $tag = 'td';
912 $css = $c['css'];
913 if ($y === 0) {
914 $tag = 'th';
915 $css .= $c['width'] ? 'width: '.round($c['width'] * 0.47, 2).'em;' : '';
916 }
917
918 if ($x === 0 && $c['height']) {
919 $css .= 'height: '.round($c['height'] * 1.3333).'px;';
920 }
921 $s .= '<'.$tag.' style="'.$css.'" nowrap>' . ($c['value'] === '' ? '&nbsp' : htmlspecialchars($c['value'], ENT_QUOTES)) . '</'.$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 = SimpleXLSX::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']) && $this->sheetMetaData[$worksheetIndex]['state'] === 'hidden';
1085 }
1086
1087 public function getStyles()
1088 {
1089 return $this->styles;
1090 }
1091
1092 public function getPackage()
1093 {
1094 return $this->package;
1095 }
1096
1097 public function setDateTimeFormat($value)
1098 {
1099 $this->datetimeFormat = is_string($value) ? $value : false;
1100 }
1101
1102 public static function getTarget($base, $target)
1103 {
1104 $target = trim($target);
1105 if (strpos($target, '/') === 0) {
1106 return SimpleXLSX::substr($target, 1);
1107 }
1108 $target = ($base ? $base . '/' : '') . $target;
1109 // a/b/../c -> a/c
1110 $parts = explode('/', $target);
1111 $abs = [];
1112 foreach ($parts as $p) {
1113 if ('.' === $p) {
1114 continue;
1115 }
1116 if ('..' === $p) {
1117 array_pop($abs);
1118 } else {
1119 $abs[] = $p;
1120 }
1121 }
1122 return implode('/', $abs);
1123 }
1124
1125 public static function parseRichText($is = null)
1126 {
1127 $value = [];
1128
1129 if (isset($is->t)) {
1130 $value[] = (string)$is->t;
1131 } elseif (isset($is->r)) {
1132 foreach ($is->r as $run) {
1133 $value[] = (string)$run->t;
1134 }
1135 }
1136
1137 return implode('', $value);
1138 }
1139
1140 public static function num2name($num)
1141 {
1142 $numeric = ($num - 1) % 26;
1143 $letter = chr(65 + $numeric);
1144 $num2 = (int)(($num - 1) / 26);
1145 if ($num2 > 0) {
1146 return SimpleXLSX::num2name($num2) . $letter;
1147 }
1148 return $letter;
1149 }
1150
1151 public static function strlen($str)
1152 {
1153 return (ini_get('mbstring.func_overload') & 2) ? mb_strlen($str, '8bit') : strlen($str);
1154 }
1155
1156 public static function substr($str, $start, $length = null)
1157 {
1158 return (ini_get('mbstring.func_overload') & 2) ? mb_substr($str, $start, ($length === null) ? mb_strlen($str, '8bit') : $length, '8bit') : substr($str, $start, ($length === null) ? strlen($str) : $length);
1159 }
1160
1161 public static function strpos($haystack, $needle, $offset = 0)
1162 {
1163 return (ini_get('mbstring.func_overload') & 2) ? mb_strpos($haystack, $needle, $offset, '8bit') : strpos($haystack, $needle, $offset);
1164 }
1165 }
1166