Failed Conditions
Push — master ( 735103...6a4138 )
by Adrien
12:48
created

OLE::ascToUcs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace 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 PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
25
use PhpOffice\PhpSpreadsheet\Shared\OLE\ChainedBlockStream;
26
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root;
27
28
/*
29
 * Array for storing OLE instances that are accessed from
30
 * OLE_ChainedBlockStream::stream_open().
31
 *
32
 * @var array
33
 */
34 54
$GLOBALS['_OLE_INSTANCES'] = [];
35
36
/**
37
 * OLE package base class.
38
 *
39
 * @author   Xavier Noguer <[email protected]>
40
 * @author   Christian Schmidt <[email protected]>
41
 */
42
class OLE
43
{
44
    const OLE_PPS_TYPE_ROOT = 5;
45
    const OLE_PPS_TYPE_DIR = 1;
46
    const OLE_PPS_TYPE_FILE = 2;
47
    const OLE_DATA_SIZE_SMALL = 0x1000;
48
    const OLE_LONG_INT_SIZE = 4;
49
    const OLE_PPS_SIZE = 0x80;
50
51
    /**
52
     * The file handle for reading an OLE container.
53
     *
54
     * @var resource
55
     */
56
    public $_file_handle;
57
58
    /**
59
     * Array of PPS's found on the OLE container.
60
     *
61
     * @var array
62
     */
63
    public $_list = [];
64
65
    /**
66
     * Root directory of OLE container.
67
     *
68
     * @var Root
69
     */
70
    public $root;
71
72
    /**
73
     * Big Block Allocation Table.
74
     *
75
     * @var array (blockId => nextBlockId)
76
     */
77
    public $bbat;
78
79
    /**
80
     * Short Block Allocation Table.
81
     *
82
     * @var array (blockId => nextBlockId)
83
     */
84
    public $sbat;
85
86
    /**
87
     * Size of big blocks. This is usually 512.
88
     *
89
     * @var int number of octets per block
90
     */
91
    public $bigBlockSize;
92
93
    /**
94
     * Size of small blocks. This is usually 64.
95
     *
96
     * @var int number of octets per block
97
     */
98
    public $smallBlockSize;
99
100
    /**
101
     * Threshold for big blocks.
102
     *
103
     * @var int
104
     */
105
    public $bigBlockThreshold;
106
107
    /**
108
     * Reads an OLE container from the contents of the file given.
109
     *
110
     * @acces public
111
     *
112
     * @param string $file
113
     *
114
     * @return bool true on success, PEAR_Error on failure
115
     */
116
    public function read($file)
117
    {
118
        $fh = fopen($file, 'rb');
119
        if (!$fh) {
0 ignored issues
show
introduced by
$fh is of type false|resource, thus it always evaluated to false.
Loading history...
120
            throw new ReaderException("Can't open file $file");
121
        }
122
        $this->_file_handle = $fh;
123
124
        $signature = fread($fh, 8);
125
        if ("\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" != $signature) {
126
            throw new ReaderException("File doesn't seem to be an OLE container.");
127
        }
128
        fseek($fh, 28);
129
        if (fread($fh, 2) != "\xFE\xFF") {
130
            // This shouldn't be a problem in practice
131
            throw new ReaderException('Only Little-Endian encoding is supported.');
132
        }
133
        // Size of blocks and short blocks in bytes
134
        $this->bigBlockSize = 2 ** self::readInt2($fh);
135
        $this->smallBlockSize = 2 ** self::readInt2($fh);
136
137
        // Skip UID, revision number and version number
138
        fseek($fh, 44);
139
        // Number of blocks in Big Block Allocation Table
140
        $bbatBlockCount = self::readInt4($fh);
141
142
        // Root chain 1st block
143
        $directoryFirstBlockId = self::readInt4($fh);
144
145
        // Skip unused bytes
146
        fseek($fh, 56);
147
        // Streams shorter than this are stored using small blocks
148
        $this->bigBlockThreshold = self::readInt4($fh);
149
        // Block id of first sector in Short Block Allocation Table
150
        $sbatFirstBlockId = self::readInt4($fh);
151
        // Number of blocks in Short Block Allocation Table
152
        $sbbatBlockCount = self::readInt4($fh);
153
        // Block id of first sector in Master Block Allocation Table
154
        $mbatFirstBlockId = self::readInt4($fh);
155
        // Number of blocks in Master Block Allocation Table
156
        $mbbatBlockCount = self::readInt4($fh);
157
        $this->bbat = [];
158
159
        // Remaining 4 * 109 bytes of current block is beginning of Master
160
        // Block Allocation Table
161
        $mbatBlocks = [];
162
        for ($i = 0; $i < 109; ++$i) {
163
            $mbatBlocks[] = self::readInt4($fh);
164
        }
165
166
        // Read rest of Master Block Allocation Table (if any is left)
167
        $pos = $this->getBlockOffset($mbatFirstBlockId);
168
        for ($i = 0; $i < $mbbatBlockCount; ++$i) {
169
            fseek($fh, $pos);
170
            for ($j = 0; $j < $this->bigBlockSize / 4 - 1; ++$j) {
171
                $mbatBlocks[] = self::readInt4($fh);
172
            }
173
            // Last block id in each block points to next block
174
            $pos = $this->getBlockOffset(self::readInt4($fh));
175
        }
176
177
        // Read Big Block Allocation Table according to chain specified by $mbatBlocks
178
        for ($i = 0; $i < $bbatBlockCount; ++$i) {
179
            $pos = $this->getBlockOffset($mbatBlocks[$i]);
180
            fseek($fh, $pos);
181
            for ($j = 0; $j < $this->bigBlockSize / 4; ++$j) {
182
                $this->bbat[] = self::readInt4($fh);
183
            }
184
        }
185
186
        // Read short block allocation table (SBAT)
187
        $this->sbat = [];
188
        $shortBlockCount = $sbbatBlockCount * $this->bigBlockSize / 4;
189
        $sbatFh = $this->getStream($sbatFirstBlockId);
190
        for ($blockId = 0; $blockId < $shortBlockCount; ++$blockId) {
191
            $this->sbat[$blockId] = self::readInt4($sbatFh);
192
        }
193
        fclose($sbatFh);
194
195
        $this->readPpsWks($directoryFirstBlockId);
196
197
        return true;
198
    }
199
200
    /**
201
     * @param int $blockId byte offset from beginning of file
202
     *
203
     * @return int
204
     */
205
    public function getBlockOffset($blockId)
206
    {
207
        return 512 + $blockId * $this->bigBlockSize;
208
    }
209
210
    /**
211
     * Returns a stream for use with fread() etc. External callers should
212
     * use \PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\File::getStream().
213
     *
214
     * @param int|OLE\PPS $blockIdOrPps block id or PPS
215
     *
216
     * @return resource read-only stream
217
     */
218
    public function getStream($blockIdOrPps)
219
    {
220
        static $isRegistered = false;
221
        if (!$isRegistered) {
222
            stream_wrapper_register('ole-chainedblockstream', ChainedBlockStream::class);
223
            $isRegistered = true;
224
        }
225
226
        // Store current instance in global array, so that it can be accessed
227
        // in OLE_ChainedBlockStream::stream_open().
228
        // Object is removed from self::$instances in OLE_Stream::close().
229
        $GLOBALS['_OLE_INSTANCES'][] = $this;
230
        $instanceId = end(array_keys($GLOBALS['_OLE_INSTANCES']));
231
232
        $path = 'ole-chainedblockstream://oleInstanceId=' . $instanceId;
233
        if ($blockIdOrPps instanceof OLE\PPS) {
234
            $path .= '&blockId=' . $blockIdOrPps->startBlock;
235
            $path .= '&size=' . $blockIdOrPps->Size;
236
        } else {
237
            $path .= '&blockId=' . $blockIdOrPps;
238
        }
239
240
        return fopen($path, 'rb');
0 ignored issues
show
Bug Best Practice introduced by
The expression return fopen($path, 'rb') could also return false which is incompatible with the documented return type resource. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
241
    }
242
243
    /**
244
     * Reads a signed char.
245
     *
246
     * @param resource $fh file handle
247
     *
248
     * @return int
249
     */
250
    private static function readInt1($fh)
251
    {
252
        [, $tmp] = unpack('c', fread($fh, 1));
253
254
        return $tmp;
255
    }
256
257
    /**
258
     * Reads an unsigned short (2 octets).
259
     *
260
     * @param resource $fh file handle
261
     *
262
     * @return int
263
     */
264
    private static function readInt2($fh)
265
    {
266
        [, $tmp] = unpack('v', fread($fh, 2));
267
268
        return $tmp;
269
    }
270
271
    /**
272
     * Reads an unsigned long (4 octets).
273
     *
274
     * @param resource $fh file handle
275
     *
276
     * @return int
277
     */
278
    private static function readInt4($fh)
279
    {
280
        [, $tmp] = unpack('V', fread($fh, 4));
281
282
        return $tmp;
283
    }
284
285
    /**
286
     * Gets information about all PPS's on the OLE container from the PPS WK's
287
     * creates an OLE_PPS object for each one.
288
     *
289
     * @param int $blockId the block id of the first block
290
     *
291
     * @return bool true on success, PEAR_Error on failure
292
     */
293
    public function readPpsWks($blockId)
294
    {
295
        $fh = $this->getStream($blockId);
296
        for ($pos = 0; true; $pos += 128) {
297
            fseek($fh, $pos, SEEK_SET);
298
            $nameUtf16 = fread($fh, 64);
299
            $nameLength = self::readInt2($fh);
300
            $nameUtf16 = substr($nameUtf16, 0, $nameLength - 2);
301
            // Simple conversion from UTF-16LE to ISO-8859-1
302
            $name = str_replace("\x00", '', $nameUtf16);
303
            $type = self::readInt1($fh);
304
            switch ($type) {
305
                case self::OLE_PPS_TYPE_ROOT:
306
                    $pps = new OLE\PPS\Root(null, null, []);
307
                    $this->root = $pps;
308
309
                    break;
310
                case self::OLE_PPS_TYPE_DIR:
311
                    $pps = new OLE\PPS(null, null, null, null, null, null, null, null, null, []);
312
313
                    break;
314
                case self::OLE_PPS_TYPE_FILE:
315
                    $pps = new OLE\PPS\File($name);
316
317
                    break;
318
                default:
319
                    break;
320
            }
321
            fseek($fh, 1, SEEK_CUR);
322
            $pps->Type = $type;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $pps does not seem to be defined for all execution paths leading up to this point.
Loading history...
323
            $pps->Name = $name;
324
            $pps->PrevPps = self::readInt4($fh);
325
            $pps->NextPps = self::readInt4($fh);
326
            $pps->DirPps = self::readInt4($fh);
327
            fseek($fh, 20, SEEK_CUR);
328
            $pps->Time1st = self::OLE2LocalDate(fread($fh, 8));
329
            $pps->Time2nd = self::OLE2LocalDate(fread($fh, 8));
330
            $pps->startBlock = self::readInt4($fh);
331
            $pps->Size = self::readInt4($fh);
332
            $pps->No = count($this->_list);
333
            $this->_list[] = $pps;
334
335
            // check if the PPS tree (starting from root) is complete
336
            if (isset($this->root) && $this->ppsTreeComplete($this->root->No)) {
337
                break;
338
            }
339
        }
340
        fclose($fh);
341
342
        // Initialize $pps->children on directories
343
        foreach ($this->_list as $pps) {
344
            if ($pps->Type == self::OLE_PPS_TYPE_DIR || $pps->Type == self::OLE_PPS_TYPE_ROOT) {
345
                $nos = [$pps->DirPps];
346
                $pps->children = [];
347
                while ($nos) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $nos of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
348
                    $no = array_pop($nos);
349
                    if ($no != -1) {
350
                        $childPps = $this->_list[$no];
351
                        $nos[] = $childPps->PrevPps;
352
                        $nos[] = $childPps->NextPps;
353
                        $pps->children[] = $childPps;
354
                    }
355
                }
356
            }
357
        }
358
359
        return true;
360
    }
