PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.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 All 45 releases
tablepress / libraries / simplexlsx.class.php

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

1,197 lines 29.1 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.19 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 // Signed ZIP64 descriptor:
235 // signature + CRC32 + 64-bit compressed size + 64-bit uncompressed size
236 if (self::substr($vZ, -24, 4) === "\x50\x4b\x07\x08") {
237 $aP1 = unpack(
238 'V1CRC/V1CSLow/V1CSHigh/V1UCSLow/V1UCSHigh',
239 self::substr($vZ, -20)
240 );
241
242 if ((int)$aP1['CSHigh'] !== 0 || (int)$aP1['UCSHigh'] !== 0) {
243 $aI['E'] = 6;
244 $aI['EM'] = 'ZIP64 entry is too large to process.';
245 }
246
247 $aP['CRC'] = $aP1['CRC'];
248 $aP['CS'] = $aP1['CSLow'];
249 $aP['UCS'] = $aP1['UCSLow'];
250 $vZ = self::substr($vZ, 0, -24);
251 } else {
252 $aP1 = unpack('V1CRC/V1CS/V1UCS', self::substr($vZ, -12));
253
254 $aP['CRC'] = $aP1['CRC'];
255 $aP['CS'] = $aP1['CS'];
256 $aP['UCS'] = $aP1['UCS'];
257 // 2013-08-10
258 $vZ = self::substr($vZ, 0, -12);
259 if (self::substr($vZ, -4) === "\x50\x4b\x07\x08") {
260 $vZ = self::substr($vZ, 0, -4);
261 }
262 }
263 }
264
265 // Getting stored filename
266 $aI['N'] = self::substr($vZ, 26, $nF);
267 $aI['N'] = str_replace('\\', '/', $aI['N']);
268
269 if (self::substr($aI['N'], -1) === '/') {
270 // is a directory entry - will be skipped
271 continue;
272 }
273
274 // Truncate full filename in path and filename
275 $aI['P'] = dirname($aI['N']);
276 $aI['P'] = ($aI['P'] === '.') ? '' : $aI['P'];
277 $aI['N'] = basename($aI['N']);
278
279 $vZ = self::substr($vZ, 26 + $nF + $mF);
280
281 if ($aP['CS'] > 0 && (self::strlen($vZ) !== (int)$aP['CS'])) { // check only if availabled
282 $aI['E'] = 1;
283 $aI['EM'] = 'Compressed size is not equal with the value in header information.';
284 }
285 // } elseif ( $bE ) {
286 // $aI['E'] = 5;
287 // $aI['EM'] = 'File is encrypted, which is not supported from this class.';
288 /* } else {
289 switch ($aP['CM']) {
290 case 0: // Stored
291 // Here is nothing to do, the file ist flat.
292 break;
293 case 8: // Deflated
294 $vZ = gzinflate($vZ);
295 break;
296 case 12: // BZIP2
297 if (extension_loaded('bz2')) {
298 $vZ = bzdecompress($vZ);
299 } else {
300 $aI['E'] = 7;
301 $aI['EM'] = 'PHP BZIP2 extension not available.';
302 }
303 break;
304 default:
305 $aI['E'] = 6;
306 $aI['EM'] = "De-/Compression method {$aP['CM']} is not supported.";
307 }
308 if (!$aI['E']) {
309 if ($vZ === false) {
310 $aI['E'] = 2;
311 $aI['EM'] = 'Decompression of data failed.';
312 } elseif ($this->_strlen($vZ) !== (int)$aP['UCS']) {
313 $aI['E'] = 3;
314 $aI['EM'] = 'Uncompressed size is not equal with the value in header information.';
315 } elseif (crc32($vZ) !== $aP['CRC']) {
316 $aI['E'] = 4;
317 $aI['EM'] = 'CRC32 checksum is not equal with the value in header information.';
318 }
319 }
320 }
321 */
322
323 // DOS to UNIX timestamp
324 $aI['T'] = mktime(
325 ($aP['FT'] & 0xf800) >> 11,
326 ($aP['FT'] & 0x07e0) >> 5,
327 ($aP['FT'] & 0x001f) << 1,
328 ($aP['FD'] & 0x01e0) >> 5,
329 $aP['FD'] & 0x001f,
330 (($aP['FD'] & 0xfe00) >> 9) + 1980
331 );
332
333 $this->package['entries'][] = [
334 'data' => $vZ,
335 'ucs' => (int)$aP['UCS'], // ucompresses size
336 'cm' => $aP['CM'], // compressed method
337 'cs' => isset($aP['CS']) ? (int) $aP['CS'] : 0, // compresses size
338 'crc' => $aP['CRC'],
339 'error' => $aI['E'],
340 'error_msg' => $aI['EM'],
341 'name' => $aI['N'],
342 'path' => $aI['P'],
343 'time' => $aI['T']
344 ];
345 } // end for each entries
346
347 return true;
348 }
349
350
351 public function error($num = null, $str = null)
352 {
353 if ($num) {
354 $this->errno = $num;
355 $this->error = $str;
356 if ($this->debug) {
357 trigger_error(__CLASS__ . ': ' . $this->error, E_USER_WARNING);
358 }
359 }
360
361 return $this->error;
362 }
363
364 public function parseEntries()
365 {
366 // Document data holders
367 $this->sharedstrings = [];
368 $this->sheets = [];
369 // $this->styles = array();
370 // $m1 = 0; // memory_get_peak_usage( true );
371 // Read relations and search for officeDocument
372 if ($relations = $this->getEntryXML('_rels/.rels')) {
373 foreach ($relations->Relationship as $rel) {
374 $rel_type = basename(trim((string)$rel['Type'])); // officeDocument
375 $rel_target = self::getTarget('', (string)$rel['Target']); // /xl/workbook.xml or xl/workbook.xml
376
377 if ($rel_type === 'officeDocument'
378 && $workbook = $this->getEntryXML($rel_target)
379 ) {
380 $index_rId = []; // [0 => rId1]
381
382 $index = 0;
383 foreach ($workbook->sheets->sheet as $s) {
384 $a = [];
385 foreach ($s->attributes() as $k => $v) {
386 $a[(string)$k] = (string)$v;
387 }
388 $this->sheetMetaData[$index] = $a;
389 $index_rId[$index] = (string)$s['id'];
390 $index++;
391 }
392 if ((int)$workbook->workbookPr['date1904'] === 1) {
393 $this->date1904 = 1;
394 }
395
396
397 if ($workbookRelations = $this->getEntryXML(dirname($rel_target) . '/_rels/workbook.xml.rels')) {
398 // Loop relations for workbook and extract sheets...
399 foreach ($workbookRelations->Relationship as $workbookRelation) {
400 $wrel_type = basename(trim((string)$workbookRelation['Type'])); // worksheet
401 $wrel_target = self::getTarget(dirname($rel_target), (string)$workbookRelation['Target']);
402 if (!$this->entryExists($wrel_target)) {
403 continue;
404 }
405
406 if ($wrel_type === 'worksheet') { // Sheets
407 if ($sheet = $this->getEntryXML($wrel_target)) {
408 $index = array_search((string)$workbookRelation['Id'], $index_rId, true);
409 $this->sheets[$index] = $sheet;
410 $this->sheetFiles[$index] = $wrel_target;
411 $srel_d = dirname($wrel_target);
412 $srel_f = basename($wrel_target);
413 $srel_file = $srel_d . '/_rels/' . $srel_f . '.rels';
414 if ($this->entryExists($srel_file)) {
415 $this->sheetRels[$index] = $this->getEntryXML($srel_file);
416 }
417 }
418 } elseif ($wrel_type === 'sharedStrings') {
419 if ($sharedStrings = $this->getEntryXML($wrel_target)) {
420 foreach ($sharedStrings->si as $val) {
421 if (isset($val->t)) {
422 $this->sharedstrings[] = (string)$val->t;
423 } elseif (isset($val->r)) {
424 $this->sharedstrings[] = self::parseRichText($val);
425 }
426 }
427 }
428 } elseif ($wrel_type === 'styles') {
429 $this->styles = $this->getEntryXML($wrel_target);
430
431 // number formats
432 $this->nf = [];
433 if (isset($this->styles->numFmts->numFmt)) {
434 foreach ($this->styles->numFmts->numFmt as $v) {
435 $this->nf[(int)$v['numFmtId']] = (string)$v['formatCode'];
436 }
437 }
438
439 $this->cellFormats = [];
440 if (isset($this->styles->cellXfs->xf)) {
441 foreach ($this->styles->cellXfs->xf as $v) {
442 $x = [
443 'format' => null
444 ];
445 foreach ($v->attributes() as $k1 => $v1) {
446 $x[ $k1 ] = (int) $v1;
447 }
448 if (isset($x['numFmtId'])) {
449 if (isset($this->nf[$x['numFmtId']])) {
450 $x['format'] = $this->nf[$x['numFmtId']];
451 } elseif (isset(self::$CF[$x['numFmtId']])) {
452 $x['format'] = self::$CF[$x['numFmtId']];
453 }
454 }
455
456 $this->cellFormats[] = $x;
457 }
458 }
459 } elseif ($wrel_type === 'theme') {
460 $this->theme = $this->getEntryXML($wrel_target);
461 }
462 }
463
464 // break;
465 }
466 // reptile hack :: find active sheet from workbook.xml
467 if ($workbook->bookViews->workbookView) {
468 foreach ($workbook->bookViews->workbookView as $v) {
469 if (!empty($v['activeTab'])) {
470 $this->activeSheet = (int)$v['activeTab'];
471 }
472 }
473 }
474
475 break;
476 }
477 }
478 }
479
480 // $m2 = memory_get_peak_usage(true);
481 // echo __FUNCTION__.' M='.round( ($m2-$m1) / 1048576, 2).'MB'.PHP_EOL;
482
483 if (count($this->sheets)) {
484 // Sort sheets
485 ksort($this->sheets);
486
487 return true;
488 }
489
490 return false;
491 }
492
493 public function getEntryXML($name)
494 {
495 if ($entry_xml = $this->getEntryData($name)) {
496 $this->deleteEntry($name); // economy memory
497 // dirty remove namespace prefixes and empty rows
498 $entry_xml = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $entry_xml); // remove namespaces
499 $entry_xml .= ' '; // force run garbage collector
500 // remove namespaced attrs
501 $entry_xml = preg_replace('/[a-zA-Z0-9]+:([a-zA-Z0-9]+="[^"]+")/', '$1', $entry_xml);
502 $entry_xml .= ' ';
503 $entry_xml = preg_replace('/<[a-zA-Z0-9]+:([^>]+)>/', '<$1>', $entry_xml); // fix namespaced openned tags
504 $entry_xml .= ' ';
505 $entry_xml = preg_replace('/<\/[a-zA-Z0-9]+:([^>]+)>/', '</$1>', $entry_xml); // fix namespaced closed tags
506 $entry_xml .= ' ';
507
508 if (strpos($name, '/sheet')) { // dirty skip empty rows
509 // remove <row...> <c /><c /></row>
510 $cnt = $cnt2 = $cnt3 = null;
511 $entry_xml = preg_replace('/<row[^>]+>\s*(<c[^\/]+\/>\s*)+<\/row>/', '', $entry_xml, -1, $cnt);
512 $entry_xml .= ' ';
513 // remove <row />
514 $entry_xml = preg_replace('/<row[^\/>]*\/>/', '', $entry_xml, -1, $cnt2);
515 $entry_xml .= ' ';
516 // remove <row...></row>
517 $entry_xml = preg_replace('/<row[^>]*><\/row>/', '', $entry_xml, -1, $cnt3);
518 $entry_xml .= ' ';
519 if ($cnt || $cnt2 || $cnt3) {
520 $entry_xml = preg_replace('/<dimension[^\/]+\/>/', '', $entry_xml);
521 $entry_xml .= ' ';
522 }
523 // file_put_contents( basename( $name ), $entry_xml ); // @to do comment!!!
524 }
525 $entry_xml = trim($entry_xml);
526
527 // $m1 = memory_get_usage();
528 // XML External Entity (XXE) Prevention, libxml_disable_entity_loader deprecated in PHP 8
529 if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) {
530 $_old = libxml_disable_entity_loader();
531 }
532
533 $_old_uie = libxml_use_internal_errors(true);
534
535 $entry_xmlobj = simplexml_load_string($entry_xml, 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_PARSEHUGE);
536
537 libxml_use_internal_errors($_old_uie);
538
539 if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) {
540 /** @noinspection PhpUndefinedVariableInspection */
541 libxml_disable_entity_loader($_old);
542 }
543
544 // $m2 = memory_get_usage();
545 // echo round( ($m2-$m1) / (1024 * 1024), 2).' MB'.PHP_EOL;
546
547 if ($entry_xmlobj) {
548 return $entry_xmlobj;
549 }
550 $e = libxml_get_last_error();
551 if ($e) {
552 $this->error(3, 'XML-entry ' . $name . ' parser error ' . $e->message . ' line ' . $e->line);
553 }
554 } else {
555 $this->error(4, 'XML-entry not found ' . $name);
556 }
557
558 return false;
559 }
560
561 // sheets numeration: 1,2,3....
562
563 public function getEntryData($name)
564 {
565 $name = ltrim(str_replace('\\', '/', $name), '/');
566 $dir = self::strtoupper(dirname($name));
567 $name = self::strtoupper(basename($name));
568 foreach ($this->package['entries'] as &$entry) {
569 if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
570 if ($entry['error']) {
571 return false;
572 }
573 switch ($entry['cm']) {
574 case -1:
575 case 0: // Stored
576 // Here is nothing to do, the file ist flat.
577 break;
578 case 8: // Deflated
579 $entry['data'] = gzinflate($entry['data']);
580 break;
581 case 12: // BZIP2
582 if (extension_loaded('bz2')) {
583 $entry['data'] = bzdecompress($entry['data']);
584 } else {
585 $entry['error'] = 7;
586 $entry['error_message'] = 'PHP BZIP2 extension not available.';
587 }
588 break;
589 default:
590 $entry['error'] = 6;
591 $entry['error_msg'] = 'De-/Compression method '.$entry['cm'].' is not supported.';
592 }
593 if (!$entry['error'] && $entry['cm'] > -1) {
594 $entry['cm'] = -1;
595 if ($entry['data'] === false) {
596 $entry['error'] = 2;
597 $entry['error_msg'] = 'Decompression of data failed.';
598 } elseif ($entry['ucs'] > 0 && (self::strlen($entry['data']) !== (int)$entry['ucs'])) {
599 $entry['error'] = 3;
600 $entry['error_msg'] = 'Uncompressed size is not equal with the value in header information.';
601 } elseif (crc32($entry['data']) !== $entry['crc']) {
602 $entry['error'] = 4;
603 $entry['error_msg'] = 'CRC32 checksum is not equal with the value in header information.';
604 }
605 }
606
607 return $entry['data'];
608 }
609 }
610 unset($entry);
611 $this->error(5, 'Entry not found ' . ($dir ? $dir . '/' : '') . $name);
612
613 return false;
614 }
615 public function deleteEntry($name)
616 {
617 $name = ltrim(str_replace('\\', '/', $name), '/');
618 $dir = self::strtoupper(dirname($name));
619 $name = self::strtoupper(basename($name));
620 foreach ($this->package['entries'] as $k => $entry) {
621 if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
622 unset($this->package['entries'][$k]);
623 return true;
624 }
625 }
626 return false;
627 }
628
629 public static function strtoupper($str)
630 {
631 return (ini_get('mbstring.func_overload') & 2) ? mb_strtoupper($str, '8bit') : strtoupper($str);
632 }
633
634 /*
635 * @param string $name Filename in archive
636 * @return SimpleXMLElement|bool
637 */
638
639 public function entryExists($name)
640 {
641 // 0.6.6
642 $dir = self::strtoupper(dirname($name));
643 $name = self::strtoupper(basename($name));
644 foreach ($this->package['entries'] as $entry) {
645 if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
646 return true;
647 }
648 }
649
650 return false;
651 }
652
653 public static function parseFile($filename, $debug = false)
654 {
655 return self::parse($filename, false, $debug);
656 }
657
658 public static function parse($filename, $is_data = false, $debug = false)
659 {
660 $xlsx = new self();
661 $xlsx->debug = $debug;
662 if ($xlsx->unzip($filename, $is_data)) {
663 $xlsx->parseEntries();
664 }
665 if ($xlsx->success()) {
666 return $xlsx;
667 }
668 self::parseError($xlsx->error());
669 self::parseErrno($xlsx->errno());
670
671 return false;
672 }
673
674 public function success()
675 {
676 return !$this->error;
677 }
678
679 // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
680
681 public static function parseError($set = false)
682 {
683 static $error = false;
684
685 return $set ? $error = $set : $error;
686 }
687
688 public static function parseErrno($set = false)
689 {
690 static $errno = false;
691
692 return $set ? $errno = $set : $errno;
693 }
694
695 public function errno()
696 {
697 return $this->errno;
698 }
699
700 public static function parseData($data, $debug = false)
701 {
702 return self::parse($data, true, $debug);
703 }
704
705
706
707 public function worksheet($worksheetIndex = 0)
708 {
709 if (isset($this->sheets[$worksheetIndex])) {
710 return $this->sheets[$worksheetIndex];
711 }
712 $this->error(6, 'Worksheet not found ' . $worksheetIndex);
713
714 return false;
715 }
716
717 /**
718 * returns [numCols,numRows] of worksheet
719 *
720 * @param int $worksheetIndex
721 *
722 * @return array
723 */
724 public function dimension($worksheetIndex = 0)
725 {
726
727 if (($ws = $this->worksheet($worksheetIndex)) === false) {
728 return [0, 0];
729 }
730 /* @var SimpleXMLElement $ws */
731
732 $ref = (string)$ws->dimension['ref'];
733
734 if (self::strpos($ref, ':') !== false) {
735 $d = explode(':', $ref);
736 $idx = $this->getIndex($d[1]);
737
738 return [$idx[0] + 1, $idx[1] + 1];
739 }
740 /*
741 if ( $ref !== '' ) { // 0.6.8
742 $index = $this->getIndex( $ref );
743
744 return [ $index[0] + 1, $index[1] + 1 ];
745 }
746 */
747
748 // slow method
749 $maxC = $maxR = 0;
750 $iR = -1;
751 foreach ($ws->sheetData->row as $row) {
752 $iR++;
753 $iC = -1;
754 foreach ($row->c as $c) {
755 $iC++;
756 $idx = $this->getIndex((string)$c['r']);
757 $x = $idx[0];
758 $y = $idx[1];
759 if ($x > -1) {
760 if ($x > $maxC) {
761 $maxC = $x;
762 }
763 if ($y > $maxR) {
764 $maxR = $y;
765 }
766 } else {
767 if ($iC > $maxC) {
768 $maxC = $iC;
769 }
770 if ($iR > $maxR) {
771 $maxR = $iR;
772 }
773 }
774 }
775 }
776
777 return [$maxC + 1, $maxR + 1];
778 }
779
780 public function getIndex($cell = 'A1')
781 {
782 $m = null;
783
784 if (preg_match('/([A-Z]+)(\d+)/', $cell, $m)) {
785 $col = $m[1];
786 $row = $m[2];
787
788 $colLen = self::strlen($col);
789 $index = 0;
790
791 for ($i = $colLen - 1; $i >= 0; $i--) {
792 $index += (ord($col[$i]) - 64) * pow(26, $colLen - $i - 1);
793 }
794
795 return [$index - 1, $row - 1];
796 }
797
798 // $this->error( 'Invalid cell index ' . $cell );
799
800 return [-1, -1];
801 }
802
803 public function value($cell)
804 {
805 // Determine data type
806 $dataType = (string)$cell['t'];
807
808 if ($dataType === '' || $dataType === 'n') { // number
809 $s = (int)$cell['s'];
810 if ($s > 0 && isset($this->cellFormats[$s])) {
811 if (array_key_exists('format', $this->cellFormats[$s])) {
812 $format = $this->cellFormats[$s]['format'];
813 if ($format && preg_match('/[mM]/', preg_replace('/\"[^"]+\"/', '', $format))) { // [mm]onth,AM|PM
814 $dataType = 'D';
815 }
816 } else {
817 $dataType = 'n';
818 }
819 }
820 }
821
822 $value = '';
823
824 switch ($dataType) {
825 case 's':
826 // Value is a shared string
827 if ((string)$cell->v !== '') {
828 $value = $this->sharedstrings[(int)$cell->v];
829 }
830 break;
831
832 case 'str': // formula?
833 if ((string)$cell->v !== '') {
834 $value = (string)$cell->v;
835 }
836 break;
837
838 case 'b':
839 // Value is boolean
840 $value = self::boolean((string)$cell->v);
841
842 break;
843
844 case 'inlineStr':
845 // Value is rich text inline
846 $value = self::parseRichText($cell->is);
847
848 break;
849
850 case 'e':
851 // Value is an error message
852 if ((string)$cell->v !== '') {
853 $value = (string)$cell->v;
854 }
855 break;
856
857 case 'D':
858 // Date as float
859 if (!empty($cell->v)) {
860 $value = $this->datetimeFormat ?
861 gmdate($this->datetimeFormat, $this->unixstamp((float)$cell->v)) : (float)$cell->v;
862 }
863 break;
864
865 case 'd':
866 // Date as ISO YYYY-MM-DD
867 if ((string)$cell->v !== '') {
868 $value = (string)$cell->v;
869 }
870 break;
871
872 default:
873 // Value is a string
874 $value = (string)$cell->v;
875
876 // Check for numeric values
877 if (is_numeric($value)) {
878 /** @noinspection TypeUnsafeComparisonInspection */
879 if ($value == (int)$value) {
880 $value = (int)$value;
881 } /** @noinspection TypeUnsafeComparisonInspection */ elseif ($value == (float)$value) {
882 $value = (float)$value;
883 }
884 }
885 }
886
887 return $value;
888 }
889
890 public function unixstamp($excelDateTime)
891 {
892
893 $d = floor($excelDateTime); // days since 1900 or 1904
894 $t = $excelDateTime - $d;
895
896 if ($this->date1904) {
897 $d += 1462;
898 }
899
900 $t = (abs($d) > 0) ? ($d - 25569) * 86400 + round($t * 86400) : round($t * 86400);
901
902 return (int)$t;
903 }
904
905 public function toHTML($worksheetIndex = 0)
906 {
907 $s = '<table class=excel>';
908 foreach ($this->readRows($worksheetIndex) as $r) {
909 $s .= '<tr>';
910 foreach ($r as $c) {
911 $s .= '<td nowrap>' . ($c === '' ? '&nbsp' : htmlspecialchars($c, ENT_QUOTES)) . '</td>';
912 }
913 $s .= "</tr>\r\n";
914 }
915 $s .= '</table>';
916
917 return $s;
918 }
919 public function toHTMLEx($worksheetIndex = 0)
920 {
921 $s = '<table class=excel>';
922 $y = 0;
923 foreach ($this->readRowsEx($worksheetIndex) as $r) {
924 $s .= '<tr>';
925 $x = 0;
926 foreach ($r as $c) {
927 $tag = 'td';
928 $css = $c['css'];
929 if ($y === 0) {
930 $tag = 'th';
931 $css .= $c['width'] ? 'width: '.round($c['width'] * 0.47, 2).'em;' : '';
932 }
933
934 if ($x === 0 && $c['height']) {
935 $css .= 'height: '.round($c['height'] * 1.3333).'px;';
936 }
937 $v = htmlspecialchars($c['value'], ENT_QUOTES);
938 $v = preg_replace('/\R/', "<br>\r\n", $v);
939 $s .= '<'.$tag.' style="'.$css.'" nowrap>'
940 . ($v === '' ? '&nbsp' : $v) . '</'.$tag.'>';
941 $x++;
942 }
943 $s .= "</tr>\r\n";
944 $y++;
945 }
946 $s .= '</table>';
947
948 return $s;
949 }
950 public function rows($worksheetIndex = 0, $limit = 0)
951 {
952 return iterator_to_array($this->readRows($worksheetIndex, $limit), false);
953 }
954 // thx Gonzo
955 /**
956 * @param $worksheetIndex
957 * @param $limit
958 * @return \Generator
959 */
960 public function readRows($worksheetIndex = 0, $limit = 0)
961 {
962
963 if (($ws = $this->worksheet($worksheetIndex)) === false) {
964 return;
965 }
966 $dim = $this->dimension($worksheetIndex);
967 $numCols = $dim[0];
968 $numRows = $dim[1];
969
970 $emptyRow = [];
971 for ($i = 0; $i < $numCols; $i++) {
972 $emptyRow[] = '';
973 }
974
975 $curR = 0;
976 $_limit = $limit;
977 /* @var SimpleXMLElement $ws */
978 foreach ($ws->sheetData->row as $row) {
979 $r = $emptyRow;
980 $curC = 0;
981 foreach ($row->c as $c) {
982 // detect skipped cols
983 $idx = $this->getIndex((string)$c['r']);
984 $x = $idx[0];
985 $y = $idx[1];
986
987 if ($x > -1) {
988 $curC = $x;
989 while ($curR < $y) {
990 yield $emptyRow;
991 $curR++;
992 $_limit--;
993 if ($_limit === 0) {
994 return;
995 }
996 }
997 }
998 $r[$curC] = $this->value($c);
999 $curC++;
1000 }
1001 yield $r;
1002
1003 $curR++;
1004 $_limit--;
1005 if ($_limit === 0) {
1006 return;
1007 }
1008 }
1009 while ($curR < $numRows) {
1010 yield $emptyRow;
1011 $curR++;
1012 $_limit--;
1013 if ($_limit === 0) {
1014 return;
1015 }
1016 }
1017 }
1018
1019 public function rowsEx($worksheetIndex = 0, $limit = 0)
1020 {
1021 return iterator_to_array($this->readRowsEx($worksheetIndex, $limit), false);
1022 }
1023 // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
1024 /**
1025 * @param $worksheetIndex
1026 * @param $limit
1027 * @return \Generator|null
1028 */
1029 public function readRowsEx($worksheetIndex = 0, $limit = 0)
1030 {
1031 if (!$this->rowsExReader) {
1032 require_once __DIR__ . '/SimpleXLSXEx.php';
1033 $this->rowsExReader = new SimpleXLSXEx($this);
1034 }
1035 return $this->rowsExReader->readRowsEx($worksheetIndex, $limit);
1036 }
1037
1038 /**
1039 * Returns cell value
1040 * VERY SLOW! Use ->rows() or ->rowsEx()
1041 *
1042 * @param int $worksheetIndex
1043 * @param string|array $cell ref or coords, D12 or [3,12]
1044 *
1045 * @return mixed Returns NULL if not found
1046 */
1047 public function getCell($worksheetIndex = 0, $cell = 'A1')
1048 {
1049
1050 if (($ws = $this->worksheet($worksheetIndex)) === false) {
1051 return false;
1052 }
1053 if (is_array($cell)) {
1054 $cell = self::num2name($cell[0]) . $cell[1];// [3,21] -> D21
1055 }
1056 if (is_string($cell)) {
1057 $result = $ws->sheetData->xpath("row/c[@r='" . $cell . "']");
1058 if (count($result)) {
1059 return $this->value($result[0]);
1060 }
1061 }
1062
1063 return null;
1064 }
1065
1066 public function getSheets()
1067 {
1068 return $this->sheets;
1069 }
1070
1071 public function sheetsCount()
1072 {
1073 return count($this->sheets);
1074 }
1075
1076 public function sheetName($worksheetIndex)
1077 {
1078 $sn = $this->sheetNames();
1079 if (isset($sn[$worksheetIndex])) {
1080 return $sn[$worksheetIndex];
1081 }
1082
1083 return false;
1084 }
1085
1086 public function sheetNames()
1087 {
1088 $a = [];
1089 foreach ($this->sheetMetaData as $k => $v) {
1090 $a[$k] = $v['name'];
1091 }
1092 return $a;
1093 }
1094 public function sheetMeta($worksheetIndex = null)
1095 {
1096 if ($worksheetIndex === null) {
1097 return $this->sheetMetaData;
1098 }
1099 return isset($this->sheetMetaData[$worksheetIndex]) ? $this->sheetMetaData[$worksheetIndex] : false;
1100 }
1101 public function isHiddenSheet($worksheetIndex)
1102 {
1103 return isset($this->sheetMetaData[$worksheetIndex]['state'])
1104 && $this->sheetMetaData[$worksheetIndex]['state'] === 'hidden';
1105 }
1106
1107 public function getStyles()
1108 {
1109 return $this->styles;
1110 }
1111
1112 public function getPackage()
1113 {
1114 return $this->package;
1115 }
1116
1117 public function setDateTimeFormat($value)
1118 {
1119 $this->datetimeFormat = is_string($value) ? $value : false;
1120 }
1121
1122 public static function getTarget($base, $target)
1123 {
1124 $target = trim($target);
1125 if (strpos($target, '/') === 0) {
1126 return self::substr($target, 1);
1127 }
1128 $target = ($base ? $base . '/' : '') . $target;
1129 // a/b/../c -> a/c
1130 $parts = explode('/', $target);
1131 $abs = [];
1132 foreach ($parts as $p) {
1133 if ('.' === $p) {
1134 continue;
1135 }
1136 if ('..' === $p) {
1137 array_pop($abs);
1138 } else {
1139 $abs[] = $p;
1140 }
1141 }
1142 return implode('/', $abs);
1143 }
1144
1145 public static function parseRichText($is = null)
1146 {
1147 $value = [];
1148
1149 if (isset($is->t)) {
1150 $value[] = (string)$is->t;
1151 } elseif (isset($is->r)) {
1152 foreach ($is->r as $run) {
1153 $value[] = (string)$run->t;
1154 }
1155 }
1156
1157 return implode('', $value);
1158 }
1159
1160 public static function num2name($num)
1161 {
1162 $numeric = ($num - 1) % 26;
1163 $letter = chr(65 + $numeric);
1164 $num2 = (int)(($num - 1) / 26);
1165 if ($num2 > 0) {
1166 return self::num2name($num2) . $letter;
1167 }
1168 return $letter;
1169 }
1170
1171 public static function strlen($str)
1172 {
1173 return (ini_get('mbstring.func_overload') & 2) ? mb_strlen($str, '8bit') : strlen($str);
1174 }
1175
1176 public static function substr($str, $start, $length = null)
1177 {
1178 return (ini_get('mbstring.func_overload') & 2) ?
1179 mb_substr($str, $start, ($length === null) ? mb_strlen($str, '8bit') : $length, '8bit')
1180 : substr($str, $start, ($length === null) ? strlen($str) : $length);
1181 }
1182
1183 public static function strpos($haystack, $needle, $offset = 0)
1184 {
1185 return (ini_get('mbstring.func_overload') & 2) ?
1186 mb_strpos($haystack, $needle, $offset, '8bit') : strpos($haystack, $needle, $offset);
1187 }
1188 public static function boolean($value)
1189 {
1190 if (is_numeric($value)) {
1191 return (bool) $value;
1192 }
1193
1194 return $value === 'true' || $value === 'TRUE';
1195 }
1196 }
1197