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

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

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