361
362
    /**
363
     * It checks whether the PPS tree is complete (all PPS's read)
364
     * starting with the given PPS (not necessarily root).
365
     *
366
     * @param int $index The index of the PPS from which we are checking
367
     *
368
     * @return bool Whether the PPS tree for the given PPS is complete
369
     */
370
    private function ppsTreeComplete($index)
371
    {
372
        return isset($this->_list[$index]) &&
373
            ($pps = $this->_list[$index]) &&
374
            ($pps->PrevPps == -1 ||
375
                $this->ppsTreeComplete($pps->PrevPps)) &&
376
            ($pps->NextPps == -1 ||
377
                $this->ppsTreeComplete($pps->NextPps)) &&
378
            ($pps->DirPps == -1 ||
379
                $this->ppsTreeComplete($pps->DirPps));
380
    }
381
382
    /**
383
     * Checks whether a PPS is a File PPS or not.
384
     * If there is no PPS for the index given, it will return false.
385
     *
386
     * @param int $index The index for the PPS
387
     *
388
     * @return bool true if it's a File PPS, false otherwise
389
     */
390
    public function isFile($index)
391
    {
392
        if (isset($this->_list[$index])) {
393
            return $this->_list[$index]->Type == self::OLE_PPS_TYPE_FILE;
394
        }
395
396
        return false;
397
    }
