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

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