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