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
tablepress / libraries / vendor / PhpSpreadsheet / Shared / OLE.php

OLE.php in TablePress – Tables in WordPress made easy 3.4, at libraries/vendor/PhpSpreadsheet/Shared/OLE.php

647 lines 17.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace TablePress\PhpOffice\PhpSpreadsheet\Shared;
4
5 // vim: set expandtab tabstop=4 shiftwidth=4:
6 // +----------------------------------------------------------------------+
7 // | PHP Version 4 |
8 // +----------------------------------------------------------------------+
9 // | Copyright (c) 1997-2002 The PHP Group |
10 // +----------------------------------------------------------------------+
11 // | This source file is subject to version 2.02 of the PHP license, |
12 // | that is bundled with this package in the file LICENSE, and is |
13 // | available at through the world-wide-web at |
14 // | http://www.php.net/license/2_02.txt. |
15 // | If you did not receive a copy of the PHP license and are unable to |
16 // | obtain it through the world-wide-web, please send a note to |
17 // | [email protected] so we can mail you a copy immediately. |
18 // +----------------------------------------------------------------------+
19 // | Author: Xavier Noguer <[email protected]> |
20 // | Based on OLE::Storage_Lite by Kawai, Takanori |
21 // +----------------------------------------------------------------------+
22 //
23
24 use TablePress\PhpOffice\PhpSpreadsheet\Exception;
25 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
26 use TablePress\PhpOffice\PhpSpreadsheet\Shared\OLE\ChainedBlockStream;
27 use TablePress\PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root;
28
29 /*
30 * Array for storing OLE instances that are accessed from
31 * OLE_ChainedBlockStream::stream_open().
32 *
33 * @var array
34 */
35 $GLOBALS['_OLE_INSTANCES'] = [];
36
37 /**
38 * OLE package base class.
39 *
40 * @author Xavier Noguer <[email protected]>
41 * @author Christian Schmidt <[email protected]>
42 */
43 class OLE
44 {
45 const OLE_PPS_TYPE_ROOT = 5;
46 const OLE_PPS_TYPE_DIR = 1;
47 const OLE_PPS_TYPE_FILE = 2;
48 const OLE_DATA_SIZE_SMALL = 0x1000;
49 const OLE_LONG_INT_SIZE = 4;
50 const OLE_PPS_SIZE = 0x80;
51
52 /**
53 * The file handle for reading an OLE container.
54 *
55 * @var resource
56 */
57 public $_file_handle;
58
59 /**
60 * Array of PPS's found on the OLE container.
61 *
62 * @var array<OLE\PPS|OLE\PPS\File|Root>
63 */
64 public array $_list = [];
65
66 /**
67 * Root directory of OLE container.
68 */
69 public Root $root;
70
71 /**
72 * Big Block Allocation Table.
73 *
74 * @var mixed[] (blockId => nextBlockId)
75 */
76 public array $bbat;
77
78 /**
79 * Short Block Allocation Table.
80 *
81 * @var mixed[] (blockId => nextBlockId)
82 */
83 public array $sbat;
84
85 /**
86 * Size of big blocks. This is usually 512.
87 *
88 * @var int<1, max> number of octets per block
89 */
90 public int $bigBlockSize;
91
92 /**
93 * Size of small blocks. This is usually 64.
94 *
95 * @var int number of octets per block
96 */
97 public int $smallBlockSize;
98
99 /**
100 * Threshold for big blocks.
101 */
102 public int $bigBlockThreshold;
103
104 /**
105 * Reads an OLE container from the contents of the file given.
106 *
107 * @acces public
108 *
109 * @return bool true on success, PEAR_Error on failure
110 */
111 public function read(string $filename): bool
112 {
113 $fh = @fopen($filename, 'rb');
114 if ($fh === false) {
115 throw new ReaderException("Can't open file $filename");
116 }
117 $this->_file_handle = $fh;
118
119 $signature = fread($fh, 8);
120 if ("\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" != $signature) {
121 throw new ReaderException("File doesn't seem to be an OLE container.");
122 }
123 fseek($fh, 28);
124 if (fread($fh, 2) != "\xFE\xFF") {
125 // This shouldn't be a problem in practice
126 throw new ReaderException('Only Little-Endian encoding is supported.');
127 }
128 // Size of blocks and short blocks in bytes
129 /** @var int<1, max> */
130 $temp = 2 ** self::readInt2($fh);
131 $this->bigBlockSize = $temp;
132 $this->smallBlockSize = 2 ** self::readInt2($fh);
133
134 // Skip UID, revision number and version number
135 fseek($fh, 44);
136 // Number of blocks in Big Block Allocation Table
137 $bbatBlockCount = self::readInt4($fh);
138
139 // Root chain 1st block
140 $directoryFirstBlockId = self::readInt4($fh);
141
142 // Skip unused bytes
143 fseek($fh, 56);
144 // Streams shorter than this are stored using small blocks
145 $this->bigBlockThreshold = self::readInt4($fh);
146 // Block id of first sector in Short Block Allocation Table
147 $sbatFirstBlockId = self::readInt4($fh);
148 // Number of blocks in Short Block Allocation Table
149 $sbbatBlockCount = self::readInt4($fh);
150 // Block id of first sector in Master Block Allocation Table
151 $mbatFirstBlockId = self::readInt4($fh);
152 // Number of blocks in Master Block Allocation Table
153 $mbbatBlockCount = self::readInt4($fh);
154 $this->bbat = [];
155
156 // Remaining 4 * 109 bytes of current block is beginning of Master
157 // Block Allocation Table
158 $mbatBlocks = [];
159 for ($i = 0; $i < 109; ++$i) {
160 $mbatBlocks[] = self::readInt4($fh);
161 }
162
163 // Read rest of Master Block Allocation Table (if any is left)
164 $pos = $this->getBlockOffset($mbatFirstBlockId);
165 for ($i = 0; $i < $mbbatBlockCount; ++$i) {
166 fseek($fh, $pos);
167 for ($j = 0; $j < $this->bigBlockSize / 4 - 1; ++$j) {
168 $mbatBlocks[] = self::readInt4($fh);
169 }
170 // Last block id in each block points to next block
171 $pos = $this->getBlockOffset(self::readInt4($fh));
172 }
173
174 // Read Big Block Allocation Table according to chain specified by $mbatBlocks
175 for ($i = 0; $i < $bbatBlockCount; ++$i) {
176 $pos = $this->getBlockOffset($mbatBlocks[$i]);
177 fseek($fh, $pos);
178 for ($j = 0; $j < $this->bigBlockSize / 4; ++$j) {
179 $this->bbat[] = self::readInt4($fh);
180 }
181 }
182
183 // Read short block allocation table (SBAT)
184 $this->sbat = [];
185 $shortBlockCount = $sbbatBlockCount * $this->bigBlockSize / 4;
186 $sbatFh = $this->getStream($sbatFirstBlockId);
187 for ($blockId = 0; $blockId < $shortBlockCount; ++$blockId) {
188 $this->sbat[$blockId] = self::readInt4($sbatFh);
189 }
190 fclose($sbatFh);
191
192 $this->readPpsWks($directoryFirstBlockId);
193
194 return true;
195 }
196
197 /**
198 * @param int $blockId byte offset from beginning of file
199 */
200 public function getBlockOffset(int $blockId): int
201 {
202 return 512 + $blockId * $this->bigBlockSize;
203 }
204
205 /**
206 * Returns a stream for use with fread() etc. External callers should
207 * use \TablePress\PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\File::getStream().
208 *
209 * @param int|OLE\PPS $blockIdOrPps block id or PPS
210 *
211 * @return resource read-only stream
212 */
213 public function getStream($blockIdOrPps)
214 {
215 static $isRegistered = false;
216 if (!$isRegistered) {
217 stream_wrapper_register('ole-chainedblockstream', ChainedBlockStream::class);
218 $isRegistered = true;
219 }
220
221 // Store current instance in global array, so that it can be accessed
222 // in OLE_ChainedBlockStream::stream_open().
223 // Object is removed from self::$instances in OLE_Stream::close().
224 $GLOBALS['_OLE_INSTANCES'][] = $this; //* @phpstan-ignore offsetAccess.nonOffsetAccessible (I don't know how to fix this)
225 $keys = array_keys($GLOBALS['_OLE_INSTANCES']); //* @phpstan-ignore argument.type (I don't know how to fix this)
226 $instanceId = end($keys);
227
228 $path = 'ole-chainedblockstream://oleInstanceId=' . $instanceId;
229 if ($blockIdOrPps instanceof OLE\PPS) {
230 $path .= '&blockId=' . $blockIdOrPps->startBlock;
231 $path .= '&size=' . $blockIdOrPps->Size;
232 if ($blockIdOrPps instanceof Root) {
233 $path .= '&isRoot=1';
234 }
235 } else {
236 $path .= '&blockId=' . $blockIdOrPps;
237 }
238 if (!fopen($path, 'rb')) {
239 throw new Exception("Unable to open stream $path");
240 }
241 $resource = fopen($path, 'rb');
242
243 return $resource;
244 }
245
246 /**
247 * Reads a signed char.
248 *
249 * @param resource $fileHandle file handle
250 */
251 private static function readInt1($fileHandle): int
252 {
253 [, $tmp] = unpack('c', fread($fileHandle, 1) ?: '') ?: [0, 0];
254 /** @var int $tmp */
255
256 return $tmp;
257 }
258
259 /**
260 * Reads an unsigned short (2 octets).
261 *
262 * @param resource $fileHandle file handle
263 */
264 private static function readInt2($fileHandle): int
265 {
266 [, $tmp] = unpack('v', fread($fileHandle, 2) ?: '') ?: [0, 0];
267 /** @var int $tmp */
268
269 return $tmp;
270 }
271
272 private const SIGNED_4OCTET_LIMIT = 2147483648;
273
274 private const SIGNED_4OCTET_SUBTRACT = 2 * self::SIGNED_4OCTET_LIMIT;
275
276 /**
277 * Reads long (4 octets), interpreted as if signed on 32-bit system.
278 *
279 * @param resource $fileHandle file handle
280 */
281 private static function readInt4($fileHandle): int
282 {
283 [, $tmp] = unpack('V', fread($fileHandle, 4) ?: '') ?: [0, 0];
284 /** @var int $tmp */
285 if ($tmp >= self::SIGNED_4OCTET_LIMIT) {
286 $tmp -= self::SIGNED_4OCTET_SUBTRACT;
287 }
288
289 return $tmp;
290 }
291
292 /**
293 * Gets information about all PPS's on the OLE container from the PPS WK's
294 * creates an OLE_PPS object for each one.
295 *
296 * @param int $blockId the block id of the first block
297 *
298 * @return bool true on success, PEAR_Error on failure
299 */
300 public function readPpsWks(int $blockId): bool
301 {
302 $fh = $this->getStream($blockId);
303 for ($pos = 0; true; $pos += 128) {
304 fseek($fh, $pos, SEEK_SET);
305 $nameUtf16 = (string) fread($fh, 64);
306 $nameLength = self::readInt2($fh);
307 $nameUtf16 = (string) substr($nameUtf16, 0, $nameLength - 2);
308 // Simple conversion from UTF-16LE to ISO-8859-1
309 $name = str_replace("\x00", '', $nameUtf16);
310 $type = self::readInt1($fh);
311 switch ($type) {
312 case self::OLE_PPS_TYPE_ROOT:
313 $pps = new Root(null, null, []);
314 $this->root = $pps;
315
316 break;
317 case self::OLE_PPS_TYPE_DIR:
318 $pps = new OLE\PPS(null, null, null, null, null, null, null, null, null, []);
319
320 break;
321 case self::OLE_PPS_TYPE_FILE:
322 $pps = new OLE\PPS\File($name);
323
324 break;
325 default:
326 throw new Exception('Unsupported PPS type');
327 }
328 fseek($fh, 1, SEEK_CUR);
329 $pps->Type = $type;
330 $pps->Name = $name;
331 $pps->PrevPps = self::readInt4($fh);
332 $pps->NextPps = self::readInt4($fh);
333 $pps->DirPps = self::readInt4($fh);
334 fseek($fh, 20, SEEK_CUR);
335 $pps->Time1st = self::OLE2LocalDate((string) fread($fh, 8));
336 $pps->Time2nd = self::OLE2LocalDate((string) fread($fh, 8));
337 $pps->startBlock = self::readInt4($fh);
338 $pps->Size = self::readInt4($fh);
339 $pps->No = count($this->_list);
340 $this->_list[] = $pps;
341
342 // check if the PPS tree (starting from root) is complete
343 if (isset($this->root) && $this->ppsTreeComplete($this->root->No)) {
344 break;
345 }
346 }
347 fclose($fh);
348
349 // Initialize $pps->children on directories
350 foreach ($this->_list as $pps) {
351 if ($pps->Type == self::OLE_PPS_TYPE_DIR || $pps->Type == self::OLE_PPS_TYPE_ROOT) {
352 $nos = [$pps->DirPps];
353 $pps->children = [];
354 while (!empty($nos)) {
355 $no = array_pop($nos);
356 if ($no != -1) {
357 $childPps = $this->_list[$no];
358 $nos[] = $childPps->PrevPps;
359 $nos[] = $childPps->NextPps;
360 $pps->children[] = $childPps;
361 }
362 }
363 }
364 }
365
366 return true;
367 }
368
369 /**
370 * It checks whether the PPS tree is complete (all PPS's read)
371 * starting with the given PPS (not necessarily root).
372 *
373 * @param int $index The index of the PPS from which we are checking
374 *
375 * @return bool Whether the PPS tree for the given PPS is complete
376 */
377 private function ppsTreeComplete(int $index): bool
378 {
379 if (!isset($this->_list[$index])) {
380 return false;
381 }
382 $pps = $this->_list[$index];
383
384 return
385 ($pps->PrevPps == -1
386 || $this->ppsTreeComplete($pps->PrevPps))
387 && ($pps->NextPps == -1
388 || $this->ppsTreeComplete($pps->NextPps))
389 && ($pps->DirPps == -1
390 || $this->ppsTreeComplete($pps->DirPps));
391 }
392
393 /**
394 * Checks whether a PPS is a File PPS or not.
395 * If there is no PPS for the index given, it will return false.
396 *
397 * @param int $index The index for the PPS
398 *
399 * @return bool true if it's a File PPS, false otherwise
400 */
401 public function isFile(int $index): bool
402 {
403 if (isset($this->_list[$index])) {
404 return $this->_list[$index]->Type == self::OLE_PPS_TYPE_FILE;
405 }
406
407 return false;
408 }
409
410 /**
411 * Checks whether a PPS is a Root PPS or not.
412 * If there is no PPS for the index given, it will return false.
413 *
414 * @param int $index the index for the PPS
415 *
416 * @return bool true if it's a Root PPS, false otherwise
417 */
418 public function isRoot(int $index): bool
419 {
420 if (isset($this->_list[$index])) {
421 return $this->_list[$index]->Type == self::OLE_PPS_TYPE_ROOT;
422 }
423
424 return false;
425 }
426
427 /**
428 * Gives the total number of PPS's found in the OLE container.
429 *
430 * @return int The total number of PPS's found in the OLE container
431 */
432 public function ppsTotal(): int
433 {
434 return count($this->_list);
435 }
436
437 /**
438 * Gets data from a PPS
439 * If there is no PPS for the index given, it will return an empty string.
440 *
441 * @param int $index The index for the PPS
442 * @param int $position The position from which to start reading
443 * (relative to the PPS)
444 * @param int $length The amount of bytes to read (at most)
445 *
446 * @return string The binary string containing the data requested
447 *
448 * @see OLE_PPS_File::getStream()
449 */
450 public function getData(int $index, int $position, int $length): string
451 {
452 // if position is not valid return empty string
453 if (!isset($this->_list[$index]) || ($position >= $this->_list[$index]->Size) || ($position < 0)) {
454 return '';
455 }
456 $fh = $this->getStream($this->_list[$index]);
457 $data = (string) stream_get_contents($fh, $length, $position);
458 fclose($fh);
459
460 return $data;
461 }
462
463 /**
464 * Gets the data length from a PPS
465 * If there is no PPS for the index given, it will return 0.
466 *
467 * @param int $index The index for the PPS
468 *
469 * @return int The amount of bytes in data the PPS has
470 */
471 public function getDataLength(int $index): int
472 {
473 if (isset($this->_list[$index])) {
474 return $this->_list[$index]->Size;
475 }
476
477 return 0;
478 }
479
480 /**
481 * Get the complete contents of a named file stream.
482 *
483 * @throws ReaderException if no file stream has the requested name
484 */
485 public function getDataByName(string $name): string
486 {
487 foreach ($this->_list as $index => $pps) {
488 if ($pps->Type === self::OLE_PPS_TYPE_FILE && $pps->Name === $name) {
489 return $this->getData($index, 0, $pps->Size);
490 }
491 }
492
493 throw new ReaderException("OLE stream '$name' was not found.");
494 }
495
496 /**
497 * Check whether a named file stream exists without reading its contents.
498 */
499 public function hasDataByName(string $name): bool
500 {
501 foreach ($this->_list as $pps) {
502 if ($pps->Type === self::OLE_PPS_TYPE_FILE && $pps->Name === $name) {
503 return true;
504 }
505 }
506
507 return false;
508 }
509
510 /**
511 * Copy a named file stream to an open output handle without loading a
512 * large stream into memory.
513 *
514 * @param resource $output
515 *
516 * @throws ReaderException if no file stream has the requested name
517 */
518 public function copyDataByName(string $name, $output): void
519 {
520 foreach ($this->_list as $pps) {
521 if ($pps->Type !== self::OLE_PPS_TYPE_FILE || $pps->Name !== $name) {
522 continue;
523 }
524 if ($pps->Size < $this->bigBlockThreshold) {
525 $data = $this->getData($pps->No, 0, $pps->Size);
526 if (fwrite($output, $data) !== strlen($data)) {
527 throw new ReaderException("Could not write OLE stream '$name'.");
528 }
529
530 return;
531 }
532
533 $remaining = $pps->Size;
534 $blockId = $pps->startBlock;
535 $blocks = [];
536 while ($remaining > 0) {
537 if (!is_int($blockId) || $blockId === -2 || isset($blocks[$blockId], $this->bbat[$blockId])) {
538 throw new ReaderException("Invalid OLE stream '$name'.");
539 }
540 $blocks[$blockId] = true;
541 fseek($this->_file_handle, $this->getBlockOffset($blockId));
542 $length = min($remaining, $this->bigBlockSize);
543 $data = fread($this->_file_handle, $length);
544 if ($data === false || strlen($data) !== $length || fwrite($output, $data) !== $length) {
545 throw new ReaderException("Could not copy OLE stream '$name'.");
546 }
547 $remaining -= $length;
548 $blockId = $this->bbat[$blockId] ?? null;
549 if (!is_int($blockId)) {
550 throw new ReaderException("Invalid OLE stream '$name'.");
551 }
552 }
553
554 return;
555 }
556
557 throw new ReaderException("OLE stream '$name' was not found.");
558 }
559
560 /**
561 * Utility function to transform ASCII text to Unicode.
562 *
563 * @param string $ascii The ASCII string to transform
564 *
565 * @return string The string in Unicode
566 */
567 public static function ascToUcs(string $ascii): string
568 {
569 $rawname = '';
570 $iMax = strlen($ascii);
571 for ($i = 0; $i < $iMax; ++$i) {
572 $rawname .= $ascii[$i]
573 . "\x00";
574 }
575
576 return $rawname;
577 }
578
579 /**
580 * Utility function
581 * Returns a string for the OLE container with the date given.
582 *
583 * @param float|int $date A timestamp
584 *
585 * @return string The string for the OLE container
586 */
587 public static function localDateToOLE($date): string
588 {
589 if (!$date) {
590 return "\x00\x00\x00\x00\x00\x00\x00\x00";
591 }
592 $dateTime = Date::dateTimeFromTimestamp("$date");
593
594 // days from 1-1-1601 until the beginning of UNIX era
595 $days = 134774;
596 // calculate seconds
597 $big_date = $days * 24 * 3600 + (float) $dateTime->format('U');
598 // multiply just to make MS happy
599 $big_date *= 10000000;
600
601 // Make HEX string
602 $res = '';
603
604 $factor = 2 ** 56;
605 while ($factor >= 1) {
606 $hex = (int) floor($big_date / $factor);
607 $res = pack('c', $hex) . $res;
608 $big_date = fmod($big_date, $factor);
609 $factor /= 256;
610 }
611
612 return $res;
613 }
614
615 /**
616 * Returns a timestamp from an OLE container's date.
617 *
618 * @param string $oleTimestamp A binary string with the encoded date
619 *
620 * @return float|int The Unix timestamp corresponding to the string
621 */
622 public static function OLE2LocalDate(string $oleTimestamp)
623 {
624 if (strlen($oleTimestamp) != 8) {
625 throw new ReaderException('Expecting 8 byte string');
626 }
627
628 // convert to units of 100 ns since 1601:
629 /** @var int[] */
630 $unpackedTimestamp = unpack('v4', $oleTimestamp) ?: [];
631 $timestampHigh = (float) $unpackedTimestamp[4] * 65536 + (float) $unpackedTimestamp[3];
632 $timestampLow = (float) $unpackedTimestamp[2] * 65536 + (float) $unpackedTimestamp[1];
633
634 // translate to seconds since 1601:
635 $timestampHigh /= 10000000;
636 $timestampLow /= 10000000;
637
638 // days from 1601 to 1970:
639 $days = 134774;
640
641 // translate to seconds since 1970:
642 $unixTimestamp = floor(65536.0 * 65536.0 * $timestampHigh + $timestampLow - $days * 24 * 3600 + 0.5);
643
644 return IntOrFloat::evaluate($unixTimestamp);
645 }
646 }
647