398
399
    /**
400
     * Checks whether a PPS is a Root PPS or not.
401
     * If there is no PPS for the index given, it will return false.
402
     *
403
     * @param int $index the index for the PPS
404
     *
405
     * @return bool true if it's a Root PPS, false otherwise
406
     */
407
    public function isRoot($index)
408
    {
409
        if (isset($this->_list[$index])) {
410
            return $this->_list[$index]->Type == self::OLE_PPS_TYPE_ROOT;
411
        }
412
413
        return false;
414
    }
415
416
    /**
417
     * Gives the total number of PPS's found in the OLE container.
418
     *
419
     * @return int The total number of PPS's found in the OLE container
420
     */
421
    public function ppsTotal()
422
    {
423
        return count($this->_list);
424
    }
425
426
    /**
427
     * Gets data from a PPS
428
     * If there is no PPS for the index given, it will return an empty string.
429
     *
430
     * @param int $index The index for the PPS
431
     * @param int $position The position from which to start reading
432
     *                          (relative to the PPS)
433
     * @param int $length The amount of bytes to read (at most)
434
     *
435
     * @return string The binary string containing the data requested
436
     *
437
     * @see OLE_PPS_File::getStream()
438
     */
439
    public function getData($index, $position, $length)
440
    {
441
        // if position is not valid return empty string
442
        if (!isset($this->_list[$index]) || ($position >= $this->_list[$index]->Size) || ($position < 0)) {
443
            return '';
444
        }
445
        $fh = $this->getStream($this->_list[$index]);
446
        $data = stream_get_contents($fh, $length, $position);
447
        fclose($fh);
448
449
        return $data;
450
    }
