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 +722 -568 1.14 → 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.24 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 - protected $sheets;
83 - protected $sheetNames = [];
84 - protected $sheetFiles = [];
83 + public $sheets;
84 + public $sheetFiles = [];
85 + public $sheetMetaData = [];
86 + public $sheetRels = [];
85 87 // scheme
86 - protected $styles;
87 - protected $hyperlinks;
88 + public $styles;
88 89 /* @var array[] $package */
89 - protected $package;
90 - protected $sharedstrings;
91 - protected $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 - protected $errno = 0;
133 - protected $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 - protected 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 - } 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'] ) {
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 - } elseif ( $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 - } elseif ( 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 - protected 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 - } 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 );
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 - } elseif ( $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 - } elseif ( 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,263 +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
527 +// $m1 = memory_get_usage();
507 528 // XML External Entity (XXE) Prevention, libxml_disable_entity_loader deprecated in PHP 8
508 - if ( LIBXML_VERSION < 20900 ) {
529 + if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) {
509 530 $_old = libxml_disable_entity_loader();
510 531 }
511 - $entry_xmlobj = simplexml_load_string( $entry_xml );
512 - if ( LIBXML_VERSION < 20900 ) {
532 +
533 + $_old_uie = libxml_use_internal_errors(true);
534 +
535 + $entry_xmlobj = simplexml_load_string($entry_xml, 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_PARSEHUGE);
536 +
537 + libxml_use_internal_errors($_old_uie);
538 +
539 + if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) {
513 540 /** @noinspection PhpUndefinedVariableInspection */
514 - libxml_disable_entity_loader( $_old );
541 + libxml_disable_entity_loader($_old);
515 542 }
516 - if ( $entry_xmlobj ) {
543 +
544 +// $m2 = memory_get_usage();
545 +// echo round( ($m2-$m1) / (1024 * 1024), 2).' MB'.PHP_EOL;
546 +
547 + if ($entry_xmlobj) {
517 548 return $entry_xmlobj;
518 549 }
519 550 $e = libxml_get_last_error();
520 - if ( $e ) {
521 - $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);
522 553 }
523 554 } else {
524 - $this->error( 4, 'XML-entry not found ' . $name );
555 + $this->error(4, 'XML-entry not found ' . $name);
525 556 }
526 557
527 558 return false;
528 559 }
529 560
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 ) {
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 +
536 607 return $entry['data'];
537 608 }
538 609 }
539 - $this->error( 5, 'Entry not found ' . ( $dir ? $dir . '/' : '' ) . $name );
610 + unset($entry);
611 + $this->error(5, 'Entry not found ' . ($dir ? $dir . '/' : '') . $name);
540 612
541 613 return false;
542 614 }
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 ) {
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]);
549 623 return true;
550 624 }
551 625 }
552 -
553 626 return false;
554 627 }
555 628
556 - public function success() {
557 - return ! $this->error;
629 + public static function strtoupper($str)
630 + {
631 + return (ini_get('mbstring.func_overload') & 2) ? mb_strtoupper($str, '8bit') : strtoupper($str);
558 632 }
559 633
560 - public function rows( $worksheetIndex = 0 ) {
634 + /*
635 + * @param string $name Filename in archive
636 + * @return SimpleXMLElement|bool
637 + */
561 638
562 - if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) {
563 - return false;
639 + public function entryExists($name)
640 + {
641 + // 0.6.6
642 + $dir = self::strtoupper(dirname($name));
643 + $name = self::strtoupper(basename($name));
644 + foreach ($this->package['entries'] as $entry) {
645 + if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
646 + return true;
647 + }
564 648 }
565 - $dim = $this->dimension( $worksheetIndex );
566 - $numCols = $dim[0];
567 - $numRows = $dim[1];
568 649
569 - $emptyRow = [];
570 - for ( $i = 0; $i < $numCols; $i ++ ) {
571 - $emptyRow[] = '';
572 - }
650 + return false;
651 + }
573 652
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;
653 + public static function parseFile($filename, $debug = false)
654 + {
655 + return self::parse($filename, false, $debug);
602 656 }
603 - // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
604 - public function rowsEx( $worksheetIndex = 0 ) {
605 657
606 - if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) {
607 - 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();
608 664 }
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 - }
665 + if ($xlsx->success()) {
666 + return $xlsx;
633 667 }
668 + self::parseError($xlsx->error());
669 + self::parseErrno($xlsx->errno());
634 670
635 - $curR = 0;
636 - /* @var SimpleXMLElement $ws */
637 - foreach ( $ws->sheetData->row as $row ) {
671 + return false;
672 + }
638 673
639 - $r_idx = (int) $row['r'];
640 - $curC = 0;
674 + public function success()
675 + {
676 + return !$this->error;
677 + }
641 678
642 - foreach ( $row->c as $c ) {
643 - $r = (string) $c['r'];
644 - $t = (string) $c['t'];
645 - $s = (int) $c['s'];
679 + // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
646 680
647 - $idx = $this->getIndex( $r );
648 - $x = $idx[0];
649 - $y = $idx[1];
681 + public static function parseError($set = false)
682 + {
683 + static $error = false;
650 684
651 - if ( $x > - 1 ) {
652 - $curC = $x;
653 - $curR = $y;
654 - }
685 + return $set ? $error = $set : $error;
686 + }
655 687
656 - if ( $s > 0 && isset( $this->cellFormats[ $s ] ) ) {
657 - $format = $this->cellFormats[ $s ]['format'];
658 - } else {
659 - $format = '';
660 - }
688 + public static function parseErrno($set = false)
689 + {
690 + static $errno = false;
661 691
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 - }
692 + return $set ? $errno = $set : $errno;
693 + }
675 694
676 - return $rows;
677 -
695 + public function errno()
696 + {
697 + return $this->errno;
678 698 }
679 699
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;
700 + public static function parseData($data, $debug = false)
701 + {
702 + return self::parse($data, true, $debug);
692 703 }
693 704
694 - public function worksheet( $worksheetIndex = 0 ) {
695 705
696 706
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;
707 + public function worksheet($worksheetIndex = 0)
708 + {
709 + if (isset($this->sheets[$worksheetIndex])) {
710 + return $this->sheets[$worksheetIndex];
736 711 }
737 - $this->error( 6, 'Worksheet not found ' . $worksheetIndex );
712 + $this->error(6, 'Worksheet not found ' . $worksheetIndex);
738 713
739 714 return false;
740 715 }
741 716
@@ -745,22 +720,23 @@
745 720 * @param int $worksheetIndex
746 721 *
747 722 * @return array
748 723 */
749 - public function dimension( $worksheetIndex = 0 ) {
724 + public function dimension($worksheetIndex = 0)
725 + {
750 726
751 - if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) {
752 - return [ 0, 0 ];
727 + if (($ws = $this->worksheet($worksheetIndex)) === false) {
728 + return [0, 0];
753 729 }
754 730 /* @var SimpleXMLElement $ws */
755 731
756 - $ref = (string) $ws->dimension['ref'];
732 + $ref = (string)$ws->dimension['ref'];
757 733
758 - if ( $this->_strpos( $ref, ':' ) !== false ) {
759 - $d = explode( ':', $ref );
760 - $idx = $this->getIndex( $d[1] );
734 + if (self::strpos($ref, ':') !== false) {
735 + $d = explode(':', $ref);
736 + $idx = $this->getIndex($d[1]);
761 737
762 - return [ $idx[0] + 1, $idx[1] + 1 ];
738 + return [$idx[0] + 1, $idx[1] + 1];
763 739 }
764 740 /*
765 741 if ( $ref !== '' ) { // 0.6.8
766 742 $index = $this->getIndex( $ref );
@@ -770,124 +746,141 @@
770 746 */
771 747
772 748 // slow method
773 749 $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 ) {
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) {
781 761 $maxC = $x;
782 762 }
783 - if ( $y > $maxR ) {
763 + if ($y > $maxR) {
784 764 $maxR = $y;
785 765 }
766 + } else {
767 + if ($iC > $maxC) {
768 + $maxC = $iC;
769 + }
770 + if ($iR > $maxR) {
771 + $maxR = $iR;
772 + }
786 773 }
787 774 }
788 775 }
789 776
790 - return [ $maxC + 1, $maxR + 1 ];
777 + return [$maxC + 1, $maxR + 1];
791 778 }
792 779
793 - public function getIndex( $cell = 'A1' ) {
780 + public function getIndex($cell = 'A1')
781 + {
782 + $m = null;
794 783
795 - if ( preg_match( '/([A-Z]+)(\d+)/', $cell, $m ) ) {
784 + if (preg_match('/([A-Z]+)(\d+)/', $cell, $m)) {
796 785 $col = $m[1];
797 786 $row = $m[2];
798 787
799 - $colLen = $this->_strlen( $col );
800 - $index = 0;
788 + $colLen = self::strlen($col);
789 + $index = 0;
801 790
802 - for ( $i = $colLen - 1; $i >= 0; $i -- ) {
803 - /** @noinspection PowerOperatorCanBeUsedInspection */
804 - $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);
805 793 }
806 794
807 - return [ $index - 1, $row - 1 ];
795 + return [$index - 1, $row - 1];
808 796 }
809 797
810 -// $this->error( 'Invalid cell index ' . $cell );
798 +// $this->error( 'Invalid cell index ' . $cell );
811 799
812 - return [ - 1, - 1 ];
800 + return [-1, -1];
813 801 }
814 802
815 - public function value( $cell ) {
803 + public function value($cell)
804 + {
816 805 // Determine data type
817 - $dataType = (string) $cell['t'];
806 + $dataType = (string)$cell['t'];
818 807
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';
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';
826 815 }
816 + } else {
817 + $dataType = 'n';
827 818 }
828 - else {
829 - $dataType = 's';
830 - }
831 819 }
832 820 }
833 821
834 822 $value = '';
835 823
836 - switch ( $dataType ) {
824 + switch ($dataType) {
837 825 case 's':
838 826 // Value is a shared string
839 - if ( (string) $cell->v !== '' ) {
840 - $value = $this->sharedstrings[ (int) $cell->v ];
827 + if ((string)$cell->v !== '') {
828 + $value = $this->sharedstrings[(int)$cell->v];
841 829 }
830 + break;
842 831
832 + case 'str': // formula?
833 + if ((string)$cell->v !== '') {
834 + $value = (string)$cell->v;
835 + }
843 836 break;
844 837
845 838 case 'b':
846 839 // 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 - }
840 + $value = self::boolean((string)$cell->v);
855 841
856 842 break;
857 843
858 844 case 'inlineStr':
859 845 // Value is rich text inline
860 - $value = $this->_parseRichText( $cell->is );
846 + $value = self::parseRichText($cell->is);
861 847
862 848 break;
863 849
864 850 case 'e':
865 851 // Value is an error message
866 - if ( (string) $cell->v !== '' ) {
867 - $value = (string) $cell->v;
852 + if ((string)$cell->v !== '') {
853 + $value = (string)$cell->v;
868 854 }
855 + break;
869 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 + }
870 863 break;
864 +
871 865 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;
866 + // Date as ISO YYYY-MM-DD
867 + if ((string)$cell->v !== '') {
868 + $value = (string)$cell->v;
875 869 }
876 870 break;
877 871
878 -
879 872 default:
880 873 // Value is a string
881 - $value = (string) $cell->v;
874 + $value = (string)$cell->v;
882 875
883 876 // Check for numeric values
884 - if ( is_numeric( $value ) && $dataType !== 's' ) {
877 + if (is_numeric($value)) {
885 878 /** @noinspection TypeUnsafeComparisonInspection */
886 - if ( $value == (int) $value ) {
887 - $value = (int) $value;
888 - } /** @noinspection TypeUnsafeComparisonInspection */ elseif ( $value == (float) $value ) {
889 - $value = (float) $value;
879 + if ($value == (int)$value) {
880 + $value = (int)$value;
881 + } /** @noinspection TypeUnsafeComparisonInspection */ elseif ($value == (float)$value) {
882 + $value = (float)$value;
890 883 }
891 884 }
892 885 }
893 886
@@ -893,23 +886,157 @@
893 886
894 887 return $value;
895 888 }
896 889
897 - public function unixstamp( $excelDateTime ) {
890 + public function unixstamp($excelDateTime)
891 + {
898 892
899 - $d = floor( $excelDateTime ); // days since 1900 or 1904
893 + $d = floor($excelDateTime); // days since 1900 or 1904
900 894 $t = $excelDateTime - $d;
901 895
902 - if ( $this->date1904 ) {
896 + if ($this->date1904) {
903 897 $d += 1462;
904 898 }
905 899
906 - $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);
907 901
908 - return (int) $t;
902 + return (int)$t;
909 903 }
910 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
911 955 /**
956 + * @param $worksheetIndex
957 + * @param $limit
958 + * @return \Generator
959 + */
960 + public function readRows($worksheetIndex = 0, $limit = 0)
961 + {
962 +
963 + if (($ws = $this->worksheet($worksheetIndex)) === false) {
964 + return;
965 + }
966 + $dim = $this->dimension($worksheetIndex);
967 + $numCols = $dim[0];
968 + $numRows = $dim[1];
969 +
970 + $emptyRow = [];
971 + for ($i = 0; $i < $numCols; $i++) {
972 + $emptyRow[] = '';
973 + }
974 +
975 + $curR = 0;
976 + $_limit = $limit;
977 + /* @var SimpleXMLElement $ws */
978 + foreach ($ws->sheetData->row as $row) {
979 + $r = $emptyRow;
980 + $curC = 0;
981 + foreach ($row->c as $c) {
982 + // detect skipped cols
983 + $idx = $this->getIndex((string)$c['r']);
984 + $x = $idx[0];
985 + $y = $idx[1];
986 +
987 + if ($x > -1) {
988 + $curC = $x;
989 + while ($curR < $y) {
990 + yield $emptyRow;
991 + $curR++;
992 + $_limit--;
993 + if ($_limit === 0) {
994 + return;
995 + }
996 + }
997 + }
998 + $r[$curC] = $this->value($c);
999 + $curC++;
1000 + }
1001 + yield $r;
1002 +
1003 + $curR++;
1004 + $_limit--;
1005 + if ($_limit === 0) {
1006 + return;
1007 + }
1008 + }
1009 + while ($curR < $numRows) {
1010 + yield $emptyRow;
1011 + $curR++;
1012 + $_limit--;
1013 + if ($_limit === 0) {
1014 + return;
1015 + }
1016 + }
1017 + }
1018 +
1019 + public function rowsEx($worksheetIndex = 0, $limit = 0)
1020 + {
1021 + return iterator_to_array($this->readRowsEx($worksheetIndex, $limit), false);
1022 + }
1023 + // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
1024 + /**
1025 + * @param $worksheetIndex
1026 + * @param $limit
1027 + * @return \Generator|null
1028 + */
1029 + public function readRowsEx($worksheetIndex = 0, $limit = 0)
1030 + {
1031 + if (!$this->rowsExReader) {
1032 + require_once __DIR__ . '/SimpleXLSXEx.php';
1033 + $this->rowsExReader = new SimpleXLSXEx($this);
1034 + }
1035 + return $this->rowsExReader->readRowsEx($worksheetIndex, $limit);
1036 + }
1037 +
1038 + /**
912 1039 * Returns cell value
913 1040 * VERY SLOW! Use ->rows() or ->rowsEx()
914 1041 *
915 1042 * @param int $worksheetIndex
@@ -916,20 +1043,21 @@
916 1043 * @param string|array $cell ref or coords, D12 or [3,12]
917 1044 *
918 1045 * @return mixed Returns NULL if not found
919 1046 */
920 - public function getCell( $worksheetIndex = 0, $cell = 'A1' ) {
1047 + public function getCell($worksheetIndex = 0, $cell = 'A1')
1048 + {
921 1049
922 - if ( ( $ws = $this->worksheet( $worksheetIndex ) ) === false ) {
1050 + if (($ws = $this->worksheet($worksheetIndex)) === false) {
923 1051 return false;
924 1052 }
925 - if ( is_array( $cell )) {
926 - $cell = $this->_num2name($cell[0]).$cell[1];// [3,21] -> D21
1053 + if (is_array($cell)) {
1054 + $cell = self::num2name($cell[0]) . $cell[1];// [3,21] -> D21
927 1055 }
928 - if ( is_string( $cell ) ) {
929 - $result = $ws->sheetData->xpath( "row/c[@r='" . $cell . "']" );
930 - if ( count($result) ) {
931 - return $this->value( $result[0] );
1056 + if (is_string($cell)) {
1057 + $result = $ws->sheetData->xpath("row/c[@r='" . $cell . "']");
1058 + if (count($result)) {
1059 + return $this->value($result[0]);
932 1060 }
933 1061 }
934 1062
935 1063 return null;
@@ -934,109 +1062,135 @@
934 1062
935 1063 return null;
936 1064 }
937 1065
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() {
1066 + public function getSheets()
1067 + {
944 1068 return $this->sheets;
945 1069 }
946 1070
947 - public function sheetsCount() {
948 - return count( $this->sheets );
1071 + public function sheetsCount()
1072 + {
1073 + return count($this->sheets);
949 1074 }
950 1075
951 - public function sheetName( $worksheetIndex ) {
952 - if ( isset( $this->sheetNames[ $worksheetIndex ] ) ) {
953 - return $this->sheetNames[ $worksheetIndex ];
1076 + public function sheetName($worksheetIndex)
1077 + {
1078 + $sn = $this->sheetNames();
1079 + if (isset($sn[$worksheetIndex])) {
1080 + return $sn[$worksheetIndex];
954 1081 }
955 1082
956 1083 return false;
957 1084 }
958 1085
959 - public function sheetNames() {
960 -
961 - return $this->sheetNames;
1086 + public function sheetNames()
1087 + {
1088 + $a = [];
1089 + foreach ($this->sheetMetaData as $k => $v) {
1090 + $a[$k] = $v['name'];
1091 + }
1092 + return $a;
962 1093 }
1094 + public function sheetMeta($worksheetIndex = null)
1095 + {
1096 + if ($worksheetIndex === null) {
1097 + return $this->sheetMetaData;
1098 + }
1099 + return isset($this->sheetMetaData[$worksheetIndex]) ? $this->sheetMetaData[$worksheetIndex] : false;
1100 + }
1101 + public function isHiddenSheet($worksheetIndex)
1102 + {
1103 + return isset($this->sheetMetaData[$worksheetIndex]['state'])
1104 + && $this->sheetMetaData[$worksheetIndex]['state'] === 'hidden';
1105 + }
963 1106
964 - // thx Gonzo
965 -
966 - public function getStyles() {
1107 + public function getStyles()
1108 + {
967 1109 return $this->styles;
968 1110 }
969 1111
970 - public function getPackage() {
1112 + public function getPackage()
1113 + {
971 1114 return $this->package;
972 1115 }
973 1116
974 - public function setDateTimeFormat( $value ) {
975 - $this->datetimeFormat = is_string( $value ) ? $value : false;
1117 + public function setDateTimeFormat($value)
1118 + {
1119 + $this->datetimeFormat = is_string($value) ? $value : false;
976 1120 }
977 - protected function _parseRichText( $is = null ) {
1121 +
1122 + public static function getTarget($base, $target)
1123 + {
1124 + $target = trim($target);
1125 + if (strpos($target, '/') === 0) {
1126 + return self::substr($target, 1);
1127 + }
1128 + $target = ($base ? $base . '/' : '') . $target;
1129 + // a/b/../c -> a/c
1130 + $parts = explode('/', $target);
1131 + $abs = [];
1132 + foreach ($parts as $p) {
1133 + if ('.' === $p) {
1134 + continue;
1135 + }
1136 + if ('..' === $p) {
1137 + array_pop($abs);
1138 + } else {
1139 + $abs[] = $p;
1140 + }
1141 + }
1142 + return implode('/', $abs);
1143 + }
1144 +
1145 + public static function parseRichText($is = null)
1146 + {
978 1147 $value = [];
979 1148
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;
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;
985 1154 }
986 1155 }
987 1156
988 - return implode( '', $value );
1157 + return implode('', $value);
989 1158 }
990 1159
991 - protected function _strlen( $str ) {
992 - return ( ini_get( 'mbstring.func_overload' ) & 2 ) ? mb_strlen( $str, '8bit' ) : strlen( $str );
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;
993 1169 }
994 1170
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 );
1171 + public static function strlen($str)
1172 + {
1173 + return (ini_get('mbstring.func_overload') & 2) ? mb_strlen($str, '8bit') : strlen($str);
997 1174 }
998 1175
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 );
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);
1005 1181 }
1006 1182
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 );
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);
1009 1187 }
1188 + public static function boolean($value)
1189 + {
1190 + if (is_numeric($value)) {
1191 + return (bool) $value;
1192 + }
1010 1193
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 );
1194 + return $value === 'true' || $value === 'TRUE';
1031 1195 }
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
1196 +}