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 +992 -781 1.9.2 → 3.4 View file →
@@ -1,128 +1,37 @@
1 1 <?php
2 2 /**
3 - * Excel 2007-2013 Reader Class
3 + * Excel 2007-2019/Office 365 Reader class
4 4 *
5 - * Based on SimpleXLSX v0.7.13 by Sergey Schuchkin.
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 - * @author Sergey Schuchkin, Tobias Bäthge
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
16 +use SimpleXMLElement;
17 +
17 18 /**
18 - * PHP Excel 2007-2013 Reader Class
19 + * PHP Excel 2007-2013/Office 365 Reader class
20 + *
19 21 * @package TablePress
20 22 * @subpackage Import
21 - * @author Sergey Schuchkin, Tobias Bäthge
23 + * @author Sergey Shuchkin, Tobias Bäthge
22 24 * @since 1.1.0
23 25 */
24 26 class SimpleXLSX {
25 -
26 - /**
27 - * XML Schema URLs.
28 - */
29 - const SCHEMA_REL_OFFICEDOCUMENT = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument';
30 - const SCHEMA_REL_SHAREDSTRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings';
31 - const SCHEMA_REL_WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
32 - const SCHEMA_REL_STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles';
33 -
34 - /**
35 - * [$workbook description]
36 - *
37 - * @since 1.1.0
38 - * @var [type]
39 - */
40 - protected $workbook;
41 -
42 - /**
43 - * [$sheets description]
44 - *
45 - * @since 1.1.0
46 - * @var array
47 - */
48 - protected $sheets = array();
49 -
50 - /**
51 - * [$sheetNames description]
52 - *
53 - * @since 1.9.1
54 - * @var array
55 - */
56 - protected $sheetNames = array();
57 -
58 - /**
59 - * [$styles description]
60 - *
61 - * @since 1.1.0
62 - * @var array
63 - */
64 - protected $styles = array();
65 -
66 - /**
67 - * [$hyperlinks description]
68 - *
69 - * @since 1.1.0
70 - * @var [type]
71 - */
72 - protected $hyperlinks;
73 -
74 - /**
75 - * [$package description]
76 - *
77 - * @since 1.1.0
78 - * @var array
79 - */
80 - protected $package = array(
81 - 'filename' => '',
82 - 'mtime' => 0,
83 - 'size' => 0,
84 - 'comment' => '',
85 - 'entries' => array(),
86 - );
87 -
88 - /**
89 - * [$sharedstrings description]
90 - *
91 - * @since 1.1.0
92 - * @var array
93 - */
94 - protected $sharedstrings = array();
95 -
96 - /**
97 - * [$error description]
98 - *
99 - * @since 1.1.0
100 - * @var string
101 - */
102 - protected $error = '';
103 -
104 - /**
105 - * [$workbook_cell_formats description]
106 - *
107 - * @since 1.1.0
108 - * @var array
109 - */
110 - public $workbook_cell_formats = array();
111 -
112 - /**
113 - * [$built_in_cell_formats description]
114 - *
115 - * @since 1.1.0
116 - * @var array
117 - */
118 - public static $built_in_cell_formats = array(
119 - 0 => 'General',
120 - 1 => '0',
121 - 2 => '0.00',
122 - 3 => '#,##0',
123 - 4 => '#,##0.00',
124 - 9 => '0%',
27 + public static $CF = [ // Cell formats
28 + 0 => 'General',
29 + 1 => '0',
30 + 2 => '0.00',
31 + 3 => '#,##0',
32 + 4 => '#,##0.00',
33 + 9 => '0%',
125 34 10 => '0.00%',
126 35 11 => '0.00E+00',
127 36 12 => '# ?/?',
128 37 13 => '# ??/??',
@@ -134,12 +43,14 @@
134 43 19 => 'h:mm:ss AM/PM',
135 44 20 => 'h:mm',
136 45 21 => 'h:mm:ss',
137 46 22 => 'm/d/yy h:mm',
47 +
138 48 37 => '#,##0 ;(#,##0)',
139 49 38 => '#,##0 ;[Red](#,##0)',
140 50 39 => '#,##0.00;(#,##0.00)',
141 51 40 => '#,##0.00;[Red](#,##0.00)',
52 +
142 53 44 => '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)',
143 54 45 => 'mm:ss',
144 55 46 => '[h]:mm:ss',
145 56 47 => 'mmss.0',
@@ -144,13 +55,15 @@
144 55 46 => '[h]:mm:ss',
145 56 47 => 'mmss.0',
146 57 48 => '##0.0E+0',
147 58 49 => '@',
59 +
148 60 27 => '[$-404]e/m/d',
149 61 30 => 'm/d/yy',
150 62 36 => '[$-404]e/m/d',
151 63 50 => '[$-404]e/m/d',
152 64 57 => '[$-404]e/m/d',
65 +
153 66 59 => 't0',
154 67 60 => 't0.00',
155 68 61 => 't#,##0',
156 69 62 => 't#,##0.00',
@@ -157,829 +70,1127 @@
157 70 67 => 't0%',
158 71 68 => 't0.00%',
159 72 69 => 't# ?/?',
160 73 70 => 't# ??/??',
161 - );
74 + ];
75 + public $nf = []; // number formats
76 + public $cellFormats = []; // cellXfs
77 + public $datetimeFormat = 'Y-m-d H:i:s';
78 + public $debug;
79 + public $activeSheet = 0;
80 + public $rowsExReader;
162 81
163 - /**
164 - * [$datetime_format description]
165 - *
166 - * @since 1.8.1
167 - * @var string
168 - */
169 - public $datetime_format = 'Y-m-d H:i:s';
82 + /* @var SimpleXMLElement[] $sheets */
83 + public $sheets;
84 + public $sheetFiles = [];
85 + public $sheetMetaData = [];
86 + public $sheetRels = [];
87 + // scheme
88 + public $styles;
89 + /* @var array[] $package */
90 + public $package;
91 + public $sharedstrings;
92 + public $date1904 = 0;
170 93
171 - /**
172 - * Constructor.
173 - *
174 - * @since 1.1.0
175 - *
176 - * @param string $filename [description]
177 - * @param bool $is_data Optional. [description]
178 - */
179 - public function __construct( $filename, $is_data = false ) {
180 - $this->datetime_format = get_option( 'date_format' );
181 - if ( $this->_unzip( $filename, $is_data ) ) {
182 - $this->_parse();
183 - }
184 - }
185 94
95 + /*
96 + private $date_formats = array(
97 + 0xe => "d/m/Y",
98 + 0xf => "d-M-Y",
99 + 0x10 => "d-M",
100 + 0x11 => "M-Y",
101 + 0x12 => "h:i a",
102 + 0x13 => "h:i:s a",
103 + 0x14 => "H:i",
104 + 0x15 => "H:i:s",
105 + 0x16 => "d/m/Y H:i",
106 + 0x2d => "i:s",
107 + 0x2e => "H:i:s",
108 + 0x2f => "i:s.S"
109 + );
110 + private $number_formats = array(
111 + 0x1 => "%1.0f", // "0"
112 + 0x2 => "%1.2f", // "0.00",
113 + 0x3 => "%1.0f", //"#,##0",
114 + 0x4 => "%1.2f", //"#,##0.00",
115 + 0x5 => "%1.0f", //"$#,##0;($#,##0)",
116 + 0x6 => '$%1.0f', //"$#,##0;($#,##0)",
117 + 0x7 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
118 + 0x8 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
119 + 0x9 => '%1.0f%%', //"0%"
120 + 0xa => '%1.2f%%', //"0.00%"
121 + 0xb => '%1.2f', //"0.00E00",
122 + 0x25 => '%1.0f', //"#,##0;(#,##0)",
123 + 0x26 => '%1.0f', //"#,##0;(#,##0)",
124 + 0x27 => '%1.2f', //"#,##0.00;(#,##0.00)",
125 + 0x28 => '%1.2f', //"#,##0.00;(#,##0.00)",
126 + 0x29 => '%1.0f', //"#,##0;(#,##0)",
127 + 0x2a => '$%1.0f', //"$#,##0;($#,##0)",
128 + 0x2b => '%1.2f', //"#,##0.00;(#,##0.00)",
129 + 0x2c => '$%1.2f', //"$#,##0.00;($#,##0.00)",
130 + 0x30 => '%1.0f'); //"##0.0E0";
131 + // }}}
132 + */
133 + public $errno = 0;
134 + public $error = false;
186 135 /**
187 - * [sheets description]
188 - *
189 - * @since 1.1.0
190 - *
191 - * @return [type] [description]
136 + * @var false|SimpleXMLElement
192 137 */
193 - public function sheets() {
194 - return $this->sheets;
195 - }
138 + public $theme;
196 139
197 - /**
198 - * [sheetsCount description]
199 - *
200 - * @since 1.1.0
201 - *
202 - * @return int Number of sheets.
203 - */
204 - public function sheetsCount() {
205 - return count( $this->sheets );
206 - }
207 140
208 - /**
209 - * [sheetName description]
210 - *
211 - * @since 1.1.0
212 - *
213 - * @param [type] $worksheet_index [description]
214 - * @return string|bool [description]
215 - */
216 - public function sheetName( $worksheet_index ) {
217 - if ( isset( $this->sheetNames[ $worksheet_index ] ) ) {
218 - return $this->sheetNames[ $worksheet_index ];
141 + public function __construct($filename = null, $is_data = null, $debug = null)
142 + {
143 + if ($debug !== null) {
144 + $this->debug = $debug;
219 145 }
220 - return false;
146 + $this->package = [
147 + 'filename' => '',
148 + 'mtime' => 0,
149 + 'size' => 0,
150 + 'comment' => '',
151 + 'entries' => []
152 + ];
153 + if ($filename && $this->unzip($filename, $is_data)) {
154 + $this->parseEntries();
155 + }
221 156 }
222 157
223 - /**
224 - * [sheetNames description]
225 - *
226 - * @since 1.1.0
227 - *
228 - * @return array [description]
229 - */
230 - public function sheetNames() {
231 - return $this->sheetNames;
232 - }
158 + public function unzip($filename, $is_data = false)
159 + {
233 160
234 - /**
235 - * [worksheet description]
236 - *
237 - * @since 1.1.0
238 - *
239 - * @param [type] $worksheet_index [description]. Optional.
240 - * @return [type] [description]
241 - */
242 - public function worksheet( $worksheet_index = 0 ) {
243 - if ( isset( $this->sheets[ $worksheet_index ] ) ) {
244 - $ws = $this->sheets[ $worksheet_index ];
245 - if ( isset( $ws->hyperlinks ) ) {
246 - $this->hyperlinks = array();
247 - foreach ( $ws->hyperlinks->hyperlink as $hyperlink ) {
248 - $this->hyperlinks[ (string) $hyperlink['ref'] ] = (string) $hyperlink['display'];
249 - }
161 + if ($is_data) {
162 + $this->package['filename'] = 'default.xlsx';
163 + $this->package['mtime'] = time();
164 + $this->package['size'] = self::strlen($filename);
165 +
166 + $vZ = $filename;
167 + } else {
168 + if (!is_readable($filename)) {
169 + $this->error(1, 'File not found ' . $filename);
170 +
171 + return false;
250 172 }
251 - return $ws;
252 - }
253 - $this->error( 'Worksheet ' . $worksheet_index . ' not found.' );
254 - return false;
255 - }
256 173
257 - /**
258 - * [dimension description]
259 - *
260 - * "Don't trust ->dimension(), so xlsx generators very lazy and don't publish a dimension attribute."
261 - *
262 - * @since 1.1.0
263 - *
264 - * @param int $worksheet_index Optional. [description]
265 - * @return array|false [description]
266 - */
267 - public function dimension( $worksheet_index = 0 ) {
268 - if ( false === ( $ws = $this->worksheet( $worksheet_index ) ) ) {
269 - return false;
270 - }
174 + // Package information
175 + $this->package['filename'] = $filename;
176 + $this->package['mtime'] = filemtime($filename);
177 + $this->package['size'] = filesize($filename);
271 178
272 - $ref = (string) $ws->dimension['ref'];
273 -
274 - if ( false !== strpos( $ref, ':' ) ) {
275 - $d = explode( ':', $ref );
276 - $index = $this->_columnIndex( $d[1] );
277 - return array( $index[0] + 1, $index[1] + 1 );
179 + // Read file
180 + $vZ = file_get_contents($filename);
278 181 }
182 + // Cut end of central directory
183 + /* $aE = explode("\x50\x4b\x05\x06", $vZ);
279 184
280 - if ( '' !== $ref ) {
281 - $index = $this->_columnIndex( $ref );
282 - return array( $index[0] + 1, $index[1] + 1 );
283 - }
185 + if (count($aE) == 1) {
186 + $this->error('Unknown format');
187 + return false;
188 + }
189 + */
190 + // Explode to each part
191 + $aE = explode("\x50\x4b\x03\x04", $vZ);
192 + array_shift($aE);
284 193
285 - return array( 0, 0 );
286 - }
194 + $aEL = count($aE);
195 + if ($aEL === 0) {
196 + $this->error(2, 'Unknown archive format');
287 197
288 - /**
289 - * [rows description]
290 - *
291 - * Sheets numeration: 1, 2, 3, ...
292 - *
293 - * @since 1.1.0
294 - *
295 - * @param int $worksheet_index Optional. [description]
296 - * @return array|bool [description]
297 - */
298 - public function rows( $worksheet_index = 0 ) {
299 - if ( false === ( $ws = $this->worksheet( $worksheet_index ) ) ) {
300 198 return false;
301 199 }
200 + // Search central directory end record
201 + $last = $aE[$aEL - 1];
202 + $last = explode("\x50\x4b\x05\x06", $last);
203 + if (count($last) !== 2) {
204 + $this->error(2, 'Unknown archive format');
302 205
303 - $rows = array();
304 - $current_row = 0;
305 -
306 - list( $cols, ) = $this->dimension( $worksheet_index );
307 -
308 - foreach ( $ws->sheetData->row as $row ) {
309 - $rows[ $current_row ] = array();
310 - foreach ( $row->c as $c ) {
311 - list( $current_cell, ) = $this->_columnIndex( (string) $c['r'] );
312 - $rows[ $current_row ][ $current_cell ] = $this->value( $c );
313 - }
314 - for ( $i = 0; $i < $cols; $i++ ) {
315 - if ( ! isset( $rows[ $current_row ][ $i ] ) ) {
316 - $rows[ $current_row ][ $i ] = '';
317 - }
318 - }
319 - ksort( $rows[ $current_row ] );
320 - $current_row++;
206 + return false;
321 207 }
322 - return $rows;
323 - }
208 + // Search central directory
209 + $last = explode("\x50\x4b\x01\x02", $last[0]);
210 + if (count($last) < 2) {
211 + $this->error(2, 'Unknown archive format');
324 212
325 - /**
326 - * [rowsEx description]
327 - *
328 - * @since 1.1.0
329 - *
330 - * @param int $worksheet_index Optional. [description]
331 - * @return array|bool [description]
332 - */
333 - public function rowsEx( $worksheet_index = 0 ) {
334 - if ( false === ( $ws = $this->worksheet( $worksheet_index ) ) ) {
335 213 return false;
336 214 }
215 + $aE[$aEL - 1] = $last[0];
337 216
338 - $rows = array();
339 - $current_row = 0;
340 - list( $cols, ) = $this->dimension( $worksheet_index );
217 + // Loop through the entries
218 + foreach ($aE as $vZ) {
219 + $aI = [];
220 + $aI['E'] = 0;
221 + $aI['EM'] = '';
222 + // Retrieving local file header information
223 +// $aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL', $vZ);
224 + $aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL/v1EFL', $vZ);
341 225
342 - foreach ( $ws->sheetData->row as $row ) {
343 - $r_idx = (int) $row['r'];
344 - foreach ( $row->c as $c ) {
345 - $r = (string) $c['r'];
346 - $t = (string) $c['t'];
347 - $s = (int) $c['s'];
348 - list( $current_cell, ) = $this->_columnIndex( $r );
349 - if ( $s > 0 && isset( $this->workbook_cell_formats[ $s ] ) ) {
350 - $format = $this->workbook_cell_formats[ $s ]['format'];
351 - if ( false !== strpos( $format, 'm' ) ) {
352 - $t = 'd';
226 + // Check if data is encrypted
227 +// $bE = ($aP['GPF'] && 0x0001) ? TRUE : FALSE;
228 +// $bE = false;
229 + $nF = $aP['FNL'];
230 + $mF = $aP['EFL'];
231 +
232 + // Special case : value block after the compressed data
233 + if ($aP['GPF'] & 0x0008) {
234 + // Signed ZIP64 descriptor:
235 + // signature + CRC32 + 64-bit compressed size + 64-bit uncompressed size
236 + if (self::substr($vZ, -24, 4) === "\x50\x4b\x07\x08") {
237 + $aP1 = unpack(
238 + 'V1CRC/V1CSLow/V1CSHigh/V1UCSLow/V1UCSHigh',
239 + self::substr($vZ, -20)
240 + );
241 +
242 + if ((int)$aP1['CSHigh'] !== 0 || (int)$aP1['UCSHigh'] !== 0) {
243 + $aI['E'] = 6;
244 + $aI['EM'] = 'ZIP64 entry is too large to process.';
353 245 }
246 +
247 + $aP['CRC'] = $aP1['CRC'];
248 + $aP['CS'] = $aP1['CSLow'];
249 + $aP['UCS'] = $aP1['UCSLow'];
250 + $vZ = self::substr($vZ, 0, -24);
354 251 } else {
355 - $format = '';
356 - }
252 + $aP1 = unpack('V1CRC/V1CS/V1UCS', self::substr($vZ, -12));
357 253
358 - $rows[ $current_row ][ $current_cell ] = array(
359 - 'type' => $t,
360 - 'name' => $r,
361 - 'value' => $this->value( $c, $format ),
362 - 'href' => $this->href( $c ),
363 - 'f' => (string) $c->f,
364 - 'format' => $format,
365 - 'r' => $r_idx,
366 - );
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 + }
367 263 }
368 - for ( $i = 0; $i < $cols; $i++ ) {
369 - if ( ! isset( $rows[ $current_row ][ $i ] ) ) {
370 - for ( $c = '', $j = $i; $j >= 0; $j = (int) ( $j / 26 ) - 1 ) {
371 - $c = chr( $j % 26 + 65 ) . $c;
372 - }
373 264
374 - $rows[ $current_row ][ $i ] = array(
375 - 'type' => '',
376 - // 'name' => chr( $i + 65 ) . ( $current_row + 1 ),
377 - 'name' => $c . ( $current_row + 1 ),
378 - 'value' => '',
379 - 'href' => '',
380 - 'f' => '',
381 - 'format' => '',
382 - 'r' => $r_idx,
383 - );
384 - }
265 + // Getting stored filename
266 + $aI['N'] = self::substr($vZ, 26, $nF);
267 + $aI['N'] = str_replace('\\', '/', $aI['N']);
268 +
269 + if (self::substr($aI['N'], -1) === '/') {
270 + // is a directory entry - will be skipped
271 + continue;
385 272 }
386 - ksort( $rows[ $current_row ] );
387 - $current_row++;
388 - }
389 - return $rows;
390 - }
391 273
392 - /**
393 - * [_columnIndex description]
394 - *
395 - * @since 1.1.0
396 - *
397 - * @param string $cell Optional. [description]
398 - * @return array [description]
399 - */
400 - protected function _columnIndex( $cell = 'A1' ) {
401 - if ( preg_match( '/([A-Z]+)(\d+)/', $cell, $m ) ) {
402 - list( , $col, $row ) = $m;
274 + // Truncate full filename in path and filename
275 + $aI['P'] = dirname($aI['N']);
276 + $aI['P'] = ($aI['P'] === '.') ? '' : $aI['P'];
277 + $aI['N'] = basename($aI['N']);
403 278
404 - $colLen = strlen( $col );
405 - $index = 0;
279 + $vZ = self::substr($vZ, 26 + $nF + $mF);
406 280
407 - for ( $i = $colLen - 1; $i >= 0; $i-- ) {
408 - $index += ( ord( $col[ $i ] ) - 64 ) * pow( 26, $colLen - $i - 1 );
281 + if ($aP['CS'] > 0 && (self::strlen($vZ) !== (int)$aP['CS'])) { // check only if availabled
282 + $aI['E'] = 1;
283 + $aI['EM'] = 'Compressed size is not equal with the value in header information.';
409 284 }
285 +// } elseif ( $bE ) {
286 +// $aI['E'] = 5;
287 +// $aI['EM'] = 'File is encrypted, which is not supported from this class.';
288 +/* } else {
289 + switch ($aP['CM']) {
290 + case 0: // Stored
291 + // Here is nothing to do, the file ist flat.
292 + break;
293 + case 8: // Deflated
294 + $vZ = gzinflate($vZ);
295 + break;
296 + case 12: // BZIP2
297 + if (extension_loaded('bz2')) {
298 + $vZ = bzdecompress($vZ);
299 + } else {
300 + $aI['E'] = 7;
301 + $aI['EM'] = 'PHP BZIP2 extension not available.';
302 + }
303 + break;
304 + default:
305 + $aI['E'] = 6;
306 + $aI['EM'] = "De-/Compression method {$aP['CM']} is not supported.";
307 + }
308 + if (!$aI['E']) {
309 + if ($vZ === false) {
310 + $aI['E'] = 2;
311 + $aI['EM'] = 'Decompression of data failed.';
312 + } elseif ($this->_strlen($vZ) !== (int)$aP['UCS']) {
313 + $aI['E'] = 3;
314 + $aI['EM'] = 'Uncompressed size is not equal with the value in header information.';
315 + } elseif (crc32($vZ) !== $aP['CRC']) {
316 + $aI['E'] = 4;
317 + $aI['EM'] = 'CRC32 checksum is not equal with the value in header information.';
318 + }
319 + }
320 + }
321 +*/
410 322
411 - return array( $index - 1, $row - 1 );
412 - }
413 - $this->error( 'Invalid cell index ' . $cell );
323 + // DOS to UNIX timestamp
324 + $aI['T'] = mktime(
325 + ($aP['FT'] & 0xf800) >> 11,
326 + ($aP['FT'] & 0x07e0) >> 5,
327 + ($aP['FT'] & 0x001f) << 1,
328 + ($aP['FD'] & 0x01e0) >> 5,
329 + $aP['FD'] & 0x001f,
330 + (($aP['FD'] & 0xfe00) >> 9) + 1980
331 + );
414 332
415 - return false;
333 + $this->package['entries'][] = [
334 + 'data' => $vZ,
335 + 'ucs' => (int)$aP['UCS'], // ucompresses size
336 + 'cm' => $aP['CM'], // compressed method
337 + 'cs' => isset($aP['CS']) ? (int) $aP['CS'] : 0, // compresses size
338 + 'crc' => $aP['CRC'],
339 + 'error' => $aI['E'],
340 + 'error_msg' => $aI['EM'],
341 + 'name' => $aI['N'],
342 + 'path' => $aI['P'],
343 + 'time' => $aI['T']
344 + ];
345 + } // end for each entries
346 +
347 + return true;
416 348 }
417 349
418 - /**
419 - * [value description]
420 - *
421 - * @since 1.1.0
422 - *
423 - * @param [type] $cell [description]
424 - * @param [type] $format [description]
425 - * @return mixed [description]
426 - */
427 - public function value( $cell, $format = null ) {
428 - // Determine data type.
429 - $dataType = (string) $cell['t'];
430 350
431 - if ( null === $format ) {
432 - $s = (int) $cell['s'];
433 - if ( $s > 0 && isset( $this->workbook_cell_formats[ $s ] ) ) {
434 - $format = $this->workbook_cell_formats[ $s ]['format'];
351 + public function error($num = null, $str = null)
352 + {
353 + if ($num) {
354 + $this->errno = $num;
355 + $this->error = $str;
356 + if ($this->debug) {
357 + trigger_error(__CLASS__ . ': ' . $this->error, E_USER_WARNING);
435 358 }
436 359 }
437 - if ( false !== strpos( $format, 'm' ) ) {
438 - $dataType = 'd';
439 - }
440 - $value = '';
441 - switch ( $dataType ) {
442 - case 's':
443 - // Value is a shared string.
444 - if ( '' !== (string) $cell->v ) {
445 - $value = $this->sharedstrings[ (int) $cell->v ];
446 - }
447 - break;
448 - case 'b':
449 - // Value is boolean.
450 - $value = (string) $cell->v;
451 - if ( '0' === $value ) {
452 - $value = false;
453 - } elseif ( '1' === $value ) {
454 - $value = true;
455 - } else {
456 - $value = (bool) $cell->v;
457 - }
458 - break;
459 - case 'inlineStr':
460 - // Value is rich text inline.
461 - $value = $this->_parseRichText( $cell->is );
462 - break;
463 - case 'e':
464 - // Value is an error message.
465 - if ( '' !== (string) $cell->v ) {
466 - $value = (string) $cell->v;
467 - }
468 - break;
469 - case 'd':
470 - // Value is a date.
471 - $value = $this->datetime_format ? gmdate( $this->datetime_format, $this->unixstamp( (float) $cell->v ) ) : (float) $cell->v;
472 - break;
473 - default:
474 - // Value is a string.
475 - $value = (string) $cell->v;
476 - // Check for numeric values by converting them forth and back.
477 - if ( is_numeric( $value ) && 's' !== $dataType ) {
478 - if ( $value == (int) $value ) {
479 - $value = (int) $value;
480 - } elseif ( $value == (float) $value ) {
481 - $value = (float) $value;
360 +
361 + return $this->error;
362 + }
363 +
364 + public function parseEntries()
365 + {
366 + // Document data holders
367 + $this->sharedstrings = [];
368 + $this->sheets = [];
369 +// $this->styles = array();
370 +// $m1 = 0; // memory_get_peak_usage( true );
371 + // Read relations and search for officeDocument
372 + if ($relations = $this->getEntryXML('_rels/.rels')) {
373 + foreach ($relations->Relationship as $rel) {
374 + $rel_type = basename(trim((string)$rel['Type'])); // officeDocument
375 + $rel_target = self::getTarget('', (string)$rel['Target']); // /xl/workbook.xml or xl/workbook.xml
376 +
377 + if ($rel_type === 'officeDocument'
378 + && $workbook = $this->getEntryXML($rel_target)
379 + ) {
380 + $index_rId = []; // [0 => rId1]
381 +
382 + $index = 0;
383 + foreach ($workbook->sheets->sheet as $s) {
384 + $a = [];
385 + foreach ($s->attributes() as $k => $v) {
386 + $a[(string)$k] = (string)$v;
387 + }
388 + $this->sheetMetaData[$index] = $a;
389 + $index_rId[$index] = (string)$s['id'];
390 + $index++;
482 391 }
483 - }
484 - }
485 - return $value;
486 - }
392 + if ((int)$workbook->workbookPr['date1904'] === 1) {
393 + $this->date1904 = 1;
394 + }
487 395
488 - /**
489 - * [href description]
490 - *
491 - * @since 1.1.0
492 - *
493 - * @param [type] $cell [description]
494 - * @return string [description]
495 - */
496 - public function href( $cell ) {
497 - return isset( $this->hyperlinks[ (string) $cell['r'] ] ) ? $this->hyperlinks[ (string) $cell['r'] ] : '';
498 - }
499 396
500 - /**
501 - * [getStyles description]
502 - *
503 - * @since 1.8.1
504 - *
505 - * @return [type] [description]
506 - */
507 - public function getStyles() {
508 - return $this->styles;
509 - }
397 + if ($workbookRelations = $this->getEntryXML(dirname($rel_target) . '/_rels/workbook.xml.rels')) {
398 + // Loop relations for workbook and extract sheets...
399 + foreach ($workbookRelations->Relationship as $workbookRelation) {
400 + $wrel_type = basename(trim((string)$workbookRelation['Type'])); // worksheet
401 + $wrel_target = self::getTarget(dirname($rel_target), (string)$workbookRelation['Target']);
402 + if (!$this->entryExists($wrel_target)) {
403 + continue;
404 + }
510 405
511 - /**
512 - * [_unzip description]
513 - *
514 - * @since 1.1.0
515 - *
516 - * @param [type] $filename [description]
517 - * @param bool $is_data Optional. [description]
518 - * @return [type] [description]
519 - */
520 - protected function _unzip( $filename, $is_data = false ) {
521 - if ( $is_data ) {
522 - $this->package['filename'] = 'default.xlsx';
523 - $this->package['mtime'] = time();
524 - $this->package['size'] = strlen( $filename );
525 - $vZ = $filename;
526 - } else {
527 - if ( ! is_readable( $filename ) ) {
528 - $this->error( 'File not found ' . $filename );
529 - return false;
406 + if ($wrel_type === 'worksheet') { // Sheets
407 + if ($sheet = $this->getEntryXML($wrel_target)) {
408 + $index = array_search((string)$workbookRelation['Id'], $index_rId, true);
409 + $this->sheets[$index] = $sheet;
410 + $this->sheetFiles[$index] = $wrel_target;
411 + $srel_d = dirname($wrel_target);
412 + $srel_f = basename($wrel_target);
413 + $srel_file = $srel_d . '/_rels/' . $srel_f . '.rels';
414 + if ($this->entryExists($srel_file)) {
415 + $this->sheetRels[$index] = $this->getEntryXML($srel_file);
416 + }
417 + }
418 + } elseif ($wrel_type === 'sharedStrings') {
419 + if ($sharedStrings = $this->getEntryXML($wrel_target)) {
420 + foreach ($sharedStrings->si as $val) {
421 + if (isset($val->t)) {
422 + $this->sharedstrings[] = (string)$val->t;
423 + } elseif (isset($val->r)) {
424 + $this->sharedstrings[] = self::parseRichText($val);
425 + }
426 + }
427 + }
428 + } elseif ($wrel_type === 'styles') {
429 + $this->styles = $this->getEntryXML($wrel_target);
430 +
431 + // number formats
432 + $this->nf = [];
433 + if (isset($this->styles->numFmts->numFmt)) {
434 + foreach ($this->styles->numFmts->numFmt as $v) {
435 + $this->nf[(int)$v['numFmtId']] = (string)$v['formatCode'];
436 + }
437 + }
438 +
439 + $this->cellFormats = [];
440 + if (isset($this->styles->cellXfs->xf)) {
441 + foreach ($this->styles->cellXfs->xf as $v) {
442 + $x = [
443 + 'format' => null
444 + ];
445 + foreach ($v->attributes() as $k1 => $v1) {
446 + $x[ $k1 ] = (int) $v1;
447 + }
448 + if (isset($x['numFmtId'])) {
449 + if (isset($this->nf[$x['numFmtId']])) {
450 + $x['format'] = $this->nf[$x['numFmtId']];
451 + } elseif (isset(self::$CF[$x['numFmtId']])) {
452 + $x['format'] = self::$CF[$x['numFmtId']];
453 + }
454 + }
455 +
456 + $this->cellFormats[] = $x;
457 + }
458 + }
459 + } elseif ($wrel_type === 'theme') {
460 + $this->theme = $this->getEntryXML($wrel_target);
461 + }
462 + }
463 +
464 +// break;
465 + }
466 + // reptile hack :: find active sheet from workbook.xml
467 + if ($workbook->bookViews->workbookView) {
468 + foreach ($workbook->bookViews->workbookView as $v) {
469 + if (!empty($v['activeTab'])) {
470 + $this->activeSheet = (int)$v['activeTab'];
471 + }
472 + }
473 + }
474 +
475 + break;
476 + }
530 477 }
478 + }
531 479
532 - // Package information.
533 - $this->package['filename'] = $filename;
534 - $this->package['mtime'] = filemtime( $filename );
535 - $this->package['size'] = filesize( $filename );
480 +// $m2 = memory_get_peak_usage(true);
481 +// echo __FUNCTION__.' M='.round( ($m2-$m1) / 1048576, 2).'MB'.PHP_EOL;
536 482
537 - // Read file.
538 - $vZ = file_get_contents( $filename );
483 + if (count($this->sheets)) {
484 + // Sort sheets
485 + ksort($this->sheets);
486 +
487 + return true;
539 488 }
540 489
541 - /*
542 - // Cut end of central directory
543 - $aE = explode( "\x50\x4b\x05\x06", $vZ );
544 - if ( 1 === count( $aE ) ) {
545 - $this->error( 'Unknown format' );
546 - return false;
547 - }
548 - */
490 + return false;
491 + }
549 492
550 - if ( false === ( $pcd = strrpos( $vZ, "\x50\x4b\x05\x06" ) ) ) {
551 - $this->error( 'Unknown archive format' );
552 - return false;
553 - }
554 - $aE = array(
555 - 0 => substr( $vZ, 0, $pcd ),
556 - 1 => substr( $vZ, $pcd + 3 ),
557 - );
493 + public function getEntryXML($name)
494 + {
495 + if ($entry_xml = $this->getEntryData($name)) {
496 + $this->deleteEntry($name); // economy memory
497 + // dirty remove namespace prefixes and empty rows
498 + $entry_xml = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $entry_xml); // remove namespaces
499 + $entry_xml .= ' '; // force run garbage collector
500 + // remove namespaced attrs
501 + $entry_xml = preg_replace('/[a-zA-Z0-9]+:([a-zA-Z0-9]+="[^"]+")/', '$1', $entry_xml);
502 + $entry_xml .= ' ';
503 + $entry_xml = preg_replace('/<[a-zA-Z0-9]+:([^>]+)>/', '<$1>', $entry_xml); // fix namespaced openned tags
504 + $entry_xml .= ' ';
505 + $entry_xml = preg_replace('/<\/[a-zA-Z0-9]+:([^>]+)>/', '</$1>', $entry_xml); // fix namespaced closed tags
506 + $entry_xml .= ' ';
558 507
559 - // Normal way.
560 - $aP = unpack( 'x16/v1CL', $aE[1] );
561 - $this->package['comment'] = substr( $aE[1], 18, $aP['CL'] );
508 + if (strpos($name, '/sheet')) { // dirty skip empty rows
509 + // remove <row...> <c /><c /></row>
510 + $cnt = $cnt2 = $cnt3 = null;
511 + $entry_xml = preg_replace('/<row[^>]+>\s*(<c[^\/]+\/>\s*)+<\/row>/', '', $entry_xml, -1, $cnt);
512 + $entry_xml .= ' ';
513 + // remove <row />
514 + $entry_xml = preg_replace('/<row[^\/>]*\/>/', '', $entry_xml, -1, $cnt2);
515 + $entry_xml .= ' ';
516 + // remove <row...></row>
517 + $entry_xml = preg_replace('/<row[^>]*><\/row>/', '', $entry_xml, -1, $cnt3);
518 + $entry_xml .= ' ';
519 + if ($cnt || $cnt2 || $cnt3) {
520 + $entry_xml = preg_replace('/<dimension[^\/]+\/>/', '', $entry_xml);
521 + $entry_xml .= ' ';
522 + }
523 +// file_put_contents( basename( $name ), $entry_xml ); // @to do comment!!!
524 + }
525 + $entry_xml = trim($entry_xml);
562 526
563 - // Translates end of line from other operating systems.
564 - $this->package['comment'] = str_replace( array( "\r\n", "\r" ), "\n", $this->package['comment'] );
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 + }
565 532
566 - // Cut the entries from the central directory.
567 - $aE = explode( "\x50\x4b\x01\x02", $vZ );
568 - // Explode to each part.
569 - $aE = explode( "\x50\x4b\x03\x04", $aE[0] );
570 - // Shift out spanning signature or empty entry.
571 - array_shift( $aE );
533 + $_old_uie = libxml_use_internal_errors(true);
572 534
573 - // Loop through the entries.
574 - foreach ( $aE as $vZ ) {
575 - $aI = array();
576 - $aI['E'] = 0;
577 - $aI['EM'] = '';
578 - // Retrieving local file header information.
579 - // $aP = unpack( 'v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL', $vZ );
580 - $aP = unpack( 'v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL/v1EFL', $vZ );
535 + $entry_xmlobj = simplexml_load_string($entry_xml, 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_PARSEHUGE);
581 536
582 - // Check if data is encrypted.
583 - // $bE = ( $aP['GPF'] && 0x0001 ) ? true : false;
584 - $bE = false;
585 - $nF = $aP['FNL'];
586 - $mF = $aP['EFL'];
537 + libxml_use_internal_errors($_old_uie);
587 538
588 - // Special case: value block after the compressed data.
589 - if ( $aP['GPF'] & 0x0008 ) {
590 - $aP1 = unpack( 'V1CRC/V1CS/V1UCS', substr( $vZ, -12 ) );
591 - $aP['CRC'] = $aP1['CRC'];
592 - $aP['CS'] = $aP1['CS'];
593 - $aP['UCS'] = $aP1['UCS'];
594 - // 2013-08-10
595 - $vZ = substr( $vZ, 0, -12 );
596 - if ( "\x50\x4b\x07\x08" === substr( $vZ, -4 ) ) {
597 - $vZ = substr( $vZ, 0, -4 );
598 - }
539 + if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) {
540 + /** @noinspection PhpUndefinedVariableInspection */
541 + libxml_disable_entity_loader($_old);
599 542 }
600 543
601 - // Get stored filename.
602 - $aI['N'] = substr( $vZ, 26, $nF );
544 +// $m2 = memory_get_usage();
545 +// echo round( ($m2-$m1) / (1024 * 1024), 2).' MB'.PHP_EOL;
603 546
604 - // If it's a directory entry, it will be skipped.
605 - if ( '/' === substr( $aI['N'], -1 ) ) {
606 - continue;
547 + if ($entry_xmlobj) {
548 + return $entry_xmlobj;
607 549 }
550 + $e = libxml_get_last_error();
551 + if ($e) {
552 + $this->error(3, 'XML-entry ' . $name . ' parser error ' . $e->message . ' line ' . $e->line);
553 + }
554 + } else {
555 + $this->error(4, 'XML-entry not found ' . $name);
556 + }
608 557
609 - // Truncate full filename in path and filename.
610 - $aI['P'] = dirname( $aI['N'] );
611 - $aI['P'] = ( '.' === $aI['P'] ) ? '' : $aI['P'];
612 - $aI['N'] = basename( $aI['N'] );
558 + return false;
559 + }
613 560
614 - $vZ = substr( $vZ, 26 + $nF + $mF );
561 + // sheets numeration: 1,2,3....
615 562
616 - if ( strlen( $vZ ) !== (int) $aP['CS'] ) { // Check only if available.
617 - $aI['E'] = 1;
618 - $aI['EM'] = 'Compressed size is not equal with the value in header information.';
619 - } elseif ( $bE ) {
620 - $aI['E'] = 5;
621 - $aI['EM'] = 'File is encrypted, which is not supported by this class.';
622 - } else {
623 - switch ( $aP['CM'] ) {
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:
624 575 case 0: // Stored
625 - // Here is nothing to do, the file is flat.
576 + // Here is nothing to do, the file ist flat.
626 577 break;
627 578 case 8: // Deflated
628 - $vZ = gzinflate( $vZ );
579 + $entry['data'] = gzinflate($entry['data']);
629 580 break;
630 581 case 12: // BZIP2
631 - if ( extension_loaded( 'bz2' ) ) {
632 - $vZ = bzdecompress( $vZ );
582 + if (extension_loaded('bz2')) {
583 + $entry['data'] = bzdecompress($entry['data']);
633 584 } else {
634 - $aI['E'] = 7;
635 - $aI['EM'] = 'PHP BZIP2 extension not available.';
585 + $entry['error'] = 7;
586 + $entry['error_message'] = 'PHP BZIP2 extension not available.';
636 587 }
637 588 break;
638 589 default:
639 - $aI['E'] = 6;
640 - $aI['EM'] = "De-/Compression method {$aP['CM']} is not supported.";
590 + $entry['error'] = 6;
591 + $entry['error_msg'] = 'De-/Compression method '.$entry['cm'].' is not supported.';
641 592 }
642 - if ( ! $aI['E'] ) {
643 - if ( false === $vZ ) {
644 - $aI['E'] = 2;
645 - $aI['EM'] = 'Decompression of data failed.';
646 - } elseif ( strlen( $vZ ) !== (int) $aP['UCS'] ) {
647 - $aI['E'] = 3;
648 - $aI['EM'] = 'Uncompressed size is not equal with the value in header information.';
649 - } elseif ( crc32( $vZ ) !== $aP['CRC'] ) {
650 - $aI['E'] = 4;
651 - $aI['EM'] = 'CRC32 checksum is not equal with the value in header information.';
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.';
652 604 }
653 605 }
606 +
607 + return $entry['data'];
654 608 }
609 + }
610 + unset($entry);
611 + $this->error(5, 'Entry not found ' . ($dir ? $dir . '/' : '') . $name);
655 612
656 - $aI['D'] = $vZ;
613 + return false;
614 + }
615 + public function deleteEntry($name)
616 + {
617 + $name = ltrim(str_replace('\\', '/', $name), '/');
618 + $dir = self::strtoupper(dirname($name));
619 + $name = self::strtoupper(basename($name));
620 + foreach ($this->package['entries'] as $k => $entry) {
621 + if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
622 + unset($this->package['entries'][$k]);
623 + return true;
624 + }
625 + }
626 + return false;
627 + }
657 628
658 - // DOS to UNIX timestamp.
659 - $aI['T'] = mktime(
660 - ( $aP['FT'] & 0xf800 ) >> 11,
661 - ( $aP['FT'] & 0x07e0 ) >> 5,
662 - ( $aP['FT'] & 0x001f ) << 1,
663 - ( $aP['FD'] & 0x01e0 ) >> 5,
664 - ( $aP['FD'] & 0x001f ),
665 - ( ( $aP['FD'] & 0xfe00 ) >> 9 ) + 1980
666 - );
629 + public static function strtoupper($str)
630 + {
631 + return (ini_get('mbstring.func_overload') & 2) ? mb_strtoupper($str, '8bit') : strtoupper($str);
632 + }
667 633
668 - // $this->Entries[] = new SimpleUnzipEntry( $aI );
669 - $this->package['entries'][] = array(
670 - 'data' => $aI['D'],
671 - 'error' => $aI['E'],
672 - 'error_msg' => $aI['EM'],
673 - 'name' => $aI['N'],
674 - 'path' => $aI['P'],
675 - 'time' => $aI['T'],
676 - );
634 + /*
635 + * @param string $name Filename in archive
636 + * @return SimpleXMLElement|bool
637 + */
677 638
678 - } // end foreach entries
639 + public function entryExists($name)
640 + {
641 + // 0.6.6
642 + $dir = self::strtoupper(dirname($name));
643 + $name = self::strtoupper(basename($name));
644 + foreach ($this->package['entries'] as $entry) {
645 + if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) {
646 + return true;
647 + }
648 + }
679 649
680 - return true;
650 + return false;
681 651 }
682 652
683 - /**
684 - * [getPackage description]
685 - *
686 - * @since 1.1.0
687 - *
688 - * @return [type] [description]
689 - */
690 - public function getPackage() {
691 - return $this->package;
653 + public static function parseFile($filename, $debug = false)
654 + {
655 + return self::parse($filename, false, $debug);
692 656 }
693 657
694 - /**
695 - * [entryExists description]
696 - *
697 - * @since 1.1.0
698 - *
699 - * @param [type] $name [description]
700 - * @return bool [description]
701 - */
702 - public function entryExists( $name ) {
703 - $dir = strtoupper( dirname( $name ) );
704 - $name = strtoupper( basename( $name ) );
705 - foreach ( $this->package['entries'] as $entry ) {
706 - if ( strtoupper( $entry['path'] ) === $dir && strtoupper( $entry['name'] ) === $name ) {
707 - return true;
708 - }
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();
709 664 }
665 + if ($xlsx->success()) {
666 + return $xlsx;
667 + }
668 + self::parseError($xlsx->error());
669 + self::parseErrno($xlsx->errno());
670 +
710 671 return false;
711 672 }
712 673
713 - /**
714 - * [getEntryData description]
715 - *
716 - * @since 1.1.0
717 - *
718 - * @param [type] $name [description]
719 - * @return [type] [description]
720 - */
721 - public function getEntryData( $name ) {
722 - $dir = strtoupper( dirname( $name ) );
723 - $name = strtoupper( basename( $name ) );
724 - foreach ( $this->package['entries'] as $entry ) {
725 - if ( strtoupper( $entry['path'] ) === $dir && strtoupper( $entry['name'] ) === $name ) {
726 - return $entry['data'];
727 - }
674 + public function success()
675 + {
676 + return !$this->error;
677 + }
678 +
679 + // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex
680 +
681 + public static function parseError($set = false)
682 + {
683 + static $error = false;
684 +
685 + return $set ? $error = $set : $error;
686 + }
687 +
688 + public static function parseErrno($set = false)
689 + {
690 + static $errno = false;
691 +
692 + return $set ? $errno = $set : $errno;
693 + }
694 +
695 + public function errno()
696 + {
697 + return $this->errno;
698 + }
699 +
700 + public static function parseData($data, $debug = false)
701 + {
702 + return self::parse($data, true, $debug);
703 + }
704 +
705 +
706 +
707 + public function worksheet($worksheetIndex = 0)
708 + {
709 + if (isset($this->sheets[$worksheetIndex])) {
710 + return $this->sheets[$worksheetIndex];
728 711 }
729 - $this->error( 'Entry not found: ' . $name );
712 + $this->error(6, 'Worksheet not found ' . $worksheetIndex);
713 +
730 714 return false;
731 715 }
732 716
733 717 /**
734 - * [getEntryXML description]
718 + * returns [numCols,numRows] of worksheet
735 719 *
736 - * @since 1.1.0
720 + * @param int $worksheetIndex
737 721 *
738 - * @param [type] $name [description]
739 - * @return [type] [description]
722 + * @return array
740 723 */
741 - public function getEntryXML( $name ) {
742 - if ( $entry_xml = $this->getEntryData( $name ) ) {
743 - // Remove dirty namespace prefixes.
744 - $entry_xml = preg_replace( '/xmlns[^=]*="[^"]*"/i', '', $entry_xml ); // remove namespaces
745 - $entry_xml = preg_replace( '/[a-zA-Z0-9]+:([a-zA-Z0-9]+="[^"]+")/', '$1$2', $entry_xml ); // remove namespaced attrs
746 - $entry_xml = preg_replace( '/<[a-zA-Z0-9]+:([^>]+)>/', '<$1>', $entry_xml ); // fix namespaced opened tags
747 - $entry_xml = preg_replace( '/<\/[a-zA-Z0-9]+:([^>]+)>/', '</$1>', $entry_xml ); // fix namespaced closed tags
724 + public function dimension($worksheetIndex = 0)
725 + {
748 726
749 - // XML External Entity (XXE) Prevention.
750 - $_old_value = libxml_disable_entity_loader( true );
751 - $entry_xmlobj = simplexml_load_string( $entry_xml );
752 - libxml_disable_entity_loader( $_old_value );
753 - if ( $entry_xmlobj ) {
754 - return $entry_xmlobj;
727 + if (($ws = $this->worksheet($worksheetIndex)) === false) {
728 + return [0, 0];
729 + }
730 + /* @var SimpleXMLElement $ws */
731 +
732 + $ref = (string)$ws->dimension['ref'];
733 +
734 + if (self::strpos($ref, ':') !== false) {
735 + $d = explode(':', $ref);
736 + $idx = $this->getIndex($d[1]);
737 +
738 + return [$idx[0] + 1, $idx[1] + 1];
739 + }
740 + /*
741 + if ( $ref !== '' ) { // 0.6.8
742 + $index = $this->getIndex( $ref );
743 +
744 + return [ $index[0] + 1, $index[1] + 1 ];
745 + }
746 + */
747 +
748 + // slow method
749 + $maxC = $maxR = 0;
750 + $iR = -1;
751 + foreach ($ws->sheetData->row as $row) {
752 + $iR++;
753 + $iC = -1;
754 + foreach ($row->c as $c) {
755 + $iC++;
756 + $idx = $this->getIndex((string)$c['r']);
757 + $x = $idx[0];
758 + $y = $idx[1];
759 + if ($x > -1) {
760 + if ($x > $maxC) {
761 + $maxC = $x;
762 + }
763 + if ($y > $maxR) {
764 + $maxR = $y;
765 + }
766 + } else {
767 + if ($iC > $maxC) {
768 + $maxC = $iC;
769 + }
770 + if ($iR > $maxR) {
771 + $maxR = $iR;
772 + }
773 + }
755 774 }
756 - $e = libxml_get_last_error();
757 - $this->error( 'XML-entry ' . $name . ' parser error ' . $e->message . ' line ' . $e->line );
758 - } else {
759 - $this->error( 'XML-entry not found: ' . $name );
760 775 }
761 - return false;
776 +
777 + return [$maxC + 1, $maxR + 1];
762 778 }
763 779
764 - /**
765 - * [unixstamp description]
766 - *
767 - * @since 1.1.0
768 - *
769 - * @param [type] $excelDateTime [description]
770 - * @return [type] [description]
771 - */
772 - public function unixstamp( $excelDateTime ) {
773 - $d = floor( $excelDateTime ); // seconds since 1900
774 - $t = $excelDateTime - $d;
780 + public function getIndex($cell = 'A1')
781 + {
782 + $m = null;
775 783
776 - return ( abs( $d ) > 0 ) ? ( $d - 25569 ) * DAY_IN_SECONDS + round( $t * DAY_IN_SECONDS ) : round( $t * DAY_IN_SECONDS ); // 25569 days = 70 years?
784 + if (preg_match('/([A-Z]+)(\d+)/', $cell, $m)) {
785 + $col = $m[1];
786 + $row = $m[2];
787 +
788 + $colLen = self::strlen($col);
789 + $index = 0;
790 +
791 + for ($i = $colLen - 1; $i >= 0; $i--) {
792 + $index += (ord($col[$i]) - 64) * pow(26, $colLen - $i - 1);
793 + }
794 +
795 + return [$index - 1, $row - 1];
796 + }
797 +
798 +// $this->error( 'Invalid cell index ' . $cell );
799 +
800 + return [-1, -1];
777 801 }
778 802
779 - /**
780 - * [error description]
781 - *
782 - * @since 1.1.0
783 - *
784 - * @param string $set Optional. [description]
785 - * @return [type] [description]
786 - */
787 - public function error( $set = '' ) {
788 - if ( '' !== $set ) {
789 - $this->error = $set;
790 - // trigger_error( __CLASS__ . ': ' . $set, E_USER_WARNING );
803 + public function value($cell)
804 + {
805 + // Determine data type
806 + $dataType = (string)$cell['t'];
807 +
808 + if ($dataType === '' || $dataType === 'n') { // number
809 + $s = (int)$cell['s'];
810 + if ($s > 0 && isset($this->cellFormats[$s])) {
811 + if (array_key_exists('format', $this->cellFormats[$s])) {
812 + $format = $this->cellFormats[$s]['format'];
813 + if ($format && preg_match('/[mM]/', preg_replace('/\"[^"]+\"/', '', $format))) { // [mm]onth,AM|PM
814 + $dataType = 'D';
815 + }
816 + } else {
817 + $dataType = 'n';
818 + }
819 + }
791 820 }
792 - return $this->error;
793 - }
794 821
795 - /**
796 - * [success description]
797 - *
798 - * @since 1.1.0
799 - *
800 - * @return [type] [description]
801 - */
802 - public function success() {
803 - return ! $this->error;
804 - }
822 + $value = '';
805 823
806 - /**
807 - * [_parse description]
808 - *
809 - * @since 1.1.0
810 - *
811 - * @return [type] [description]
812 - */
813 - protected function _parse() {
814 - // Document data holders.
815 - $this->sharedstrings = array();
816 - $this->sheets = array();
817 - // $this->styles = array();
824 + switch ($dataType) {
825 + case 's':
826 + // Value is a shared string
827 + if ((string)$cell->v !== '') {
828 + $value = $this->sharedstrings[(int)$cell->v];
829 + }
830 + break;
818 831
819 - // Read relations and search for officeDocument.
820 - if ( $relations = $this->getEntryXML( '_rels/.rels' ) ) {
821 - foreach ( $relations->Relationship as $rel ) {
822 - $rel_type = trim( (string) $rel['Type'] );
823 - $rel_target = trim( (string) $rel['Target'] );
824 - if ( self::SCHEMA_REL_OFFICEDOCUMENT === $rel_type && $this->workbook = $this->getEntryXML( $rel_target ) ) {
825 - $index_rId = array(); // [0 => rId1]
832 + case 'str': // formula?
833 + if ((string)$cell->v !== '') {
834 + $value = (string)$cell->v;
835 + }
836 + break;
826 837
827 - $index = 0;
828 - foreach ( $this->workbook->sheets->sheet as $s ) {
829 - $this->sheetNames[ $index ] = (string) $s['name'];
830 - $index_rId[ $index ] = (string) $s['id'];
831 - $index++;
832 - }
838 + case 'b':
839 + // Value is boolean
840 + $value = self::boolean((string)$cell->v);
833 841
834 - if ( $workbookRelations = $this->getEntryXML( dirname( $rel_target ) . '/_rels/workbook.xml.rels' ) ) {
835 - // Loop relations for workbook and extract sheets.
836 - foreach ( $workbookRelations->Relationship as $workbookRelation ) {
837 - $wrel_type = trim( (string) $workbookRelation['Type'] );
838 - $wrel_path = dirname( trim( (string) $rel['Target'] ) ) . '/' . trim( $workbookRelation['Target'] );
839 - if ( ! $this->entryExists( $wrel_path ) ) {
840 - continue;
841 - }
842 - if ( self::SCHEMA_REL_WORKSHEET === $wrel_type ) {
843 - if ( $sheet = $this->getEntryXML( $wrel_path ) ) {
844 - $index = array_search( (string) $workbookRelation['Id'], $index_rId, false );
845 - $this->sheets[ $index ] = $sheet;
846 - }
847 - } elseif ( self::SCHEMA_REL_SHAREDSTRINGS === $wrel_type ) {
848 - if ( $sharedStrings = $this->getEntryXML( $wrel_path ) ) {
849 - foreach ( $sharedStrings->si as $val ) {
850 - if ( isset( $val->t ) ) {
851 - $this->sharedstrings[] = (string) $val->t;
852 - } elseif ( isset( $val->r ) ) {
853 - $this->sharedstrings[] = $this->_parseRichText( $val );
854 - }
855 - }
856 - }
857 - } elseif ( self::SCHEMA_REL_STYLES === $wrel_type ) {
858 - $this->styles = $this->getEntryXML( $wrel_path );
842 + break;
859 843
860 - $nf = array();
861 - if ( null !== $this->styles->numFmts->numFmt ) {
862 - foreach ( $this->styles->numFmts->numFmt as $v ) {
863 - $nf[ (int) $v['numFmtId'] ] = (string) $v['formatCode'];
864 - }
865 - }
844 + case 'inlineStr':
845 + // Value is rich text inline
846 + $value = self::parseRichText($cell->is);
866 847
867 - if ( null !== $this->styles->cellXfs->xf ) {
868 - foreach ( $this->styles->cellXfs->xf as $v ) {
869 - $v = (array) $v->attributes();
870 - $v['format'] = '';
848 + break;
871 849
872 - if ( isset( $v['@attributes']['numFmtId'] ) ) {
873 - $v = $v['@attributes'];
874 - $fid = (int) $v['numFmtId'];
875 - if ( isset( self::$built_in_cell_formats[ $fid ] ) ) {
876 - $v['format'] = self::$built_in_cell_formats[ $fid ];
877 - } elseif ( isset( $nf[ $fid ] ) ) {
878 - $v['format'] = $nf[ $fid ];
879 - }
880 - }
881 - $this->workbook_cell_formats[] = $v;
882 - }
883 - }
884 - }
885 - } // foreach
886 - break;
850 + case 'e':
851 + // Value is an error message
852 + if ((string)$cell->v !== '') {
853 + $value = (string)$cell->v;
854 + }
855 + break;
856 +
857 + case 'D':
858 + // Date as float
859 + if (!empty($cell->v)) {
860 + $value = $this->datetimeFormat ?
861 + gmdate($this->datetimeFormat, $this->unixstamp((float)$cell->v)) : (float)$cell->v;
862 + }
863 + break;
864 +
865 + case 'd':
866 + // Date as ISO YYYY-MM-DD
867 + if ((string)$cell->v !== '') {
868 + $value = (string)$cell->v;
869 + }
870 + break;
871 +
872 + default:
873 + // Value is a string
874 + $value = (string)$cell->v;
875 +
876 + // Check for numeric values
877 + if (is_numeric($value)) {
878 + /** @noinspection TypeUnsafeComparisonInspection */
879 + if ($value == (int)$value) {
880 + $value = (int)$value;
881 + } /** @noinspection TypeUnsafeComparisonInspection */ elseif ($value == (float)$value) {
882 + $value = (float)$value;
887 883 }
888 884 }
889 - } // foreach
890 885 }
891 886
892 - // Sort sheets.
893 - if ( count( $this->sheets ) ) {
894 - ksort( $this->sheets );
895 - return true;
887 + return $value;
888 + }
889 +
890 + public function unixstamp($excelDateTime)
891 + {
892 +
893 + $d = floor($excelDateTime); // days since 1900 or 1904
894 + $t = $excelDateTime - $d;
895 +
896 + if ($this->date1904) {
897 + $d += 1462;
896 898 }
897 899
898 - return false;
900 + $t = (abs($d) > 0) ? ($d - 25569) * 86400 + round($t * 86400) : round($t * 86400);
901 +
902 + return (int)$t;
899 903 }
900 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
901 955 /**
902 - * [_parseRichText description]
903 - *
904 - * @since 1.1.0
905 - *
906 - * @param [type] $is [description]
907 - * @return string [description]
956 + * @param $worksheetIndex
957 + * @param $limit
958 + * @return \Generator
908 959 */
909 - protected function _parseRichText( $is ) {
910 - $value = array();
960 + public function readRows($worksheetIndex = 0, $limit = 0)
961 + {
911 962
912 - if ( isset( $is->t ) ) {
913 - $value[] = (string) $is->t;
914 - } else {
915 - foreach ( $is->r as $run ) {
916 - $value[] = (string) $run->t;
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++;
917 1000 }
1001 + yield $r;
1002 +
1003 + $curR++;
1004 + $_limit--;
1005 + if ($_limit === 0) {
1006 + return;
1007 + }
918 1008 }
1009 + while ($curR < $numRows) {
1010 + yield $emptyRow;
1011 + $curR++;
1012 + $_limit--;
1013 + if ($_limit === 0) {
1014 + return;
1015 + }
1016 + }
1017 + }
919 1018
920 - return implode( ' ', $value );
1019 + public function rowsEx($worksheetIndex = 0, $limit = 0)
1020 + {
1021 + return iterator_to_array($this->readRowsEx($worksheetIndex, $limit), false);
921 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 + }
922 1037
923 1038 /**
924 - * [parse description]
1039 + * Returns cell value
1040 + * VERY SLOW! Use ->rows() or ->rowsEx()
925 1041 *
926 - * @since 1.8.1
1042 + * @param int $worksheetIndex
1043 + * @param string|array $cell ref or coords, D12 or [3,12]
927 1044 *
928 - * @param [type] $filename [description]
929 - * @param bool $is_data [description]
930 - * @return [type] [description]
1045 + * @return mixed Returns NULL if not found
931 1046 */
932 - public static function parse( $filename, $is_data = false ) {
933 - $xlsx = new self( $filename, $is_data );
934 - if ( $xlsx->success() ) {
935 - return $xlsx;
1047 + public function getCell($worksheetIndex = 0, $cell = 'A1')
1048 + {
1049 +
1050 + if (($ws = $this->worksheet($worksheetIndex)) === false) {
1051 + return false;
936 1052 }
937 - self::parse_error( $xlsx->error() );
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 + }
938 1062
1063 + return null;
1064 + }
1065 +
1066 + public function getSheets()
1067 + {
1068 + return $this->sheets;
1069 + }
1070 +
1071 + public function sheetsCount()
1072 + {
1073 + return count($this->sheets);
1074 + }
1075 +
1076 + public function sheetName($worksheetIndex)
1077 + {
1078 + $sn = $this->sheetNames();
1079 + if (isset($sn[$worksheetIndex])) {
1080 + return $sn[$worksheetIndex];
1081 + }
1082 +
939 1083 return false;
940 1084 }
941 1085
942 - /**
943 - * [parse_error description]
944 - *
945 - * @since 1.8.1
946 - *
947 - * @param string $set [description]
948 - * @return [type] [description]
949 - */
950 - public static function parse_error( $set = '' ) {
951 - static $error = '';
952 - return ( '' !== $set ) ? $error = $set : $error;
1086 + public function sheetNames()
1087 + {
1088 + $a = [];
1089 + foreach ($this->sheetMetaData as $k => $v) {
1090 + $a[$k] = $v['name'];
1091 + }
1092 + return $a;
953 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 + }
954 1106
955 - /**
956 - * [getCell description]
957 - *
958 - * Example: xlsx->getCell(2,'B87', 0);
959 - * Get cell B87 from 2nd worksheet, formatted by General (see $built_in_cell_formats for all formats).
960 - * It's useful when we need to get a cell that has the wrong format,
961 - * Or just for direct cell reading. (thx EGO7000)
962 - *
963 - * @since 1.8.1
964 - *
965 - * @param int $worksheet_index.
966 - * @param string $cell.
967 - * @param null|int $format.
968 - *
969 - * @return mixed
970 - */
971 - public function getCell( $worksheet_index = 0, $cell = 'A1', $format = null ) {
972 - if ( false === ( $ws = $this->worksheet( $worksheet_index ) ) ) {
973 - return false;
1107 + public function getStyles()
1108 + {
1109 + return $this->styles;
1110 + }
1111 +
1112 + public function getPackage()
1113 + {
1114 + return $this->package;
1115 + }
1116 +
1117 + public function setDateTimeFormat($value)
1118 + {
1119 + $this->datetimeFormat = is_string($value) ? $value : false;
1120 + }
1121 +
1122 + public static function getTarget($base, $target)
1123 + {
1124 + $target = trim($target);
1125 + if (strpos($target, '/') === 0) {
1126 + return self::substr($target, 1);
974 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 + }
975 1144
976 - list( $current_cell, $current_row ) = is_array( $cell ) ? $cell : $this->_columnIndex( (string) $cell );
1145 + public static function parseRichText($is = null)
1146 + {
1147 + $value = [];
977 1148
978 - if ( isset( $ws->sheetData->row[ $current_row ], $ws->sheetData->row[ $current_row ]->c[ $current_cell ] ) ) {
979 - $c = $ws->sheetData->row[ $current_row ]->c[ $current_cell ];
980 - return $this->value( $c, $format );
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 + }
981 1155 }
982 - return null;
1156 +
1157 + return implode('', $value);
983 1158 }
984 1159
985 -} // class SimpleXLSX
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 +}