451
452
    /**
453
     * Gets the data length from a PPS
454
     * If there is no PPS for the index given, it will return 0.
455
     *
456
     * @param int $index The index for the PPS
457
     *
458
     * @return int The amount of bytes in data the PPS has
459
     */
460
    public function getDataLength($index)
461
    {
462
        if (isset($this->_list[$index])) {
463
            return $this->_list[$index]->Size;
464
        }
465
466
        return 0;
467
    }
468
469
    /**
470
     * Utility function to transform ASCII text to Unicode.
471
     *
472
     * @param string $ascii The ASCII string to transform
473
     *
474
     * @return string The string in Unicode
475
     */
476
    public static function ascToUcs($ascii)
477
    {
478 51
        $rawname = '';
479
        $iMax = strlen($ascii);
480 51
        for ($i = 0; $i < $iMax; ++$i) {
481 51
            $rawname .= $ascii[$i]
482 51
                . "\x00";
483 51
        }
484 51
485
        return $rawname;
486
    }
487 51
488
    /**
489
     * Utility function
490
     * Returns a string for the OLE container with the date given.
491
     *
492
     * @param int $date A timestamp
493
     *
494
     * @return string The string for the OLE container
495
     */
496
    public static function localDateToOLE($date)
497
    {
498 51
        if (!isset($date)) {
499
            return "\x00\x00\x00\x00\x00\x00\x00\x00";
500 51
        }
501 51
502
        // factor used for separating numbers into 4 bytes parts
503
        $factor = 2 ** 32;
504
505 51
        // days from 1-1-1601 until the beggining of UNIX era
506
        $days = 134774;
507
        // calculate seconds
508 51
        $big_date = $days * 24 * 3600 + mktime((int) date('H', $date), (int) date('i', $date), (int) date('s', $date), (int) date('m', $date), (int) date('d', $date), (int) date('Y', $date));
509
        // multiply just to make MS happy
510 51
        $big_date *= 10000000;
511
512 51
        $high_part = floor($big_date / $factor);
513
        // lower 4 bytes
514 51
        $low_part = floor((($big_date / $factor) - $high_part) * $factor);
515
516 51
        // Make HEX string
517
        $res = '';
518
519 51
        for ($i = 0; $i < 4; ++$i) {
520
            $hex = $low_part % 0x100;
521 51
            $res .= pack('c', $hex);
522 51
            $low_part /= 0x100;
523 51
        }
524 51
        for ($i = 0; $i < 4; ++$i) {
525
            $hex = $high_part % 0x100;
526 51
            $res .= pack('c', $hex);
527 51
            $high_part /= 0x100;
528 51
        }
529 51
530
        return $res;
531
    }
