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

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