532 51
533
    /**
534
     * Returns a timestamp from an OLE container's date.
535
     *
536
     * @param string $oleTimestamp A binary string with the encoded date
537
     *
538
     * @return int The Unix timestamp corresponding to the string
539
     */
540
    public static function OLE2LocalDate($oleTimestamp)
541
    {
542 30
        if (strlen($oleTimestamp) != 8) {
543
            throw new ReaderException('Expecting 8 byte string');
544 30
        }
545
546
        // convert to units of 100 ns since 1601:
547
        $unpackedTimestamp = unpack('v4', $oleTimestamp);
548
        $timestampHigh = (float) $unpackedTimestamp[4] * 65536 + (float) $unpackedTimestamp[3];
549 30
        $timestampLow = (float) $unpackedTimestamp[2] * 65536 + (float) $unpackedTimestamp[1];
550 30
551 30
        // translate to seconds since 1601:
552
        $timestampHigh /= 10000000;
553
        $timestampLow /= 10000000;
554 30
555 30
        // days from 1601 to 1970:
556
        $days = 134774;
557
558 30
        // translate to seconds since 1970:
559
        $unixTimestamp = floor(65536.0 * 65536.0 * $timestampHigh + $timestampLow - $days * 24 * 3600 + 0.5);
560
561 30
        $iTimestamp = (int) $unixTimestamp;
562
563 30
        // Overflow conditions can't happen on 64-bit system
564 30
        return ($iTimestamp == $unixTimestamp) ? $iTimestamp : ($unixTimestamp >= 0.0 ? PHP_INT_MAX : PHP_INT_MIN);
565
    }
566
}
567