Failed Conditions
Push — master ( bf4629...7712d5 )
by Adrien
27:59 queued 18:08
created

Xls::readPalette()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 0
dl 0
loc 16
ccs 9
cts 9
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
5
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6
use PhpOffice\PhpSpreadsheet\Cell\DataType;
7
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
8
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
9
use PhpOffice\PhpSpreadsheet\NamedRange;
10
use PhpOffice\PhpSpreadsheet\Reader\Xls\ConditionalFormatting;
11
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont;
12
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\FillPattern;
13
use PhpOffice\PhpSpreadsheet\RichText\RichText;
14
use PhpOffice\PhpSpreadsheet\Shared\CodePage;
15
use PhpOffice\PhpSpreadsheet\Shared\Date;
16
use PhpOffice\PhpSpreadsheet\Shared\Escher;
17
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer;
18
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
19
use PhpOffice\PhpSpreadsheet\Shared\File;
20
use PhpOffice\PhpSpreadsheet\Shared\OLE;
21
use PhpOffice\PhpSpreadsheet\Shared\OLERead;
22
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
23
use PhpOffice\PhpSpreadsheet\Shared\Xls as SharedXls;
24
use PhpOffice\PhpSpreadsheet\Spreadsheet;
25
use PhpOffice\PhpSpreadsheet\Style\Alignment;
26
use PhpOffice\PhpSpreadsheet\Style\Borders;
27
use PhpOffice\PhpSpreadsheet\Style\Conditional;
28
use PhpOffice\PhpSpreadsheet\Style\Fill;
29
use PhpOffice\PhpSpreadsheet\Style\Font;
30
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
31
use PhpOffice\PhpSpreadsheet\Style\Protection;
32
use PhpOffice\PhpSpreadsheet\Style\Style;
33
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
34
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
35
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
36
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
37
38
// Original file header of ParseXL (used as the base for this class):
39
// --------------------------------------------------------------------------------
40
// Adapted from Excel_Spreadsheet_Reader developed by users bizon153,
41
// trex005, and mmp11 (SourceForge.net)
42
// https://sourceforge.net/projects/phpexcelreader/
43
// Primary changes made by canyoncasa (dvc) for ParseXL 1.00 ...
44
//     Modelled moreso after Perl Excel Parse/Write modules
45
//     Added Parse_Excel_Spreadsheet object
46
//         Reads a whole worksheet or tab as row,column array or as
47
//         associated hash of indexed rows and named column fields
48
//     Added variables for worksheet (tab) indexes and names
49
//     Added an object call for loading individual woorksheets
50
//     Changed default indexing defaults to 0 based arrays
51
//     Fixed date/time and percent formats
52
//     Includes patches found at SourceForge...
53
//         unicode patch by nobody
54
//         unpack("d") machine depedency patch by matchy
55
//         boundsheet utf16 patch by bjaenichen
56
//     Renamed functions for shorter names
57
//     General code cleanup and rigor, including <80 column width
58
//     Included a testcase Excel file and PHP example calls
59
//     Code works for PHP 5.x
60
61
// Primary changes made by canyoncasa (dvc) for ParseXL 1.10 ...
62
// http://sourceforge.net/tracker/index.php?func=detail&aid=1466964&group_id=99160&atid=623334
63
//     Decoding of formula conditions, results, and tokens.
64
//     Support for user-defined named cells added as an array "namedcells"
65
//         Patch code for user-defined named cells supports single cells only.
66
//         NOTE: this patch only works for BIFF8 as BIFF5-7 use a different
67
//         external sheet reference structure
68
class Xls extends BaseReader
69
{
70
    // ParseXL definitions
71
    const XLS_BIFF8 = 0x0600;
72
    const XLS_BIFF7 = 0x0500;
73
    const XLS_WORKBOOKGLOBALS = 0x0005;
74
    const XLS_WORKSHEET = 0x0010;
75
76
    // record identifiers
77
    const XLS_TYPE_FORMULA = 0x0006;
78
    const XLS_TYPE_EOF = 0x000a;
79
    const XLS_TYPE_PROTECT = 0x0012;
80
    const XLS_TYPE_OBJECTPROTECT = 0x0063;
81
    const XLS_TYPE_SCENPROTECT = 0x00dd;
82
    const XLS_TYPE_PASSWORD = 0x0013;
83
    const XLS_TYPE_HEADER = 0x0014;
84
    const XLS_TYPE_FOOTER = 0x0015;
85
    const XLS_TYPE_EXTERNSHEET = 0x0017;
86
    const XLS_TYPE_DEFINEDNAME = 0x0018;
87
    const XLS_TYPE_VERTICALPAGEBREAKS = 0x001a;
88
    const XLS_TYPE_HORIZONTALPAGEBREAKS = 0x001b;
89
    const XLS_TYPE_NOTE = 0x001c;
90
    const XLS_TYPE_SELECTION = 0x001d;
91
    const XLS_TYPE_DATEMODE = 0x0022;
92
    const XLS_TYPE_EXTERNNAME = 0x0023;
93
    const XLS_TYPE_LEFTMARGIN = 0x0026;
94
    const XLS_TYPE_RIGHTMARGIN = 0x0027;
95
    const XLS_TYPE_TOPMARGIN = 0x0028;
96
    const XLS_TYPE_BOTTOMMARGIN = 0x0029;
97
    const XLS_TYPE_PRINTGRIDLINES = 0x002b;
98
    const XLS_TYPE_FILEPASS = 0x002f;
99
    const XLS_TYPE_FONT = 0x0031;
100
    const XLS_TYPE_CONTINUE = 0x003c;
101
    const XLS_TYPE_PANE = 0x0041;
102
    const XLS_TYPE_CODEPAGE = 0x0042;
103
    const XLS_TYPE_DEFCOLWIDTH = 0x0055;
104
    const XLS_TYPE_OBJ = 0x005d;
105
    const XLS_TYPE_COLINFO = 0x007d;
106
    const XLS_TYPE_IMDATA = 0x007f;
107
    const XLS_TYPE_SHEETPR = 0x0081;
108
    const XLS_TYPE_HCENTER = 0x0083;
109
    const XLS_TYPE_VCENTER = 0x0084;
110
    const XLS_TYPE_SHEET = 0x0085;
111
    const XLS_TYPE_PALETTE = 0x0092;
112
    const XLS_TYPE_SCL = 0x00a0;
113
    const XLS_TYPE_PAGESETUP = 0x00a1;
114
    const XLS_TYPE_MULRK = 0x00bd;
115
    const XLS_TYPE_MULBLANK = 0x00be;
116
    const XLS_TYPE_DBCELL = 0x00d7;
117
    const XLS_TYPE_XF = 0x00e0;
118
    const XLS_TYPE_MERGEDCELLS = 0x00e5;
119
    const XLS_TYPE_MSODRAWINGGROUP = 0x00eb;
120
    const XLS_TYPE_MSODRAWING = 0x00ec;
121
    const XLS_TYPE_SST = 0x00fc;
122
    const XLS_TYPE_LABELSST = 0x00fd;
123
    const XLS_TYPE_EXTSST = 0x00ff;
124
    const XLS_TYPE_EXTERNALBOOK = 0x01ae;
125
    const XLS_TYPE_DATAVALIDATIONS = 0x01b2;
126
    const XLS_TYPE_TXO = 0x01b6;
127
    const XLS_TYPE_HYPERLINK = 0x01b8;
128
    const XLS_TYPE_DATAVALIDATION = 0x01be;
129
    const XLS_TYPE_DIMENSION = 0x0200;
130
    const XLS_TYPE_BLANK = 0x0201;
131
    const XLS_TYPE_NUMBER = 0x0203;
132
    const XLS_TYPE_LABEL = 0x0204;
133
    const XLS_TYPE_BOOLERR = 0x0205;
134
    const XLS_TYPE_STRING = 0x0207;
135
    const XLS_TYPE_ROW = 0x0208;
136
    const XLS_TYPE_INDEX = 0x020b;
137
    const XLS_TYPE_ARRAY = 0x0221;
138
    const XLS_TYPE_DEFAULTROWHEIGHT = 0x0225;
139
    const XLS_TYPE_WINDOW2 = 0x023e;
140
    const XLS_TYPE_RK = 0x027e;
141
    const XLS_TYPE_STYLE = 0x0293;
142
    const XLS_TYPE_FORMAT = 0x041e;
143
    const XLS_TYPE_SHAREDFMLA = 0x04bc;
144
    const XLS_TYPE_BOF = 0x0809;
145
    const XLS_TYPE_SHEETPROTECTION = 0x0867;
146
    const XLS_TYPE_RANGEPROTECTION = 0x0868;
147
    const XLS_TYPE_SHEETLAYOUT = 0x0862;
148
    const XLS_TYPE_XFEXT = 0x087d;
149
    const XLS_TYPE_PAGELAYOUTVIEW = 0x088b;
150
    const XLS_TYPE_CFHEADER = 0x01b0;
151
    const XLS_TYPE_CFRULE = 0x01b1;
152
    const XLS_TYPE_UNKNOWN = 0xffff;
153
154
    // Encryption type
155
    const MS_BIFF_CRYPTO_NONE = 0;
156
    const MS_BIFF_CRYPTO_XOR = 1;
157
    const MS_BIFF_CRYPTO_RC4 = 2;
158
159
    // Size of stream blocks when using RC4 encryption
160
    const REKEY_BLOCK = 0x400;
161
162
    /**
163
     * Summary Information stream data.
164
     *
165
     * @var ?string
166
     */
167
    private $summaryInformation;
168
169
    /**
170
     * Extended Summary Information stream data.
171
     *
172
     * @var ?string
173
     */
174
    private $documentSummaryInformation;
175
176
    /**
177
     * Workbook stream data. (Includes workbook globals substream as well as sheet substreams).
178
     *
179
     * @var string
180
     */
181
    private $data;
182
183
    /**
184
     * Size in bytes of $this->data.
185
     *
186
     * @var int
187
     */
188
    private $dataSize;
189
190
    /**
191
     * Current position in stream.
192
     *
193
     * @var int
194
     */
195
    private $pos;
196
197
    /**
198
     * Workbook to be returned by the reader.
199
     *
200
     * @var Spreadsheet
201
     */
202
    private $spreadsheet;
203
204
    /**
205
     * Worksheet that is currently being built by the reader.
206
     *
207
     * @var Worksheet
208
     */
209
    private $phpSheet;
210
211
    /**
212
     * BIFF version.
213
     *
214
     * @var int
215
     */
216
    private $version = 0;
217
218
    /**
219
     * Codepage set in the Excel file being read. Only important for BIFF5 (Excel 5.0 - Excel 95)
220
     * For BIFF8 (Excel 97 - Excel 2003) this will always have the value 'UTF-16LE'.
221
     */
222
    private string $codepage = '';
223
224
    /**
225
     * Shared formats.
226
     *
227
     * @var array
228
     */
229
    private $formats;
230
231
    /**
232
     * Shared fonts.
233
     *
234
     * @var Font[]
235
     */
236
    private $objFonts;
237
238
    /**
239
     * Color palette.
240
     *
241
     * @var array
242
     */
243
    private $palette;
244
245
    /**
246
     * Worksheets.
247
     *
248
     * @var array
249
     */
250
    private $sheets;
251
252
    /**
253
     * External books.
254
     *
255
     * @var array
256
     */
257
    private $externalBooks;
258
259
    /**
260
     * REF structures. Only applies to BIFF8.
261
     *
262
     * @var array
263
     */
264
    private $ref;
265
266
    /**
267
     * External names.
268
     *
269
     * @var array
270
     */
271
    private $externalNames;
272
273
    /**
274
     * Defined names.
275
     *
276
     * @var array
277
     */
278
    private $definedname;
279
280
    /**
281
     * Shared strings. Only applies to BIFF8.
282
     *
283
     * @var array
284
     */
285
    private $sst;
286
287
    /**
288
     * Panes are frozen? (in sheet currently being read). See WINDOW2 record.
289
     *
290
     * @var bool
291
     */
292
    private $frozen;
293
294
    /**
295
     * Fit printout to number of pages? (in sheet currently being read). See SHEETPR record.
296
     *
297
     * @var bool
298
     */
299
    private $isFitToPages;
300
301
    /**
302
     * Objects. One OBJ record contributes with one entry.
303
     *
304
     * @var array
305
     */
306
    private $objs;
307
308
    /**
309
     * Text Objects. One TXO record corresponds with one entry.
310
     *
311
     * @var array
312
     */
313
    private $textObjects;
314
315
    /**
316
     * Cell Annotations (BIFF8).
317
     *
318
     * @var array
319
     */
320
    private $cellNotes;
321
322
    /**
323
     * The combined MSODRAWINGGROUP data.
324
     *
325
     * @var string
326
     */
327
    private $drawingGroupData;
328
329
    /**
330
     * The combined MSODRAWING data (per sheet).
331
     *
332
     * @var string
333
     */
334
    private $drawingData;
335
336
    /**
337
     * Keep track of XF index.
338
     *
339
     * @var int
340
     */
341
    private $xfIndex;
342
343
    /**
344
     * Mapping of XF index (that is a cell XF) to final index in cellXf collection.
345
     *
346
     * @var array
347
     */
348
    private $mapCellXfIndex;
349
350
    /**
351
     * Mapping of XF index (that is a style XF) to final index in cellStyleXf collection.
352
     *
353
     * @var array
354
     */
355
    private $mapCellStyleXfIndex;
356
357
    /**
358
     * The shared formulas in a sheet. One SHAREDFMLA record contributes with one value.
359
     *
360
     * @var array
361
     */
362
    private $sharedFormulas;
363
364
    /**
365
     * The shared formula parts in a sheet. One FORMULA record contributes with one value if it
366
     * refers to a shared formula.
367
     *
368
     * @var array
369
     */
370
    private $sharedFormulaParts;
371
372
    /**
373
     * The type of encryption in use.
374
     *
375
     * @var int
376
     */
377
    private $encryption = 0;
378
379
    /**
380
     * The position in the stream after which contents are encrypted.
381
     *
382
     * @var int
383
     */
384
    private $encryptionStartPos = 0;
385
386
    /**
387
     * The current RC4 decryption object.
388
     *
389
     * @var ?Xls\RC4
390
     */
391
    private $rc4Key;
392
393
    /**
394
     * The position in the stream that the RC4 decryption object was left at.
395
     *
396
     * @var int
397
     */
398
    private $rc4Pos = 0;
399
400
    /**
401
     * The current MD5 context state.
402
     * It is never set in the program, so code which uses it is suspect.
403
     *
404
     * @var string
405
     */
406
    private $md5Ctxt; // @phpstan-ignore-line
407
408
    /**
409
     * @var int
410
     */
411
    private $textObjRef;
412
413
    /**
414
     * @var string
415
     */
416
    private $baseCell;
417
418
    /** @var bool */
419
    private $activeSheetSet = false;
420
421
    /**
422
     * Create a new Xls Reader instance.
423
     */
424 119
    public function __construct()
425
    {
426 119
        parent::__construct();
427
    }
428
429
    /**
430
     * Can the current IReader read the file?
431
     */
432 19
    public function canRead(string $filename): bool
433
    {
434 19
        if (File::testFileNoThrow($filename) === false) {
435 1
            return false;
436
        }
437
438
        try {
439
            // Use ParseXL for the hard work.
440 18
            $ole = new OLERead();
441
442
            // get excel data
443 18
            $ole->read($filename);
444 11
            if ($ole->wrkbook === null) {
445 3
                throw new Exception('The filename ' . $filename . ' is not recognised as a Spreadsheet file');
446
            }
447
448 8
            return true;
449 10
        } catch (PhpSpreadsheetException) {
450 10
            return false;
451
        }
452
    }
453
454
    public function setCodepage(string $codepage): void
455
    {
456
        if (CodePage::validate($codepage) === false) {
457
            throw new PhpSpreadsheetException('Unknown codepage: ' . $codepage);
458
        }
459
460
        $this->codepage = $codepage;
461
    }
462
463 5
    public function getCodepage(): string
464
    {
465 5
        return $this->codepage;
466
    }
467
468
    /**
469
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
470
     */
471 6
    public function listWorksheetNames(string $filename): array
472
    {
473 6
        File::assertFile($filename);
474
475 6
        $worksheetNames = [];
476
477
        // Read the OLE file
478 6
        $this->loadOLE($filename);
479
480
        // total byte size of Excel data (workbook global substream + sheet substreams)
481 6
        $this->dataSize = strlen($this->data);
482
483 6
        $this->pos = 0;
484 6
        $this->sheets = [];
485
486
        // Parse Workbook Global Substream
487 6
        while ($this->pos < $this->dataSize) {
488 6
            $code = self::getUInt2d($this->data, $this->pos);
489
490 6
            match ($code) {
491 6
                self::XLS_TYPE_BOF => $this->readBof(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readBof() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readBof() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
492 6
                self::XLS_TYPE_SHEET => $this->readSheet(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readSheet() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readSheet() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
493 6
                self::XLS_TYPE_EOF => $this->readDefault(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readDefault() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readDefault() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
494 6
                self::XLS_TYPE_CODEPAGE => $this->readCodepage(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readCodepage() targeting PhpOffice\PhpSpreadsheet...der\Xls::readCodepage() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
495 6
                default => $this->readDefault(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readDefault() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readDefault() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
496 6
            };
497
498 6
            if ($code === self::XLS_TYPE_EOF) {
499 6
                break;
500
            }
501
        }
502
503 6
        foreach ($this->sheets as $sheet) {
504 6
            if ($sheet['sheetType'] != 0x00) {
505
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
506
                continue;
507
            }
508
509 6
            $worksheetNames[] = $sheet['name'];
510
        }
511
512 6
        return $worksheetNames;
513
    }
514
515
    /**
516
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
517
     */
518 4
    public function listWorksheetInfo(string $filename): array
519
    {
520 4
        File::assertFile($filename);
521
522 4
        $worksheetInfo = [];
523
524
        // Read the OLE file
525 4
        $this->loadOLE($filename);
526
527
        // total byte size of Excel data (workbook global substream + sheet substreams)
528 4
        $this->dataSize = strlen($this->data);
529
530
        // initialize
531 4
        $this->pos = 0;
532 4
        $this->sheets = [];
533
534
        // Parse Workbook Global Substream
535 4
        while ($this->pos < $this->dataSize) {
536 4
            $code = self::getUInt2d($this->data, $this->pos);
537
538 4
            match ($code) {
539 4
                self::XLS_TYPE_BOF => $this->readBof(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readBof() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readBof() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
540 4
                self::XLS_TYPE_SHEET => $this->readSheet(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readSheet() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readSheet() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
541 4
                self::XLS_TYPE_EOF => $this->readDefault(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readDefault() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readDefault() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
542 4
                self::XLS_TYPE_CODEPAGE => $this->readCodepage(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readCodepage() targeting PhpOffice\PhpSpreadsheet...der\Xls::readCodepage() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
543 4
                default => $this->readDefault(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readDefault() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readDefault() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
544 4
            };
545
546 4
            if ($code === self::XLS_TYPE_EOF) {
547 4
                break;
548
            }
549
        }
550
551
        // Parse the individual sheets
552 4
        foreach ($this->sheets as $sheet) {
553 4
            if ($sheet['sheetType'] != 0x00) {
554
                // 0x00: Worksheet
555
                // 0x02: Chart
556
                // 0x06: Visual Basic module
557
                continue;
558
            }
559
560 4
            $tmpInfo = [];
561 4
            $tmpInfo['worksheetName'] = $sheet['name'];
562 4
            $tmpInfo['lastColumnLetter'] = 'A';
563 4
            $tmpInfo['lastColumnIndex'] = 0;
564 4
            $tmpInfo['totalRows'] = 0;
565 4
            $tmpInfo['totalColumns'] = 0;
566
567 4
            $this->pos = $sheet['offset'];
568
569 4
            while ($this->pos <= $this->dataSize - 4) {
570 4
                $code = self::getUInt2d($this->data, $this->pos);
571
572
                switch ($code) {
573
                    case self::XLS_TYPE_RK:
574
                    case self::XLS_TYPE_LABELSST:
575
                    case self::XLS_TYPE_NUMBER:
576
                    case self::XLS_TYPE_FORMULA:
577
                    case self::XLS_TYPE_BOOLERR:
578
                    case self::XLS_TYPE_LABEL:
579 4
                        $length = self::getUInt2d($this->data, $this->pos + 2);
580 4
                        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
581
582
                        // move stream pointer to next record
583 4
                        $this->pos += 4 + $length;
584
585 4
                        $rowIndex = self::getUInt2d($recordData, 0) + 1;
586 4
                        $columnIndex = self::getUInt2d($recordData, 2);
587
588 4
                        $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
589 4
                        $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);
590
591 4
                        break;
592
                    case self::XLS_TYPE_BOF:
593 4
                        $this->readBof();
594
595 4
                        break;
596
                    case self::XLS_TYPE_EOF:
597 4
                        $this->readDefault();
598
599 4
                        break 2;
600
                    default:
601 4
                        $this->readDefault();
602
603 4
                        break;
604
                }
605
            }
606
607 4
            $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
608 4
            $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
609
610 4
            $worksheetInfo[] = $tmpInfo;
611
        }
612
613 4
        return $worksheetInfo;
614
    }
615
616
    /**
617
     * Loads PhpSpreadsheet from file.
618
     */
619 94
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
620
    {
621
        // Read the OLE file
622 94
        $this->loadOLE($filename);
623
624
        // Initialisations
625 94
        $this->spreadsheet = new Spreadsheet();
626 94
        $this->spreadsheet->removeSheetByIndex(0); // remove 1st sheet
627 94
        if (!$this->readDataOnly) {
628 93
            $this->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style
629 93
            $this->spreadsheet->removeCellXfByIndex(0); // remove the default style
630
        }
631
632
        // Read the summary information stream (containing meta data)
633 94
        $this->readSummaryInformation();
634
635
        // Read the Additional document summary information stream (containing application-specific meta data)
636 94
        $this->readDocumentSummaryInformation();
637
638
        // total byte size of Excel data (workbook global substream + sheet substreams)
639 94
        $this->dataSize = strlen($this->data);
640
641
        // initialize
642 94
        $this->pos = 0;
643 94
        $this->codepage = $this->codepage ?: CodePage::DEFAULT_CODE_PAGE;
644 94
        $this->formats = [];
645 94
        $this->objFonts = [];
646 94
        $this->palette = [];
647 94
        $this->sheets = [];
648 94
        $this->externalBooks = [];
649 94
        $this->ref = [];
650 94
        $this->definedname = [];
651 94
        $this->sst = [];
652 94
        $this->drawingGroupData = '';
653 94
        $this->xfIndex = 0;
654 94
        $this->mapCellXfIndex = [];
655 94
        $this->mapCellStyleXfIndex = [];
656
657
        // Parse Workbook Global Substream
658 94
        while ($this->pos < $this->dataSize) {
659 94
            $code = self::getUInt2d($this->data, $this->pos);
660
661 94
            match ($code) {
662 94
                self::XLS_TYPE_BOF => $this->readBof(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readBof() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readBof() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
663 94
                self::XLS_TYPE_FILEPASS => $this->readFilepass(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readFilepass() targeting PhpOffice\PhpSpreadsheet...der\Xls::readFilepass() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
664 94
                self::XLS_TYPE_CODEPAGE => $this->readCodepage(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readCodepage() targeting PhpOffice\PhpSpreadsheet...der\Xls::readCodepage() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
665 94
                self::XLS_TYPE_DATEMODE => $this->readDateMode(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readDateMode() targeting PhpOffice\PhpSpreadsheet...der\Xls::readDateMode() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
666 94
                self::XLS_TYPE_FONT => $this->readFont(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readFont() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readFont() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
667 94
                self::XLS_TYPE_FORMAT => $this->readFormat(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readFormat() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readFormat() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
668 94
                self::XLS_TYPE_XF => $this->readXf(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readXf() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readXf() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
669 94
                self::XLS_TYPE_XFEXT => $this->readXfExt(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readXfExt() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readXfExt() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
670 94
                self::XLS_TYPE_STYLE => $this->readStyle(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readStyle() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readStyle() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
671 94
                self::XLS_TYPE_PALETTE => $this->readPalette(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readPalette() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readPalette() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
672 94
                self::XLS_TYPE_SHEET => $this->readSheet(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readSheet() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readSheet() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
673 94
                self::XLS_TYPE_EXTERNALBOOK => $this->readExternalBook(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readExternalBook() targeting PhpOffice\PhpSpreadsheet...Xls::readExternalBook() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
674 94
                self::XLS_TYPE_EXTERNNAME => $this->readExternName(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readExternName() targeting PhpOffice\PhpSpreadsheet...r\Xls::readExternName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
675 94
                self::XLS_TYPE_EXTERNSHEET => $this->readExternSheet(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readExternSheet() targeting PhpOffice\PhpSpreadsheet...\Xls::readExternSheet() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
676 94
                self::XLS_TYPE_DEFINEDNAME => $this->readDefinedName(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readDefinedName() targeting PhpOffice\PhpSpreadsheet...\Xls::readDefinedName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
677 94
                self::XLS_TYPE_MSODRAWINGGROUP => $this->readMsoDrawingGroup(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readMsoDrawingGroup() targeting PhpOffice\PhpSpreadsheet...::readMsoDrawingGroup() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
678 94
                self::XLS_TYPE_SST => $this->readSst(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readSst() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readSst() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
679 94
                self::XLS_TYPE_EOF => $this->readDefault(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readDefault() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readDefault() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
680 94
                default => $this->readDefault(),
1 ignored issue
show
Bug introduced by
Are you sure the usage of $this->readDefault() targeting PhpOffice\PhpSpreadsheet\Reader\Xls::readDefault() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
681 94
            };
682
683 94
            if ($code === self::XLS_TYPE_EOF) {
684 94
                break;
685
            }
686
        }
687
688
        // Resolve indexed colors for font, fill, and border colors
689
        // Cannot be resolved already in XF record, because PALETTE record comes afterwards
690 94
        if (!$this->readDataOnly) {
691 93
            foreach ($this->objFonts as $objFont) {
692 93
                if (isset($objFont->colorIndex)) {
693 93
                    $color = Xls\Color::map($objFont->colorIndex, $this->palette, $this->version);
694 93
                    $objFont->getColor()->setRGB($color['rgb']);
695
                }
696
            }
697
698 93
            foreach ($this->spreadsheet->getCellXfCollection() as $objStyle) {
699
                // fill start and end color
700 93
                $fill = $objStyle->getFill();
701
702 93
                if (isset($fill->startcolorIndex)) {
703 93
                    $startColor = Xls\Color::map($fill->startcolorIndex, $this->palette, $this->version);
704 93
                    $fill->getStartColor()->setRGB($startColor['rgb']);
705
                }
706 93
                if (isset($fill->endcolorIndex)) {
707 93
                    $endColor = Xls\Color::map($fill->endcolorIndex, $this->palette, $this->version);
708 93
                    $fill->getEndColor()->setRGB($endColor['rgb']);
709
                }
710
711
                // border colors
712 93
                $top = $objStyle->getBorders()->getTop();
713 93
                $right = $objStyle->getBorders()->getRight();
714 93
                $bottom = $objStyle->getBorders()->getBottom();
715 93
                $left = $objStyle->getBorders()->getLeft();
716 93
                $diagonal = $objStyle->getBorders()->getDiagonal();
717
718 93
                if (isset($top->colorIndex)) {
719 93
                    $borderTopColor = Xls\Color::map($top->colorIndex, $this->palette, $this->version);
720 93
                    $top->getColor()->setRGB($borderTopColor['rgb']);
721
                }
722 93
                if (isset($right->colorIndex)) {
723 93
                    $borderRightColor = Xls\Color::map($right->colorIndex, $this->palette, $this->version);
724 93
                    $right->getColor()->setRGB($borderRightColor['rgb']);
725
                }
726 93
                if (isset($bottom->colorIndex)) {
727 93
                    $borderBottomColor = Xls\Color::map($bottom->colorIndex, $this->palette, $this->version);
728 93
                    $bottom->getColor()->setRGB($borderBottomColor['rgb']);
729
                }
730 93
                if (isset($left->colorIndex)) {
731 93
                    $borderLeftColor = Xls\Color::map($left->colorIndex, $this->palette, $this->version);
732 93
                    $left->getColor()->setRGB($borderLeftColor['rgb']);
733
                }
734 93
                if (isset($diagonal->colorIndex)) {
735 91
                    $borderDiagonalColor = Xls\Color::map($diagonal->colorIndex, $this->palette, $this->version);
736 91
                    $diagonal->getColor()->setRGB($borderDiagonalColor['rgb']);
737
                }
738
            }
739
        }
740
741
        // treat MSODRAWINGGROUP records, workbook-level Escher
742 94
        $escherWorkbook = null;
743 94
        if (!$this->readDataOnly && $this->drawingGroupData) {
744 17
            $escher = new Escher();
745 17
            $reader = new Xls\Escher($escher);
746 17
            $escherWorkbook = $reader->load($this->drawingGroupData);
747
        }
748
749
        // Parse the individual sheets
750 94
        $this->activeSheetSet = false;
751 94
        foreach ($this->sheets as $sheet) {
752 94
            if ($sheet['sheetType'] != 0x00) {
753
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
754
                continue;
755
            }
756
757
            // check if sheet should be skipped
758 94
            if (isset($this->loadSheetsOnly) && !in_array($sheet['name'], $this->loadSheetsOnly)) {
1 ignored issue
show
Bug introduced by
It seems like $this->loadSheetsOnly can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

758
            if (isset($this->loadSheetsOnly) && !in_array($sheet['name'], /** @scrutinizer ignore-type */ $this->loadSheetsOnly)) {
Loading history...
759 8
                continue;
760
            }
761
762
            // add sheet to PhpSpreadsheet object
763 93
            $this->phpSheet = $this->spreadsheet->createSheet();
764
            //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula
765
            //        cells... during the load, all formulae should be correct, and we're simply bringing the worksheet
766
            //        name in line with the formula, not the reverse
767 93
            $this->phpSheet->setTitle($sheet['name'], false, false);
768 93
            $this->phpSheet->setSheetState($sheet['sheetState']);
769
770 93
            $this->pos = $sheet['offset'];
771
772
            // Initialize isFitToPages. May change after reading SHEETPR record.
773 93
            $this->isFitToPages = false;
774
775
            // Initialize drawingData
776 93
            $this->drawingData = '';
777
778
            // Initialize objs
779 93
            $this->objs = [];
780
781
            // Initialize shared formula parts
782 93
            $this->sharedFormulaParts = [];
783
784
            // Initialize shared formulas
785 93
            $this->sharedFormulas = [];
786
787
            // Initialize text objs
788 93
            $this->textObjects = [];
789
790
            // Initialize cell annotations
791 93
            $this->cellNotes = [];
792 93
            $this->textObjRef = -1;
793
794 93
            while ($this->pos <= $this->dataSize - 4) {
795 93
                $code = self::getUInt2d($this->data, $this->pos);
796
797
                switch ($code) {
798
                    case self::XLS_TYPE_BOF:
799 93
                        $this->readBof();
800
801 93
                        break;
802
                    case self::XLS_TYPE_PRINTGRIDLINES:
803 90
                        $this->readPrintGridlines();
804
805 90
                        break;
806
                    case self::XLS_TYPE_DEFAULTROWHEIGHT:
807 51
                        $this->readDefaultRowHeight();
808
809 51
                        break;
810
                    case self::XLS_TYPE_SHEETPR:
811 92
                        $this->readSheetPr();
812
813 92
                        break;
814
                    case self::XLS_TYPE_HORIZONTALPAGEBREAKS:
815 4
                        $this->readHorizontalPageBreaks();
816
817 4
                        break;
818
                    case self::XLS_TYPE_VERTICALPAGEBREAKS:
819 4
                        $this->readVerticalPageBreaks();
820
821 4
                        break;
822
                    case self::XLS_TYPE_HEADER:
823 90
                        $this->readHeader();
824
825 90
                        break;
826
                    case self::XLS_TYPE_FOOTER:
827 90
                        $this->readFooter();
828
829 90
                        break;
830
                    case self::XLS_TYPE_HCENTER:
831 90
                        $this->readHcenter();
832
833 90
                        break;
834
                    case self::XLS_TYPE_VCENTER:
835 90
                        $this->readVcenter();
836
837 90
                        break;
838
                    case self::XLS_TYPE_LEFTMARGIN:
839 86
                        $this->readLeftMargin();
840
841 86
                        break;
842
                    case self::XLS_TYPE_RIGHTMARGIN:
843 86
                        $this->readRightMargin();
844
845 86
                        break;
846
                    case self::XLS_TYPE_TOPMARGIN:
847 86
                        $this->readTopMargin();
848
849 86
                        break;
850
                    case self::XLS_TYPE_BOTTOMMARGIN:
851 86
                        $this->readBottomMargin();
852
853 86
                        break;
854
                    case self::XLS_TYPE_PAGESETUP:
855 92
                        $this->readPageSetup();
856
857 92
                        break;
858
                    case self::XLS_TYPE_PROTECT:
859 6
                        $this->readProtect();
860
861 6
                        break;
862
                    case self::XLS_TYPE_SCENPROTECT:
863
                        $this->readScenProtect();
864
865
                        break;
866
                    case self::XLS_TYPE_OBJECTPROTECT:
867 1
                        $this->readObjectProtect();
868
869 1
                        break;
870
                    case self::XLS_TYPE_PASSWORD:
871 2
                        $this->readPassword();
872
873 2
                        break;
874
                    case self::XLS_TYPE_DEFCOLWIDTH:
875 91
                        $this->readDefColWidth();
876
877 91
                        break;
878
                    case self::XLS_TYPE_COLINFO:
879 84
                        $this->readColInfo();
880
881 84
                        break;
882
                    case self::XLS_TYPE_DIMENSION:
883 93
                        $this->readDefault();
884
885 93
                        break;
886
                    case self::XLS_TYPE_ROW:
887 58
                        $this->readRow();
888
889 58
                        break;
890
                    case self::XLS_TYPE_DBCELL:
891 45
                        $this->readDefault();
892
893 45
                        break;
894
                    case self::XLS_TYPE_RK:
895 27
                        $this->readRk();
896
897 27
                        break;
898
                    case self::XLS_TYPE_LABELSST:
899 58
                        $this->readLabelSst();
900
901 58
                        break;
902
                    case self::XLS_TYPE_MULRK:
903 21
                        $this->readMulRk();
904
905 21
                        break;
906
                    case self::XLS_TYPE_NUMBER:
907 46
                        $this->readNumber();
908
909 46
                        break;
910
                    case self::XLS_TYPE_FORMULA:
911 29
                        $this->readFormula();
912
913 29
                        break;
914
                    case self::XLS_TYPE_SHAREDFMLA:
915
                        $this->readSharedFmla();
916
917
                        break;
918
                    case self::XLS_TYPE_BOOLERR:
919 10
                        $this->readBoolErr();
920
921 10
                        break;
922
                    case self::XLS_TYPE_MULBLANK:
923 25
                        $this->readMulBlank();
924
925 25
                        break;
926
                    case self::XLS_TYPE_LABEL:
927 3
                        $this->readLabel();
928
929 3
                        break;
930
                    case self::XLS_TYPE_BLANK:
931 24
                        $this->readBlank();
932
933 24
                        break;
934
                    case self::XLS_TYPE_MSODRAWING:
935 16
                        $this->readMsoDrawing();
936
937 16
                        break;
938
                    case self::XLS_TYPE_OBJ:
939 12
                        $this->readObj();
940
941 12
                        break;
942
                    case self::XLS_TYPE_WINDOW2:
943 93
                        $this->readWindow2();
944
945 93
                        break;
946
                    case self::XLS_TYPE_PAGELAYOUTVIEW:
947 81
                        $this->readPageLayoutView();
948
949 81
                        break;
950
                    case self::XLS_TYPE_SCL:
951 5
                        $this->readScl();
952
953 5
                        break;
954
                    case self::XLS_TYPE_PANE:
955 8
                        $this->readPane();
956
957 8
                        break;
958
                    case self::XLS_TYPE_SELECTION:
959 90
                        $this->readSelection();
960
961 90
                        break;
962
                    case self::XLS_TYPE_MERGEDCELLS:
963 18
                        $this->readMergedCells();
964
965 18
                        break;
966
                    case self::XLS_TYPE_HYPERLINK:
967 6
                        $this->readHyperLink();
968
969 6
                        break;
970
                    case self::XLS_TYPE_DATAVALIDATIONS:
971 3
                        $this->readDataValidations();
972
973 3
                        break;
974
                    case self::XLS_TYPE_DATAVALIDATION:
975 3
                        $this->readDataValidation();
976
977 3
                        break;
978
                    case self::XLS_TYPE_CFHEADER:
979 16
                        $cellRangeAddresses = $this->readCFHeader();
980
981 16
                        break;
982
                    case self::XLS_TYPE_CFRULE:
983 16
                        $this->readCFRule($cellRangeAddresses ?? []);
984
985 16
                        break;
986
                    case self::XLS_TYPE_SHEETLAYOUT:
987 5
                        $this->readSheetLayout();
988
989 5
                        break;
990
                    case self::XLS_TYPE_SHEETPROTECTION:
991 85
                        $this->readSheetProtection();
992
993 85
                        break;
994
                    case self::XLS_TYPE_RANGEPROTECTION:
995 1
                        $this->readRangeProtection();
996
997 1
                        break;
998
                    case self::XLS_TYPE_NOTE:
999 3
                        $this->readNote();
1000
1001 3
                        break;
1002
                    case self::XLS_TYPE_TXO:
1003 2
                        $this->readTextObject();
1004
1005 2
                        break;
1006
                    case self::XLS_TYPE_CONTINUE:
1007 1
                        $this->readContinue();
1008
1009 1
                        break;
1010
                    case self::XLS_TYPE_EOF:
1011 93
                        $this->readDefault();
1012
1013 93
                        break 2;
1014
                    default:
1015 92
                        $this->readDefault();
1016
1017 92
                        break;
1018
                }
1019
            }
1020
1021
            // treat MSODRAWING records, sheet-level Escher
1022 93
            if (!$this->readDataOnly && $this->drawingData) {
1023 16
                $escherWorksheet = new Escher();
1024 16
                $reader = new Xls\Escher($escherWorksheet);
1025 16
                $escherWorksheet = $reader->load($this->drawingData);
1026
1027
                // get all spContainers in one long array, so they can be mapped to OBJ records
1028
                /** @var SpContainer[] */
1029 16
                $allSpContainers = method_exists($escherWorksheet, 'getDgContainer') ? $escherWorksheet->getDgContainer()->getSpgrContainer()->getAllSpContainers() : [];
1030
            }
1031
1032
            // treat OBJ records
1033 93
            foreach ($this->objs as $n => $obj) {
1034
                // the first shape container never has a corresponding OBJ record, hence $n + 1
1035 11
                if (isset($allSpContainers[$n + 1])) {
1036 11
                    $spContainer = $allSpContainers[$n + 1];
1037
1038
                    // we skip all spContainers that are a part of a group shape since we cannot yet handle those
1039 11
                    if ($spContainer->getNestingLevel() > 1) {
1040
                        continue;
1041
                    }
1042
1043
                    // calculate the width and height of the shape
1044
                    /** @var int $startRow */
1045 11
                    [$startColumn, $startRow] = Coordinate::coordinateFromString($spContainer->getStartCoordinates());
1046
                    /** @var int $endRow */
1047 11
                    [$endColumn, $endRow] = Coordinate::coordinateFromString($spContainer->getEndCoordinates());
1048
1049 11
                    $startOffsetX = $spContainer->getStartOffsetX();
1050 11
                    $startOffsetY = $spContainer->getStartOffsetY();
1051 11
                    $endOffsetX = $spContainer->getEndOffsetX();
1052 11
                    $endOffsetY = $spContainer->getEndOffsetY();
1053
1054 11
                    $width = SharedXls::getDistanceX($this->phpSheet, $startColumn, $startOffsetX, $endColumn, $endOffsetX);
1055 11
                    $height = SharedXls::getDistanceY($this->phpSheet, $startRow, $startOffsetY, $endRow, $endOffsetY);
1056
1057
                    // calculate offsetX and offsetY of the shape
1058 11
                    $offsetX = (int) ($startOffsetX * SharedXls::sizeCol($this->phpSheet, $startColumn) / 1024);
1059 11
                    $offsetY = (int) ($startOffsetY * SharedXls::sizeRow($this->phpSheet, $startRow) / 256);
1060
1061 11
                    switch ($obj['otObjType']) {
1062 11
                        case 0x19:
1063
                            // Note
1064 2
                            if (isset($this->cellNotes[$obj['idObjID']])) {
1065 2
                                $cellNote = $this->cellNotes[$obj['idObjID']];
0 ignored issues
show
Unused Code introduced by
The assignment to $cellNote is dead and can be removed.
Loading history...
1066
1067 2
                                if (isset($this->textObjects[$obj['idObjID']])) {
1068 2
                                    $textObject = $this->textObjects[$obj['idObjID']];
1069 2
                                    $this->cellNotes[$obj['idObjID']]['objTextData'] = $textObject;
1070
                                }
1071
                            }
1072
1073 2
                            break;
1074 11
                        case 0x08:
1075
                            // picture
1076
                            // get index to BSE entry (1-based)
1077 11
                            $BSEindex = $spContainer->getOPT(0x0104);
1078
1079
                            // If there is no BSE Index, we will fail here and other fields are not read.
1080
                            // Fix by checking here.
1081
                            // TODO: Why is there no BSE Index? Is this a new Office Version? Password protected field?
1082
                            // More likely : a uncompatible picture
1083 11
                            if (!$BSEindex) {
1084
                                continue 2;
1085
                            }
1086
1087 11
                            if ($escherWorkbook) {
1088 11
                                $BSECollection = method_exists($escherWorkbook, 'getDggContainer') ? $escherWorkbook->getDggContainer()->getBstoreContainer()->getBSECollection() : [];
1089 11
                                $BSE = $BSECollection[$BSEindex - 1];
1090 11
                                $blipType = $BSE->getBlipType();
1091
1092
                                // need check because some blip types are not supported by Escher reader such as EMF
1093 11
                                if ($blip = $BSE->getBlip()) {
1094 11
                                    $ih = imagecreatefromstring($blip->getData());
1095 11
                                    if ($ih !== false) {
1096 11
                                        $drawing = new MemoryDrawing();
1097 11
                                        $drawing->setImageResource($ih);
1 ignored issue
show
Bug introduced by
It seems like $ih can also be of type resource; however, parameter $value of PhpOffice\PhpSpreadsheet...ing::setImageResource() does only seem to accept GdImage|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1097
                                        $drawing->setImageResource(/** @scrutinizer ignore-type */ $ih);
Loading history...
1098
1099
                                        // width, height, offsetX, offsetY
1100 11
                                        $drawing->setResizeProportional(false);
1101 11
                                        $drawing->setWidth($width);
1102 11
                                        $drawing->setHeight($height);
1103 11
                                        $drawing->setOffsetX($offsetX);
1104 11
                                        $drawing->setOffsetY($offsetY);
1105
1106
                                        switch ($blipType) {
1107 10
                                            case BSE::BLIPTYPE_JPEG:
1108 9
                                                $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG);
1109 9
                                                $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG);
1110
1111 9
                                                break;
1112 10
                                            case BSE::BLIPTYPE_PNG:
1113 11
                                                imagealphablending($ih, false);
1114 11
                                                imagesavealpha($ih, true);
1115 11
                                                $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG);
1116 11
                                                $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG);
1117
1118 11
                                                break;
1119
                                        }
1120
1121 11
                                        $drawing->setWorksheet($this->phpSheet);
1122 11
                                        $drawing->setCoordinates($spContainer->getStartCoordinates());
1123
                                    }
1124
                                }
1125
                            }
1126
1127 11
                            break;
1128
                        default:
1129
                            // other object type
1130
                            break;
1131
                    }
1132
                }
1133
            }
1134
1135
            // treat SHAREDFMLA records
1136 93
            if ($this->version == self::XLS_BIFF8) {
1137 91
                foreach ($this->sharedFormulaParts as $cell => $baseCell) {
1138
                    /** @var int $row */
1139
                    [$column, $row] = Coordinate::coordinateFromString($cell);
1140
                    if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
1141
                        $formula = $this->getFormulaFromStructure($this->sharedFormulas[$baseCell], $cell);
1142
                        $this->phpSheet->getCell($cell)->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
1143
                    }
1144
                }
1145
            }
1146
1147 93
            if (!empty($this->cellNotes)) {
1148 2
                foreach ($this->cellNotes as $note => $noteDetails) {
1149 2
                    if (!isset($noteDetails['objTextData'])) {
1150
                        if (isset($this->textObjects[$note])) {
1151
                            $textObject = $this->textObjects[$note];
1152
                            $noteDetails['objTextData'] = $textObject;
1153
                        } else {
1154
                            $noteDetails['objTextData']['text'] = '';
1155
                        }
1156
                    }
1157 2
                    $cellAddress = str_replace('$', '', $noteDetails['cellRef']);
1158 2
                    $this->phpSheet->getComment($cellAddress)->setAuthor($noteDetails['author'])->setText($this->parseRichText($noteDetails['objTextData']['text']));
1159
                }
1160
            }
1161
        }
1162 94
        if ($this->activeSheetSet === false) {
1163 5
            $this->spreadsheet->setActiveSheetIndex(0);
1164
        }
1165
1166
        // add the named ranges (defined names)
1167 93
        foreach ($this->definedname as $definedName) {
1168 15
            if ($definedName['isBuiltInName']) {
1169 5
                switch ($definedName['name']) {
1170 5
                    case pack('C', 0x06):
1171
                        // print area
1172
                        //    in general, formula looks like this: Foo!$C$7:$J$66,Bar!$A$1:$IV$2
1173 5
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?
1174
1175 5
                        $extractedRanges = [];
1176
                        /** @var non-empty-string $range */
1177 5
                        foreach ($ranges as $range) {
1178
                            // $range should look like one of these
1179
                            //        Foo!$C$7:$J$66
1180
                            //        Bar!$A$1:$IV$2
1181 5
                            $explodes = Worksheet::extractSheetTitle($range, true);
1182 5
                            $sheetName = trim($explodes[0], "'");
1183 5
                            if (!str_contains($explodes[1], ':')) {
1184
                                $explodes[1] = $explodes[1] . ':' . $explodes[1];
1185
                            }
1186 5
                            $extractedRanges[] = str_replace('$', '', $explodes[1]); // C7:J66
1187
                        }
1188 5
                        if ($docSheet = $this->spreadsheet->getSheetByName($sheetName)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sheetName does not seem to be defined for all execution paths leading up to this point.
Loading history...
1189 5
                            $docSheet->getPageSetup()->setPrintArea(implode(',', $extractedRanges)); // C7:J66,A1:IV2
1190
                        }
1191
1192 5
                        break;
1193
                    case pack('C', 0x07):
1194
                        // print titles (repeating rows)
1195
                        // Assuming BIFF8, there are 3 cases
1196
                        // 1. repeating rows
1197
                        //        formula looks like this: Sheet!$A$1:$IV$2
1198
                        //        rows 1-2 repeat
1199
                        // 2. repeating columns
1200
                        //        formula looks like this: Sheet!$A$1:$B$65536
1201
                        //        columns A-B repeat
1202
                        // 3. both repeating rows and repeating columns
1203
                        //        formula looks like this: Sheet!$A$1:$B$65536,Sheet!$A$1:$IV$2
1204
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?
1205
                        foreach ($ranges as $range) {
1206
                            // $range should look like this one of these
1207
                            //        Sheet!$A$1:$B$65536
1208
                            //        Sheet!$A$1:$IV$2
1209
                            if (str_contains($range, '!')) {
1210
                                $explodes = Worksheet::extractSheetTitle($range, true);
1211
                                if ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) {
1212
                                    $extractedRange = $explodes[1];
1213
                                    $extractedRange = str_replace('$', '', $extractedRange);
1214
1215
                                    $coordinateStrings = explode(':', $extractedRange);
1216
                                    if (count($coordinateStrings) == 2) {
1217
                                        [$firstColumn, $firstRow] = Coordinate::coordinateFromString($coordinateStrings[0]);
1218
                                        [$lastColumn, $lastRow] = Coordinate::coordinateFromString($coordinateStrings[1]);
1219
1220
                                        if ($firstColumn == 'A' && $lastColumn == 'IV') {
1221
                                            // then we have repeating rows
1222
                                            $docSheet->getPageSetup()->setRowsToRepeatAtTop([$firstRow, $lastRow]);
1223
                                        } elseif ($firstRow == 1 && $lastRow == 65536) {
1224
                                            // then we have repeating columns
1225
                                            $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$firstColumn, $lastColumn]);
1226
                                        }
1227
                                    }
1228
                                }
1229
                            }
1230
                        }
1231
1232 5
                        break;
1233
                }
1234
            } else {
1235
                // Extract range
1236
                /** @var non-empty-string $formula */
1237 10
                $formula = $definedName['formula'];
1238 10
                if (str_contains($formula, '!')) {
1239 5
                    $explodes = Worksheet::extractSheetTitle($formula, true);
1240
                    if (
1241 5
                        ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) ||
1242 5
                        ($docSheet = $this->spreadsheet->getSheetByName(trim($explodes[0], "'")))
1243
                    ) {
1244 5
                        $extractedRange = $explodes[1];
1245
1246 5
                        $localOnly = ($definedName['scope'] === 0) ? false : true;
1247
1248 5
                        $scope = ($definedName['scope'] === 0) ? null : $this->spreadsheet->getSheetByName($this->sheets[$definedName['scope'] - 1]['name']);
1249
1250 5
                        $this->spreadsheet->addNamedRange(new NamedRange((string) $definedName['name'], $docSheet, $extractedRange, $localOnly, $scope));
1251
                    }
1252
                }
1253
                //    Named Value
1254
                //    TODO Provide support for named values
1255
            }
1256
        }
1257 93
        $this->data = '';
1258
1259 93
        return $this->spreadsheet;
1260
    }
1261
1262
    /**
1263
     * Read record data from stream, decrypting as required.
1264
     *
1265
     * @param string $data Data stream to read from
1266
     * @param int $pos Position to start reading from
1267
     * @param int $len Record data length
1268
     *
1269
     * @return string Record data
1270
     */
1271 103
    private function readRecordData($data, int $pos, int $len): string
1272
    {
1273 103
        $data = substr($data, $pos, $len);
1274
1275
        // File not encrypted, or record before encryption start point
1276 103
        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
1277 103
            return $data;
1278
        }
1279
1280
        $recordData = '';
1281
        if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
1282
            $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
1283
            $block = (int) floor($pos / self::REKEY_BLOCK);
1284
            $endBlock = (int) floor(($pos + $len) / self::REKEY_BLOCK);
1285
1286
            // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
1287
            // at a point earlier in the current block, re-use it as we can save some time.
1288
            if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
1289
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1290
                $step = $pos % self::REKEY_BLOCK;
1291
            } else {
1292
                $step = $pos - $this->rc4Pos;
1293
            }
1294
            $this->rc4Key->RC4(str_repeat("\0", $step));
1295
1296
            // Decrypt record data (re-keying at the end of every block)
1297
            while ($block != $endBlock) {
1298
                $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
1299
                $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
1300
                $data = substr($data, $step);
1301
                $pos += $step;
1302
                $len -= $step;
1303
                ++$block;
1304
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1305
            }
1306
            $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));
1307
1308
            // Keep track of the position of this decryptor.
1309
            // We'll try and re-use it later if we can to speed things up
1310
            $this->rc4Pos = $pos + $len;
1311
        } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
1312
            throw new Exception('XOr encryption not supported');
1313
        }
1314
1315
        return $recordData;
1316
    }
1317
1318
    /**
1319
     * Use OLE reader to extract the relevant data streams from the OLE file.
1320
     */
1321 103
    private function loadOLE(string $filename): void
1322
    {
1323
        // OLE reader
1324 103
        $ole = new OLERead();
1325
        // get excel data,
1326 103
        $ole->read($filename);
1327
        // Get workbook data: workbook stream + sheet streams
1328 103
        $this->data = $ole->getStream($ole->wrkbook); // @phpstan-ignore-line
1329
        // Get summary information data
1330 103
        $this->summaryInformation = $ole->getStream($ole->summaryInformation);
1331
        // Get additional document summary information data
1332 103
        $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation);
1333
    }
1334
1335
    /**
1336
     * Read summary information.
1337
     */
1338 94
    private function readSummaryInformation(): void
1339
    {
1340 94
        if (!isset($this->summaryInformation)) {
1341 3
            return;
1342
        }
1343
1344
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
1345
        // offset: 2; size: 2;
1346
        // offset: 4; size: 2; OS version
1347
        // offset: 6; size: 2; OS indicator
1348
        // offset: 8; size: 16
1349
        // offset: 24; size: 4; section count
1350 91
        $secCount = self::getInt4d($this->summaryInformation, 24);
0 ignored issues
show
Unused Code introduced by
The assignment to $secCount is dead and can be removed.
Loading history...
1351
1352
        // offset: 28; size: 16; first section's class id: e0 85 9f f2 f9 4f 68 10 ab 91 08 00 2b 27 b3 d9
1353
        // offset: 44; size: 4
1354 91
        $secOffset = self::getInt4d($this->summaryInformation, 44);
1355
1356
        // section header
1357
        // offset: $secOffset; size: 4; section length
1358 91
        $secLength = self::getInt4d($this->summaryInformation, $secOffset);
0 ignored issues
show
Unused Code introduced by
The assignment to $secLength is dead and can be removed.
Loading history...
1359
1360
        // offset: $secOffset+4; size: 4; property count
1361 91
        $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4);
1362
1363
        // initialize code page (used to resolve string values)
1364 91
        $codePage = 'CP1252';
1365
1366
        // offset: ($secOffset+8); size: var
1367
        // loop through property decarations and properties
1368 91
        for ($i = 0; $i < $countProperties; ++$i) {
1369
            // offset: ($secOffset+8) + (8 * $i); size: 4; property ID
1370 91
            $id = self::getInt4d($this->summaryInformation, ($secOffset + 8) + (8 * $i));
1371
1372
            // Use value of property id as appropriate
1373
            // offset: ($secOffset+12) + (8 * $i); size: 4; offset from beginning of section (48)
1374 91
            $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i));
1375
1376 91
            $type = self::getInt4d($this->summaryInformation, $secOffset + $offset);
1377
1378
            // initialize property value
1379 91
            $value = null;
1380
1381
            // extract property value based on property type
1382
            switch ($type) {
1383 91
                case 0x02: // 2 byte signed integer
1384 91
                    $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset);
1385
1386 91
                    break;
1387 91
                case 0x03: // 4 byte signed integer
1388 87
                    $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1389
1390 87
                    break;
1391 91
                case 0x13: // 4 byte unsigned integer
1392
                    // not needed yet, fix later if necessary
1393 1
                    break;
1394 91
                case 0x1E: // null-terminated string prepended by dword string length
1395 89
                    $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1396 89
                    $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
1 ignored issue
show
Bug introduced by
It seems like $this->summaryInformation can also be of type null; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1396
                    $value = substr(/** @scrutinizer ignore-type */ $this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
Loading history...
1397 89
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1398 89
                    $value = rtrim($value);
1399
1400 89
                    break;
1401 91
                case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
1402
                    // PHP-time
1403 91
                    $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8));
1404
1405 91
                    break;
1406 2
                case 0x47: // Clipboard format
1407
                    // not needed yet, fix later if necessary
1408
                    break;
1409
            }
1410
1411
            switch ($id) {
1412 91
                case 0x01:    //    Code Page
1413 91
                    $codePage = CodePage::numberToName((int) $value);
1414
1415 91
                    break;
1416 91
                case 0x02:    //    Title
1417 53
                    $this->spreadsheet->getProperties()->setTitle("$value");
1418
1419 53
                    break;
1420 91
                case 0x03:    //    Subject
1421 15
                    $this->spreadsheet->getProperties()->setSubject("$value");
1422
1423 15
                    break;
1424 91
                case 0x04:    //    Author (Creator)
1425 78
                    $this->spreadsheet->getProperties()->setCreator("$value");
1426
1427 78
                    break;
1428 91
                case 0x05:    //    Keywords
1429 15
                    $this->spreadsheet->getProperties()->setKeywords("$value");
1430
1431 15
                    break;
1432 91
                case 0x06:    //    Comments (Description)
1433 15
                    $this->spreadsheet->getProperties()->setDescription("$value");
1434
1435 15
                    break;
1436 91
                case 0x07:    //    Template
1437
                    //    Not supported by PhpSpreadsheet
1438
                    break;
1439 91
                case 0x08:    //    Last Saved By (LastModifiedBy)
1440 89
                    $this->spreadsheet->getProperties()->setLastModifiedBy("$value");
1441
1442 89
                    break;
1443 91
                case 0x09:    //    Revision
1444
                    //    Not supported by PhpSpreadsheet
1445 3
                    break;
1446 91
                case 0x0A:    //    Total Editing Time
1447
                    //    Not supported by PhpSpreadsheet
1448 3
                    break;
1449 91
                case 0x0B:    //    Last Printed
1450
                    //    Not supported by PhpSpreadsheet
1451 7
                    break;
1452 91
                case 0x0C:    //    Created Date/Time
1453 85
                    $this->spreadsheet->getProperties()->setCreated($value);
1454
1455 85
                    break;
1456 91
                case 0x0D:    //    Modified Date/Time
1457 91
                    $this->spreadsheet->getProperties()->setModified($value);
1458
1459 91
                    break;
1460 88
                case 0x0E:    //    Number of Pages
1461
                    //    Not supported by PhpSpreadsheet
1462
                    break;
1463 88
                case 0x0F:    //    Number of Words
1464
                    //    Not supported by PhpSpreadsheet
1465
                    break;
1466 88
                case 0x10:    //    Number of Characters
1467
                    //    Not supported by PhpSpreadsheet
1468
                    break;
1469 88
                case 0x11:    //    Thumbnail
1470
                    //    Not supported by PhpSpreadsheet
1471
                    break;
1472 88
                case 0x12:    //    Name of creating application
1473
                    //    Not supported by PhpSpreadsheet
1474 33
                    break;
1475 88
                case 0x13:    //    Security
1476
                    //    Not supported by PhpSpreadsheet
1477 87
                    break;
1478
            }
1479
        }
1480
    }
1481
1482
    /**
1483
     * Read additional document summary information.
1484
     */
1485 94
    private function readDocumentSummaryInformation(): void
1486
    {
1487 94
        if (!isset($this->documentSummaryInformation)) {
1488 3
            return;
1489
        }
1490
1491
        //    offset: 0;    size: 2;    must be 0xFE 0xFF (UTF-16 LE byte order mark)
1492
        //    offset: 2;    size: 2;
1493
        //    offset: 4;    size: 2;    OS version
1494
        //    offset: 6;    size: 2;    OS indicator
1495
        //    offset: 8;    size: 16
1496
        //    offset: 24;    size: 4;    section count
1497 91
        $secCount = self::getInt4d($this->documentSummaryInformation, 24);
0 ignored issues
show
Unused Code introduced by
The assignment to $secCount is dead and can be removed.
Loading history...
1498
1499
        // offset: 28;    size: 16;    first section's class id: 02 d5 cd d5 9c 2e 1b 10 93 97 08 00 2b 2c f9 ae
1500
        // offset: 44;    size: 4;    first section offset
1501 91
        $secOffset = self::getInt4d($this->documentSummaryInformation, 44);
1502
1503
        //    section header
1504
        //    offset: $secOffset;    size: 4;    section length
1505 91
        $secLength = self::getInt4d($this->documentSummaryInformation, $secOffset);
0 ignored issues
show
Unused Code introduced by
The assignment to $secLength is dead and can be removed.
Loading history...
1506
1507
        //    offset: $secOffset+4;    size: 4;    property count
1508 91
        $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4);
1509
1510
        // initialize code page (used to resolve string values)
1511 91
        $codePage = 'CP1252';
1512
1513
        //    offset: ($secOffset+8);    size: var
1514
        //    loop through property decarations and properties
1515 91
        for ($i = 0; $i < $countProperties; ++$i) {
1516
            //    offset: ($secOffset+8) + (8 * $i);    size: 4;    property ID
1517 91
            $id = self::getInt4d($this->documentSummaryInformation, ($secOffset + 8) + (8 * $i));
1518
1519
            // Use value of property id as appropriate
1520
            // offset: 60 + 8 * $i;    size: 4;    offset from beginning of section (48)
1521 91
            $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i));
1522
1523 91
            $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset);
1524
1525
            // initialize property value
1526 91
            $value = null;
1527
1528
            // extract property value based on property type
1529
            switch ($type) {
1530 91
                case 0x02:    //    2 byte signed integer
1531 91
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1532
1533 91
                    break;
1534 88
                case 0x03:    //    4 byte signed integer
1535 85
                    $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1536
1537 85
                    break;
1538 88
                case 0x0B:  // Boolean
1539 88
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1540 88
                    $value = ($value == 0 ? false : true);
1541
1542 88
                    break;
1543 87
                case 0x13:    //    4 byte unsigned integer
1544
                    // not needed yet, fix later if necessary
1545 1
                    break;
1546 86
                case 0x1E:    //    null-terminated string prepended by dword string length
1547 36
                    $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1548 36
                    $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
1 ignored issue
show
Bug introduced by
It seems like $this->documentSummaryInformation can also be of type null; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1548
                    $value = substr(/** @scrutinizer ignore-type */ $this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
Loading history...
1549 36
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1550 36
                    $value = rtrim($value);
1551
1552 36
                    break;
1553 86
                case 0x40:    //    Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
1554
                    // PHP-Time
1555
                    $value = OLE::OLE2LocalDate(substr($this->documentSummaryInformation, $secOffset + 4 + $offset, 8));
1556
1557
                    break;
1558 86
                case 0x47:    //    Clipboard format
1559
                    // not needed yet, fix later if necessary
1560
                    break;
1561
            }
1562
1563
            switch ($id) {
1564 91
                case 0x01:    //    Code Page
1565 91
                    $codePage = CodePage::numberToName((int) $value);
1566
1567 91
                    break;
1568 88
                case 0x02:    //    Category
1569 15
                    $this->spreadsheet->getProperties()->setCategory("$value");
1570
1571 15
                    break;
1572 88
                case 0x03:    //    Presentation Target
1573
                    //    Not supported by PhpSpreadsheet
1574
                    break;
1575 88
                case 0x04:    //    Bytes
1576
                    //    Not supported by PhpSpreadsheet
1577
                    break;
1578 88
                case 0x05:    //    Lines
1579
                    //    Not supported by PhpSpreadsheet
1580
                    break;
1581 88
                case 0x06:    //    Paragraphs
1582
                    //    Not supported by PhpSpreadsheet
1583
                    break;
1584 88
                case 0x07:    //    Slides
1585
                    //    Not supported by PhpSpreadsheet
1586
                    break;
1587 88
                case 0x08:    //    Notes
1588
                    //    Not supported by PhpSpreadsheet
1589
                    break;
1590 88
                case 0x09:    //    Hidden Slides
1591
                    //    Not supported by PhpSpreadsheet
1592
                    break;
1593 88
                case 0x0A:    //    MM Clips
1594
                    //    Not supported by PhpSpreadsheet
1595
                    break;
1596 88
                case 0x0B:    //    Scale Crop
1597
                    //    Not supported by PhpSpreadsheet
1598 88
                    break;
1599 88
                case 0x0C:    //    Heading Pairs
1600
                    //    Not supported by PhpSpreadsheet
1601 86
                    break;
1602 88
                case 0x0D:    //    Titles of Parts
1603
                    //    Not supported by PhpSpreadsheet
1604 86
                    break;
1605 88
                case 0x0E:    //    Manager
1606 2
                    $this->spreadsheet->getProperties()->setManager("$value");
1607
1608 2
                    break;
1609 88
                case 0x0F:    //    Company
1610 26
                    $this->spreadsheet->getProperties()->setCompany("$value");
1611
1612 26
                    break;
1613 88
                case 0x10:    //    Links up-to-date
1614
                    //    Not supported by PhpSpreadsheet
1615 88
                    break;
1616
            }
1617
        }
1618
    }
1619
1620
    /**
1621
     * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record.
1622
     */
1623 103
    private function readDefault(): void
1624
    {
1625 103
        $length = self::getUInt2d($this->data, $this->pos + 2);
1626
1627
        // move stream pointer to next record
1628 103
        $this->pos += 4 + $length;
1629
    }
1630
1631
    /**
1632
     *    The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions,
1633
     *        this record stores a note (cell note). This feature was significantly enhanced in Excel 97.
1634
     */
1635 3
    private function readNote(): void
1636
    {
1637 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
1638 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1639
1640
        // move stream pointer to next record
1641 3
        $this->pos += 4 + $length;
1642
1643 3
        if ($this->readDataOnly) {
1644
            return;
1645
        }
1646
1647 3
        $cellAddress = $this->readBIFF8CellAddress(substr($recordData, 0, 4));
1648 3
        if ($this->version == self::XLS_BIFF8) {
1649 2
            $noteObjID = self::getUInt2d($recordData, 6);
1650 2
            $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8));
1651 2
            $noteAuthor = $noteAuthor['value'];
1652 2
            $this->cellNotes[$noteObjID] = [
1653 2
                'cellRef' => $cellAddress,
1654 2
                'objectID' => $noteObjID,
1655 2
                'author' => $noteAuthor,
1656 2
            ];
1657
        } else {
1658 1
            $extension = false;
1659 1
            if ($cellAddress == '$B$65536') {
1660
                //    If the address row is -1 and the column is 0, (which translates as $B$65536) then this is a continuation
1661
                //        note from the previous cell annotation. We're not yet handling this, so annotations longer than the
1662
                //        max 2048 bytes will probably throw a wobbly.
1663
                $row = self::getUInt2d($recordData, 0);
0 ignored issues
show
Unused Code introduced by
The assignment to $row is dead and can be removed.
Loading history...
1664
                $extension = true;
1665
                $arrayKeys = array_keys($this->phpSheet->getComments());
1666
                $cellAddress = array_pop($arrayKeys);
1667
            }
1668
1669 1
            $cellAddress = str_replace('$', '', (string) $cellAddress);
1670 1
            $noteLength = self::getUInt2d($recordData, 4);
0 ignored issues
show
Unused Code introduced by
The assignment to $noteLength is dead and can be removed.
Loading history...
1671 1
            $noteText = trim(substr($recordData, 6));
1672
1673 1
            if ($extension) {
1674
                //    Concatenate this extension with the currently set comment for the cell
1675
                $comment = $this->phpSheet->getComment($cellAddress);
1676
                $commentText = $comment->getText()->getPlainText();
1677
                $comment->setText($this->parseRichText($commentText . $noteText));
1678
            } else {
1679
                //    Set comment for the cell
1680 1
                $this->phpSheet->getComment($cellAddress)->setText($this->parseRichText($noteText));
1681
//                                                    ->setAuthor($author)
1682
            }
1683
        }
1684
    }
1685
1686
    /**
1687
     * The TEXT Object record contains the text associated with a cell annotation.
1688
     */
1689 2
    private function readTextObject(): void
1690
    {
1691 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
1692 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1693
1694
        // move stream pointer to next record
1695 2
        $this->pos += 4 + $length;
1696
1697 2
        if ($this->readDataOnly) {
1698
            return;
1699
        }
1700
1701
        // recordData consists of an array of subrecords looking like this:
1702
        //    grbit: 2 bytes; Option Flags
1703
        //    rot: 2 bytes; rotation
1704
        //    cchText: 2 bytes; length of the text (in the first continue record)
1705
        //    cbRuns: 2 bytes; length of the formatting (in the second continue record)
1706
        // followed by the continuation records containing the actual text and formatting
1707 2
        $grbitOpts = self::getUInt2d($recordData, 0);
1708 2
        $rot = self::getUInt2d($recordData, 2);
1709 2
        $cchText = self::getUInt2d($recordData, 10);
0 ignored issues
show
Unused Code introduced by
The assignment to $cchText is dead and can be removed.
Loading history...
1710 2
        $cbRuns = self::getUInt2d($recordData, 12);
1711 2
        $text = $this->getSplicedRecordData();
1712
1713 2
        $textByte = $text['spliceOffsets'][1] - $text['spliceOffsets'][0] - 1;
1714 2
        $textStr = substr($text['recordData'], $text['spliceOffsets'][0] + 1, $textByte);
1715
        // get 1 byte
1716 2
        $is16Bit = ord($text['recordData'][0]);
1717
        // it is possible to use a compressed format,
1718
        // which omits the high bytes of all characters, if they are all zero
1719 2
        if (($is16Bit & 0x01) === 0) {
1720 2
            $textStr = StringHelper::ConvertEncoding($textStr, 'UTF-8', 'ISO-8859-1');
1721
        } else {
1722
            $textStr = $this->decodeCodepage($textStr);
1723
        }
1724
1725 2
        $this->textObjects[$this->textObjRef] = [
1726 2
            'text' => $textStr,
1727 2
            'format' => substr($text['recordData'], $text['spliceOffsets'][1], $cbRuns),
1728 2
            'alignment' => $grbitOpts,
1729 2
            'rotation' => $rot,
1730 2
        ];
1731
    }
1732
1733
    /**
1734
     * Read BOF.
1735
     */
1736 103
    private function readBof(): void
1737
    {
1738 103
        $length = self::getUInt2d($this->data, $this->pos + 2);
1739 103
        $recordData = substr($this->data, $this->pos + 4, $length);
1740
1741
        // move stream pointer to next record
1742 103
        $this->pos += 4 + $length;
1743
1744
        // offset: 2; size: 2; type of the following data
1745 103
        $substreamType = self::getUInt2d($recordData, 2);
1746
1747
        switch ($substreamType) {
1748
            case self::XLS_WORKBOOKGLOBALS:
1749 103
                $version = self::getUInt2d($recordData, 0);
1750 103
                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
1751
                    throw new Exception('Cannot read this Excel file. Version is too old.');
1752
                }
1753 103
                $this->version = $version;
1754
1755 103
                break;
1756
            case self::XLS_WORKSHEET:
1757
                // do not use this version information for anything
1758
                // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
1759 97
                break;
1760
            default:
1761
                // substream, e.g. chart
1762
                // just skip the entire substream
1763
                do {
1764
                    $code = self::getUInt2d($this->data, $this->pos);
1765
                    $this->readDefault();
1766
                } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
1767
1768
                break;
1769
        }
1770
    }
1771
1772
    /**
1773
     * FILEPASS.
1774
     *
1775
     * This record is part of the File Protection Block. It
1776
     * contains information about the read/write password of the
1777
     * file. All record contents following this record will be
1778
     * encrypted.
1779
     *
1780
     * --    "OpenOffice.org's Documentation of the Microsoft
1781
     *         Excel File Format"
1782
     *
1783
     * The decryption functions and objects used from here on in
1784
     * are based on the source of Spreadsheet-ParseExcel:
1785
     * https://metacpan.org/release/Spreadsheet-ParseExcel
1786
     */
1787
    private function readFilepass(): void
1788
    {
1789
        $length = self::getUInt2d($this->data, $this->pos + 2);
1790
1791
        if ($length != 54) {
1792
            throw new Exception('Unexpected file pass record length');
1793
        }
1794
1795
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1796
1797
        // move stream pointer to next record
1798
        $this->pos += 4 + $length;
1799
1800
        if (!$this->verifyPassword('VelvetSweatshop', substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) {
1801
            throw new Exception('Decryption password incorrect');
1802
        }
1803
1804
        $this->encryption = self::MS_BIFF_CRYPTO_RC4;
1805
1806
        // Decryption required from the record after next onwards
1807
        $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2);
1808
    }
1809
1810
    /**
1811
     * Make an RC4 decryptor for the given block.
1812
     *
1813
     * @param int $block Block for which to create decrypto
1814
     * @param string $valContext MD5 context state
1815
     */
1816
    private function makeKey(int $block, $valContext): Xls\RC4
1817
    {
1818
        $pwarray = str_repeat("\0", 64);
1819
1820
        for ($i = 0; $i < 5; ++$i) {
1821
            $pwarray[$i] = $valContext[$i];
1822
        }
1823
1824
        $pwarray[5] = chr($block & 0xff);
1825
        $pwarray[6] = chr(($block >> 8) & 0xff);
1826
        $pwarray[7] = chr(($block >> 16) & 0xff);
1827
        $pwarray[8] = chr(($block >> 24) & 0xff);
1828
1829
        $pwarray[9] = "\x80";
1830
        $pwarray[56] = "\x48";
1831
1832
        $md5 = new Xls\MD5();
1833
        $md5->add($pwarray);
1834
1835
        $s = $md5->getContext();
1836
1837
        return new Xls\RC4($s);
1838
    }
1839
1840
    /**
1841
     * Verify RC4 file password.
1842
     *
1843
     * @param string $password Password to check
1844
     * @param string $docid Document id
1845
     * @param string $salt_data Salt data
1846
     * @param string $hashedsalt_data Hashed salt data
1847
     * @param string $valContext Set to the MD5 context of the value
1848
     *
1849
     * @return bool Success
1850
     */
1851
    private function verifyPassword(string $password, string $docid, string $salt_data, string $hashedsalt_data, &$valContext): bool
1852
    {
1853
        $pwarray = str_repeat("\0", 64);
1854
1855
        $iMax = strlen($password);
1856
        for ($i = 0; $i < $iMax; ++$i) {
1857
            $o = ord(substr($password, $i, 1));
1858
            $pwarray[2 * $i] = chr($o & 0xff);
1859
            $pwarray[2 * $i + 1] = chr(($o >> 8) & 0xff);
1860
        }
1861
        $pwarray[2 * $i] = chr(0x80);
1862
        $pwarray[56] = chr(($i << 4) & 0xff);
1863
1864
        $md5 = new Xls\MD5();
1865
        $md5->add($pwarray);
1866
1867
        $mdContext1 = $md5->getContext();
1868
1869
        $offset = 0;
1870
        $keyoffset = 0;
1871
        $tocopy = 5;
1872
1873
        $md5->reset();
1874
1875
        while ($offset != 16) {
1876
            if ((64 - $offset) < 5) {
1877
                $tocopy = 64 - $offset;
1878
            }
1879
            for ($i = 0; $i <= $tocopy; ++$i) {
1880
                $pwarray[$offset + $i] = $mdContext1[$keyoffset + $i];
1881
            }
1882
            $offset += $tocopy;
1883
1884
            if ($offset == 64) {
1885
                $md5->add($pwarray);
1886
                $keyoffset = $tocopy;
1887
                $tocopy = 5 - $tocopy;
1888
                $offset = 0;
1889
1890
                continue;
1891
            }
1892
1893
            $keyoffset = 0;
1894
            $tocopy = 5;
1895
            for ($i = 0; $i < 16; ++$i) {
1896
                $pwarray[$offset + $i] = $docid[$i];
1897
            }
1898
            $offset += 16;
1899
        }
1900
1901
        $pwarray[16] = "\x80";
1902
        for ($i = 0; $i < 47; ++$i) {
1903
            $pwarray[17 + $i] = "\0";
1904
        }
1905
        $pwarray[56] = "\x80";
1906
        $pwarray[57] = "\x0a";
1907
1908
        $md5->add($pwarray);
1909
        $valContext = $md5->getContext();
1910
1911
        $key = $this->makeKey(0, $valContext);
1912
1913
        $salt = $key->RC4($salt_data);
1914
        $hashedsalt = $key->RC4($hashedsalt_data);
1915
1916
        $salt .= "\x80" . str_repeat("\0", 47);
1917
        $salt[56] = "\x80";
1918
1919
        $md5->reset();
1920
        $md5->add($salt);
1921
        $mdContext2 = $md5->getContext();
1922
1923
        return $mdContext2 == $hashedsalt;
1924
    }
1925
1926
    /**
1927
     * CODEPAGE.
1928
     *
1929
     * This record stores the text encoding used to write byte
1930
     * strings, stored as MS Windows code page identifier.
1931
     *
1932
     * --    "OpenOffice.org's Documentation of the Microsoft
1933
     *         Excel File Format"
1934
     */
1935 100
    private function readCodepage(): void
1936
    {
1937 100
        $length = self::getUInt2d($this->data, $this->pos + 2);
1938 100
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1939
1940
        // move stream pointer to next record
1941 100
        $this->pos += 4 + $length;
1942
1943
        // offset: 0; size: 2; code page identifier
1944 100
        $codepage = self::getUInt2d($recordData, 0);
1945
1946 100
        $this->codepage = CodePage::numberToName($codepage);
1947
    }
1948
1949
    /**
1950
     * DATEMODE.
1951
     *
1952
     * This record specifies the base date for displaying date
1953
     * values. All dates are stored as count of days past this
1954
     * base date. In BIFF2-BIFF4 this record is part of the
1955
     * Calculation Settings Block. In BIFF5-BIFF8 it is
1956
     * stored in the Workbook Globals Substream.
1957
     *
1958
     * --    "OpenOffice.org's Documentation of the Microsoft
1959
     *         Excel File Format"
1960
     */
1961 93
    private function readDateMode(): void
1962
    {
1963 93
        $length = self::getUInt2d($this->data, $this->pos + 2);
1964 93
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1965
1966
        // move stream pointer to next record
1967 93
        $this->pos += 4 + $length;
1968
1969
        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
1970 93
        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
1971 93
        if (ord($recordData[0]) == 1) {
1972
            Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
1973
        }
1974
    }
1975
1976
    /**
1977
     * Read a FONT record.
1978
     */
1979 94
    private function readFont(): void
1980
    {
1981 94
        $length = self::getUInt2d($this->data, $this->pos + 2);
1982 94
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1983
1984
        // move stream pointer to next record
1985 94
        $this->pos += 4 + $length;
1986
1987 94
        if (!$this->readDataOnly) {
1988 93
            $objFont = new Font();
1989
1990
            // offset: 0; size: 2; height of the font (in twips = 1/20 of a point)
1991 93
            $size = self::getUInt2d($recordData, 0);
1992 93
            $objFont->setSize($size / 20);
1993
1994
            // offset: 2; size: 2; option flags
1995
            // bit: 0; mask 0x0001; bold (redundant in BIFF5-BIFF8)
1996
            // bit: 1; mask 0x0002; italic
1997 93
            $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1;
1998 93
            if ($isItalic) {
1999 43
                $objFont->setItalic(true);
2000
            }
2001
2002
            // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8)
2003
            // bit: 3; mask 0x0008; strikethrough
2004 93
            $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3;
2005 93
            if ($isStrike) {
2006
                $objFont->setStrikethrough(true);
2007
            }
2008
2009
            // offset: 4; size: 2; colour index
2010 93
            $colorIndex = self::getUInt2d($recordData, 4);
2011 93
            $objFont->colorIndex = $colorIndex;
2012
2013
            // offset: 6; size: 2; font weight
2014 93
            $weight = self::getUInt2d($recordData, 6);
2015
            switch ($weight) {
2016 93
                case 0x02BC:
2017 54
                    $objFont->setBold(true);
2018
2019 54
                    break;
2020
            }
2021
2022
            // offset: 8; size: 2; escapement type
2023 93
            $escapement = self::getUInt2d($recordData, 8);
2024 93
            CellFont::escapement($objFont, $escapement);
2025
2026
            // offset: 10; size: 1; underline type
2027 93
            $underlineType = ord($recordData[10]);
2028 93
            CellFont::underline($objFont, $underlineType);
2029
2030
            // offset: 11; size: 1; font family
2031
            // offset: 12; size: 1; character set
2032
            // offset: 13; size: 1; not used
2033
            // offset: 14; size: var; font name
2034 93
            if ($this->version == self::XLS_BIFF8) {
2035 91
                $string = self::readUnicodeStringShort(substr($recordData, 14));
2036
            } else {
2037 2
                $string = $this->readByteStringShort(substr($recordData, 14));
2038
            }
2039 93
            $objFont->setName($string['value']);
2040
2041 93
            $this->objFonts[] = $objFont;
2042
        }
2043
    }
2044
2045
    /**
2046
     * FORMAT.
2047
     *
2048
     * This record contains information about a number format.
2049
     * All FORMAT records occur together in a sequential list.
2050
     *
2051
     * In BIFF2-BIFF4 other records referencing a FORMAT record
2052
     * contain a zero-based index into this list. From BIFF5 on
2053
     * the FORMAT record contains the index itself that will be
2054
     * used by other records.
2055
     *
2056
     * --    "OpenOffice.org's Documentation of the Microsoft
2057
     *         Excel File Format"
2058
     */
2059 52
    private function readFormat(): void
2060
    {
2061 52
        $length = self::getUInt2d($this->data, $this->pos + 2);
2062 52
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2063
2064
        // move stream pointer to next record
2065 52
        $this->pos += 4 + $length;
2066
2067 52
        if (!$this->readDataOnly) {
2068 51
            $indexCode = self::getUInt2d($recordData, 0);
2069
2070 51
            if ($this->version == self::XLS_BIFF8) {
2071 49
                $string = self::readUnicodeStringLong(substr($recordData, 2));
2072
            } else {
2073
                // BIFF7
2074 2
                $string = $this->readByteStringShort(substr($recordData, 2));
2075
            }
2076
2077 51
            $formatString = $string['value'];
2078
            // Apache Open Office sets wrong case writing to xls - issue 2239
2079 51
            if ($formatString === 'GENERAL') {
2080 1
                $formatString = NumberFormat::FORMAT_GENERAL;
2081
            }
2082 51
            $this->formats[$indexCode] = $formatString;
2083
        }
2084
    }
2085
2086
    /**
2087
     * XF - Extended Format.
2088
     *
2089
     * This record contains formatting information for cells, rows, columns or styles.
2090
     * According to https://support.microsoft.com/en-us/help/147732 there are always at least 15 cell style XF
2091
     * and 1 cell XF.
2092
     * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF
2093
     * and XF record 15 is a cell XF
2094
     * We only read the first cell style XF and skip the remaining cell style XF records
2095
     * We read all cell XF records.
2096
     *
2097
     * --    "OpenOffice.org's Documentation of the Microsoft
2098
     *         Excel File Format"
2099
     */
2100 94
    private function readXf(): void
2101
    {
2102 94
        $length = self::getUInt2d($this->data, $this->pos + 2);
2103 94
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2104
2105
        // move stream pointer to next record
2106 94
        $this->pos += 4 + $length;
2107
2108 94
        $objStyle = new Style();
2109
2110 94
        if (!$this->readDataOnly) {
2111
            // offset:  0; size: 2; Index to FONT record
2112 93
            if (self::getUInt2d($recordData, 0) < 4) {
2113 93
                $fontIndex = self::getUInt2d($recordData, 0);
2114
            } else {
2115
                // this has to do with that index 4 is omitted in all BIFF versions for some strange reason
2116
                // check the OpenOffice documentation of the FONT record
2117 50
                $fontIndex = self::getUInt2d($recordData, 0) - 1;
2118
            }
2119 93
            $objStyle->setFont($this->objFonts[$fontIndex]);
2120
2121
            // offset:  2; size: 2; Index to FORMAT record
2122 93
            $numberFormatIndex = self::getUInt2d($recordData, 2);
2123 93
            if (isset($this->formats[$numberFormatIndex])) {
2124
                // then we have user-defined format code
2125 46
                $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]];
2126 93
            } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') {
2127
                // then we have built-in format code
2128 93
                $numberFormat = ['formatCode' => $code];
2129
            } else {
2130
                // we set the general format code
2131 4
                $numberFormat = ['formatCode' => NumberFormat::FORMAT_GENERAL];
2132
            }
2133 93
            $objStyle->getNumberFormat()->setFormatCode($numberFormat['formatCode']);
2134
2135
            // offset:  4; size: 2; XF type, cell protection, and parent style XF
2136
            // bit 2-0; mask 0x0007; XF_TYPE_PROT
2137 93
            $xfTypeProt = self::getUInt2d($recordData, 4);
2138
            // bit 0; mask 0x01; 1 = cell is locked
2139 93
            $isLocked = (0x01 & $xfTypeProt) >> 0;
2140 93
            $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED);
2141
2142
            // bit 1; mask 0x02; 1 = Formula is hidden
2143 93
            $isHidden = (0x02 & $xfTypeProt) >> 1;
2144 93
            $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED);
2145
2146
            // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF
2147 93
            $isCellStyleXf = (0x04 & $xfTypeProt) >> 2;
2148
2149
            // offset:  6; size: 1; Alignment and text break
2150
            // bit 2-0, mask 0x07; horizontal alignment
2151 93
            $horAlign = (0x07 & ord($recordData[6])) >> 0;
2152 93
            Xls\Style\CellAlignment::horizontal($objStyle->getAlignment(), $horAlign);
2153
2154
            // bit 3, mask 0x08; wrap text
2155 93
            $wrapText = (0x08 & ord($recordData[6])) >> 3;
2156 93
            Xls\Style\CellAlignment::wrap($objStyle->getAlignment(), $wrapText);
2157
2158
            // bit 6-4, mask 0x70; vertical alignment
2159 93
            $vertAlign = (0x70 & ord($recordData[6])) >> 4;
2160 93
            Xls\Style\CellAlignment::vertical($objStyle->getAlignment(), $vertAlign);
2161
2162 93
            if ($this->version == self::XLS_BIFF8) {
2163
                // offset:  7; size: 1; XF_ROTATION: Text rotation angle
2164 91
                $angle = ord($recordData[7]);
2165 91
                $rotation = 0;
2166 91
                if ($angle <= 90) {
2167 91
                    $rotation = $angle;
2168 2
                } elseif ($angle <= 180) {
2169
                    $rotation = 90 - $angle;
2170 2
                } elseif ($angle == Alignment::TEXTROTATION_STACK_EXCEL) {
2171 2
                    $rotation = Alignment::TEXTROTATION_STACK_PHPSPREADSHEET;
2172
                }
2173 91
                $objStyle->getAlignment()->setTextRotation($rotation);
2174
2175
                // offset:  8; size: 1; Indentation, shrink to cell size, and text direction
2176
                // bit: 3-0; mask: 0x0F; indent level
2177 91
                $indent = (0x0F & ord($recordData[8])) >> 0;
2178 91
                $objStyle->getAlignment()->setIndent($indent);
2179
2180
                // bit: 4; mask: 0x10; 1 = shrink content to fit into cell
2181 91
                $shrinkToFit = (0x10 & ord($recordData[8])) >> 4;
2182
                switch ($shrinkToFit) {
2183 91
                    case 0:
2184 91
                        $objStyle->getAlignment()->setShrinkToFit(false);
2185
2186 91
                        break;
2187 1
                    case 1:
2188 1
                        $objStyle->getAlignment()->setShrinkToFit(true);
2189
2190 1
                        break;
2191
                }
2192
2193
                // offset:  9; size: 1; Flags used for attribute groups
2194
2195
                // offset: 10; size: 4; Cell border lines and background area
2196
                // bit: 3-0; mask: 0x0000000F; left style
2197 91
                if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) {
2198 91
                    $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle);
2199
                }
2200
                // bit: 7-4; mask: 0x000000F0; right style
2201 91
                if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) {
2202 91
                    $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle);
2203
                }
2204
                // bit: 11-8; mask: 0x00000F00; top style
2205 91
                if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) {
2206 91
                    $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle);
2207
                }
2208
                // bit: 15-12; mask: 0x0000F000; bottom style
2209 91
                if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) {
2210 91
                    $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle);
2211
                }
2212
                // bit: 22-16; mask: 0x007F0000; left color
2213 91
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16;
2214
2215
                // bit: 29-23; mask: 0x3F800000; right color
2216 91
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & self::getInt4d($recordData, 10)) >> 23;
2217
2218
                // bit: 30; mask: 0x40000000; 1 = diagonal line from top left to right bottom
2219 91
                $diagonalDown = (0x40000000 & self::getInt4d($recordData, 10)) >> 30 ? true : false;
2220
2221
                // bit: 31; mask: 0x80000000; 1 = diagonal line from bottom left to top right
2222 91
                $diagonalUp = ((int) 0x80000000 & self::getInt4d($recordData, 10)) >> 31 ? true : false;
2223
2224 91
                if ($diagonalUp === false) {
2225 91
                    if ($diagonalDown == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
2226 91
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
2227
                    } else {
2228 91
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN);
2229
                    }
2230 1
                } elseif ($diagonalDown == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
2231 1
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP);
2232
                } else {
2233 1
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH);
2234
                }
2235
2236
                // offset: 14; size: 4;
2237
                // bit: 6-0; mask: 0x0000007F; top color
2238 91
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0;
2239
2240
                // bit: 13-7; mask: 0x00003F80; bottom color
2241 91
                $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7;
2242
2243
                // bit: 20-14; mask: 0x001FC000; diagonal color
2244 91
                $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14;
2245
2246
                // bit: 24-21; mask: 0x01E00000; diagonal style
2247 91
                if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) {
2248 91
                    $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle);
2249
                }
2250
2251
                // bit: 31-26; mask: 0xFC000000 fill pattern
2252 91
                if ($fillType = Xls\Style\FillPattern::lookup(((int) 0xFC000000 & self::getInt4d($recordData, 14)) >> 26)) {
2253 91
                    $objStyle->getFill()->setFillType($fillType);
2254
                }
2255
                // offset: 18; size: 2; pattern and background colour
2256
                // bit: 6-0; mask: 0x007F; color index for pattern color
2257 91
                $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0;
2258
2259
                // bit: 13-7; mask: 0x3F80; color index for pattern background
2260 91
                $objStyle->getFill()->endcolorIndex = (0x3F80 & self::getUInt2d($recordData, 18)) >> 7;
2261
            } else {
2262
                // BIFF5
2263
2264
                // offset: 7; size: 1; Text orientation and flags
2265 2
                $orientationAndFlags = ord($recordData[7]);
2266
2267
                // bit: 1-0; mask: 0x03; XF_ORIENTATION: Text orientation
2268 2
                $xfOrientation = (0x03 & $orientationAndFlags) >> 0;
2269
                switch ($xfOrientation) {
2270 2
                    case 0:
2271 2
                        $objStyle->getAlignment()->setTextRotation(0);
2272
2273 2
                        break;
2274 1
                    case 1:
2275 1
                        $objStyle->getAlignment()->setTextRotation(Alignment::TEXTROTATION_STACK_PHPSPREADSHEET);
2276
2277 1
                        break;
2278
                    case 2:
2279
                        $objStyle->getAlignment()->setTextRotation(90);
2280
2281
                        break;
2282
                    case 3:
2283
                        $objStyle->getAlignment()->setTextRotation(-90);
2284
2285
                        break;
2286
                }
2287
2288
                // offset: 8; size: 4; cell border lines and background area
2289 2
                $borderAndBackground = self::getInt4d($recordData, 8);
2290
2291
                // bit: 6-0; mask: 0x0000007F; color index for pattern color
2292 2
                $objStyle->getFill()->startcolorIndex = (0x0000007F & $borderAndBackground) >> 0;
2293
2294
                // bit: 13-7; mask: 0x00003F80; color index for pattern background
2295 2
                $objStyle->getFill()->endcolorIndex = (0x00003F80 & $borderAndBackground) >> 7;
2296
2297
                // bit: 21-16; mask: 0x003F0000; fill pattern
2298 2
                $objStyle->getFill()->setFillType(Xls\Style\FillPattern::lookup((0x003F0000 & $borderAndBackground) >> 16));
2299
2300
                // bit: 24-22; mask: 0x01C00000; bottom line style
2301 2
                $objStyle->getBorders()->getBottom()->setBorderStyle(Xls\Style\Border::lookup((0x01C00000 & $borderAndBackground) >> 22));
2302
2303
                // bit: 31-25; mask: 0xFE000000; bottom line color
2304 2
                $objStyle->getBorders()->getBottom()->colorIndex = ((int) 0xFE000000 & $borderAndBackground) >> 25;
2305
2306
                // offset: 12; size: 4; cell border lines
2307 2
                $borderLines = self::getInt4d($recordData, 12);
2308
2309
                // bit: 2-0; mask: 0x00000007; top line style
2310 2
                $objStyle->getBorders()->getTop()->setBorderStyle(Xls\Style\Border::lookup((0x00000007 & $borderLines) >> 0));
2311
2312
                // bit: 5-3; mask: 0x00000038; left line style
2313 2
                $objStyle->getBorders()->getLeft()->setBorderStyle(Xls\Style\Border::lookup((0x00000038 & $borderLines) >> 3));
2314
2315
                // bit: 8-6; mask: 0x000001C0; right line style
2316 2
                $objStyle->getBorders()->getRight()->setBorderStyle(Xls\Style\Border::lookup((0x000001C0 & $borderLines) >> 6));
2317
2318
                // bit: 15-9; mask: 0x0000FE00; top line color index
2319 2
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000FE00 & $borderLines) >> 9;
2320
2321
                // bit: 22-16; mask: 0x007F0000; left line color index
2322 2
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & $borderLines) >> 16;
2323
2324
                // bit: 29-23; mask: 0x3F800000; right line color index
2325 2
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & $borderLines) >> 23;
2326
            }
2327
2328
            // add cellStyleXf or cellXf and update mapping
2329 93
            if ($isCellStyleXf) {
2330
                // we only read one style XF record which is always the first
2331 93
                if ($this->xfIndex == 0) {
2332 93
                    $this->spreadsheet->addCellStyleXf($objStyle);
2333 93
                    $this->mapCellStyleXfIndex[$this->xfIndex] = 0;
2334
                }
2335
            } else {
2336
                // we read all cell XF records
2337 93
                $this->spreadsheet->addCellXf($objStyle);
2338 93
                $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1;
2339
            }
2340
2341
            // update XF index for when we read next record
2342 93
            ++$this->xfIndex;
2343
        }
2344
    }
2345
2346 40
    private function readXfExt(): void
2347
    {
2348 40
        $length = self::getUInt2d($this->data, $this->pos + 2);
2349 40
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2350
2351
        // move stream pointer to next record
2352 40
        $this->pos += 4 + $length;
2353
2354 40
        if (!$this->readDataOnly) {
2355
            // offset: 0; size: 2; 0x087D = repeated header
2356
2357
            // offset: 2; size: 2
2358
2359
            // offset: 4; size: 8; not used
2360
2361
            // offset: 12; size: 2; record version
2362
2363
            // offset: 14; size: 2; index to XF record which this record modifies
2364 39
            $ixfe = self::getUInt2d($recordData, 14);
2365
2366
            // offset: 16; size: 2; not used
2367
2368
            // offset: 18; size: 2; number of extension properties that follow
2369 39
            $cexts = self::getUInt2d($recordData, 18);
0 ignored issues
show
Unused Code introduced by
The assignment to $cexts is dead and can be removed.
Loading history...
2370
2371
            // start reading the actual extension data
2372 39
            $offset = 20;
2373 39
            while ($offset < $length) {
2374
                // extension type
2375 39
                $extType = self::getUInt2d($recordData, $offset);
2376
2377
                // extension length
2378 39
                $cb = self::getUInt2d($recordData, $offset + 2);
2379
2380
                // extension data
2381 39
                $extData = substr($recordData, $offset + 4, $cb);
2382
2383
                switch ($extType) {
2384 39
                    case 4:        // fill start color
2385 39
                        $xclfType = self::getUInt2d($extData, 0); // color type
2386 39
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2387
2388 39
                        if ($xclfType == 2) {
2389 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2390
2391
                            // modify the relevant style property
2392 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2393 5
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
2394 5
                                $fill->getStartColor()->setRGB($rgb);
2395 5
                                $fill->startcolorIndex = null; // normal color index does not apply, discard
2396
                            }
2397
                        }
2398
2399 39
                        break;
2400 37
                    case 5:        // fill end color
2401 3
                        $xclfType = self::getUInt2d($extData, 0); // color type
2402 3
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2403
2404 3
                        if ($xclfType == 2) {
2405 3
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2406
2407
                            // modify the relevant style property
2408 3
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2409 3
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
2410 3
                                $fill->getEndColor()->setRGB($rgb);
2411 3
                                $fill->endcolorIndex = null; // normal color index does not apply, discard
2412
                            }
2413
                        }
2414
2415 3
                        break;
2416 37
                    case 7:        // border color top
2417 37
                        $xclfType = self::getUInt2d($extData, 0); // color type
2418 37
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2419
2420 37
                        if ($xclfType == 2) {
2421 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2422
2423
                            // modify the relevant style property
2424 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2425 2
                                $top = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getTop();
2426 2
                                $top->getColor()->setRGB($rgb);
2427 2
                                $top->colorIndex = null; // normal color index does not apply, discard
2428
                            }
2429
                        }
2430
2431 37
                        break;
2432 37
                    case 8:        // border color bottom
2433 37
                        $xclfType = self::getUInt2d($extData, 0); // color type
2434 37
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2435
2436 37
                        if ($xclfType == 2) {
2437 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2438
2439
                            // modify the relevant style property
2440 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2441 3
                                $bottom = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getBottom();
2442 3
                                $bottom->getColor()->setRGB($rgb);
2443 3
                                $bottom->colorIndex = null; // normal color index does not apply, discard
2444
                            }
2445
                        }
2446
2447 37
                        break;
2448 37
                    case 9:        // border color left
2449 37
                        $xclfType = self::getUInt2d($extData, 0); // color type
2450 37
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2451
2452 37
                        if ($xclfType == 2) {
2453 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2454
2455
                            // modify the relevant style property
2456 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2457 2
                                $left = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getLeft();
2458 2
                                $left->getColor()->setRGB($rgb);
2459 2
                                $left->colorIndex = null; // normal color index does not apply, discard
2460
                            }
2461
                        }
2462
2463 37
                        break;
2464 37
                    case 10:        // border color right
2465 37
                        $xclfType = self::getUInt2d($extData, 0); // color type
2466 37
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2467
2468 37
                        if ($xclfType == 2) {
2469 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2470
2471
                            // modify the relevant style property
2472 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2473 2
                                $right = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getRight();
2474 2
                                $right->getColor()->setRGB($rgb);
2475 2
                                $right->colorIndex = null; // normal color index does not apply, discard
2476
                            }
2477
                        }
2478
2479 37
                        break;
2480 37
                    case 11:        // border color diagonal
2481
                        $xclfType = self::getUInt2d($extData, 0); // color type
2482
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2483
2484
                        if ($xclfType == 2) {
2485
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2486
2487
                            // modify the relevant style property
2488
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2489
                                $diagonal = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getDiagonal();
2490
                                $diagonal->getColor()->setRGB($rgb);
2491
                                $diagonal->colorIndex = null; // normal color index does not apply, discard
2492
                            }
2493
                        }
2494
2495
                        break;
2496 37
                    case 13:    // font color
2497 37
                        $xclfType = self::getUInt2d($extData, 0); // color type
2498 37
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2499
2500 37
                        if ($xclfType == 2) {
2501 37
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2502
2503
                            // modify the relevant style property
2504 37
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2505 7
                                $font = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFont();
2506 7
                                $font->getColor()->setRGB($rgb);
2507 7
                                $font->colorIndex = null; // normal color index does not apply, discard
2508
                            }
2509
                        }
2510
2511 37
                        break;
2512
                }
2513
2514 39
                $offset += $cb;
2515
            }
2516
        }
2517
    }
2518
2519
    /**
2520
     * Read STYLE record.
2521
     */
2522 94
    private function readStyle(): void
2523
    {
2524 94
        $length = self::getUInt2d($this->data, $this->pos + 2);
2525 94
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2526
2527
        // move stream pointer to next record
2528 94
        $this->pos += 4 + $length;
2529
2530 94
        if (!$this->readDataOnly) {
2531
            // offset: 0; size: 2; index to XF record and flag for built-in style
2532 93
            $ixfe = self::getUInt2d($recordData, 0);
2533
2534
            // bit: 11-0; mask 0x0FFF; index to XF record
2535 93
            $xfIndex = (0x0FFF & $ixfe) >> 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $xfIndex is dead and can be removed.
Loading history...
2536
2537
            // bit: 15; mask 0x8000; 0 = user-defined style, 1 = built-in style
2538 93
            $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15);
2539
2540 93
            if ($isBuiltIn) {
2541
                // offset: 2; size: 1; identifier for built-in style
2542 93
                $builtInId = ord($recordData[2]);
2543
2544
                switch ($builtInId) {
2545 93
                    case 0x00:
2546
                        // currently, we are not using this for anything
2547 93
                        break;
2548
                    default:
2549 45
                        break;
2550
                }
2551
            }
2552
            // user-defined; not supported by PhpSpreadsheet
2553
        }
2554
    }
2555
2556
    /**
2557
     * Read PALETTE record.
2558
     */
2559 63
    private function readPalette(): void
2560
    {
2561 63
        $length = self::getUInt2d($this->data, $this->pos + 2);
2562 63
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2563
2564
        // move stream pointer to next record
2565 63
        $this->pos += 4 + $length;
2566
2567 63
        if (!$this->readDataOnly) {
2568
            // offset: 0; size: 2; number of following colors
2569 63
            $nm = self::getUInt2d($recordData, 0);
2570
2571
            // list of RGB colors
2572 63
            for ($i = 0; $i < $nm; ++$i) {
2573 63
                $rgb = substr($recordData, 2 + 4 * $i, 4);
2574 63
                $this->palette[] = self::readRGB($rgb);
2575
            }
2576
        }
2577
    }
2578
2579
    /**
2580
     * SHEET.
2581
     *
2582
     * This record is  located in the  Workbook Globals
2583
     * Substream  and represents a sheet inside the workbook.
2584
     * One SHEET record is written for each sheet. It stores the
2585
     * sheet name and a stream offset to the BOF record of the
2586
     * respective Sheet Substream within the Workbook Stream.
2587
     *
2588
     * --    "OpenOffice.org's Documentation of the Microsoft
2589
     *         Excel File Format"
2590
     */
2591 103
    private function readSheet(): void
2592
    {
2593 103
        $length = self::getUInt2d($this->data, $this->pos + 2);
2594 103
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2595
2596
        // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
2597
        // NOTE: not encrypted
2598 103
        $rec_offset = self::getInt4d($this->data, $this->pos + 4);
2599
2600
        // move stream pointer to next record
2601 103
        $this->pos += 4 + $length;
2602
2603
        // offset: 4; size: 1; sheet state
2604 103
        $sheetState = match (ord($recordData[4])) {
2605 103
            0x00 => Worksheet::SHEETSTATE_VISIBLE,
2606 103
            0x01 => Worksheet::SHEETSTATE_HIDDEN,
2607 103
            0x02 => Worksheet::SHEETSTATE_VERYHIDDEN,
2608 103
            default => Worksheet::SHEETSTATE_VISIBLE,
2609 103
        };
2610
2611
        // offset: 5; size: 1; sheet type
2612 103
        $sheetType = ord($recordData[5]);
2613
2614
        // offset: 6; size: var; sheet name
2615 103
        $rec_name = null;
2616 103
        if ($this->version == self::XLS_BIFF8) {
2617 97
            $string = self::readUnicodeStringShort(substr($recordData, 6));
2618 97
            $rec_name = $string['value'];
2619 6
        } elseif ($this->version == self::XLS_BIFF7) {
2620 6
            $string = $this->readByteStringShort(substr($recordData, 6));
2621 6
            $rec_name = $string['value'];
2622
        }
2623
2624 103
        $this->sheets[] = [
2625 103
            'name' => $rec_name,
2626 103
            'offset' => $rec_offset,
2627 103
            'sheetState' => $sheetState,
2628 103
            'sheetType' => $sheetType,
2629 103
        ];
2630
    }
2631
2632
    /**
2633
     * Read EXTERNALBOOK record.
2634
     */
2635 73
    private function readExternalBook(): void
2636
    {
2637 73
        $length = self::getUInt2d($this->data, $this->pos + 2);
2638 73
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2639
2640
        // move stream pointer to next record
2641 73
        $this->pos += 4 + $length;
2642
2643
        // offset within record data
2644 73
        $offset = 0;
2645
2646
        // there are 4 types of records
2647 73
        if (strlen($recordData) > 4) {
2648
            // external reference
2649
            // offset: 0; size: 2; number of sheet names ($nm)
2650
            $nm = self::getUInt2d($recordData, 0);
2651
            $offset += 2;
2652
2653
            // offset: 2; size: var; encoded URL without sheet name (Unicode string, 16-bit length)
2654
            $encodedUrlString = self::readUnicodeStringLong(substr($recordData, 2));
2655
            $offset += $encodedUrlString['size'];
2656
2657
            // offset: var; size: var; list of $nm sheet names (Unicode strings, 16-bit length)
2658
            $externalSheetNames = [];
2659
            for ($i = 0; $i < $nm; ++$i) {
2660
                $externalSheetNameString = self::readUnicodeStringLong(substr($recordData, $offset));
2661
                $externalSheetNames[] = $externalSheetNameString['value'];
2662
                $offset += $externalSheetNameString['size'];
2663
            }
2664
2665
            // store the record data
2666
            $this->externalBooks[] = [
2667
                'type' => 'external',
2668
                'encodedUrl' => $encodedUrlString['value'],
2669
                'externalSheetNames' => $externalSheetNames,
2670
            ];
2671 73
        } elseif (substr($recordData, 2, 2) == pack('CC', 0x01, 0x04)) {
2672
            // internal reference
2673
            // offset: 0; size: 2; number of sheet in this document
2674
            // offset: 2; size: 2; 0x01 0x04
2675 73
            $this->externalBooks[] = [
2676 73
                'type' => 'internal',
2677 73
            ];
2678
        } elseif (substr($recordData, 0, 4) == pack('vCC', 0x0001, 0x01, 0x3A)) {
2679
            // add-in function
2680
            // offset: 0; size: 2; 0x0001
2681
            $this->externalBooks[] = [
2682
                'type' => 'addInFunction',
2683
            ];
2684
        } elseif (substr($recordData, 0, 2) == pack('v', 0x0000)) {
2685
            // DDE links, OLE links
2686
            // offset: 0; size: 2; 0x0000
2687
            // offset: 2; size: var; encoded source document name
2688
            $this->externalBooks[] = [
2689
                'type' => 'DDEorOLE',
2690
            ];
2691
        }
2692
    }
2693
2694
    /**
2695
     * Read EXTERNNAME record.
2696
     */
2697
    private function readExternName(): void
2698
    {
2699
        $length = self::getUInt2d($this->data, $this->pos + 2);
2700
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2701
2702
        // move stream pointer to next record
2703
        $this->pos += 4 + $length;
2704
2705
        // external sheet references provided for named cells
2706
        if ($this->version == self::XLS_BIFF8) {
2707
            // offset: 0; size: 2; options
2708
            $options = self::getUInt2d($recordData, 0);
0 ignored issues
show
Unused Code introduced by
The assignment to $options is dead and can be removed.
Loading history...
2709
2710
            // offset: 2; size: 2;
2711
2712
            // offset: 4; size: 2; not used
2713
2714
            // offset: 6; size: var
2715
            $nameString = self::readUnicodeStringShort(substr($recordData, 6));
2716
2717
            // offset: var; size: var; formula data
2718
            $offset = 6 + $nameString['size'];
2719
            $formula = $this->getFormulaFromStructure(substr($recordData, $offset));
2720
2721
            $this->externalNames[] = [
2722
                'name' => $nameString['value'],
2723
                'formula' => $formula,
2724
            ];
2725
        }
2726
    }
2727
2728
    /**
2729
     * Read EXTERNSHEET record.
2730
     */
2731 74
    private function readExternSheet(): void
2732
    {
2733 74
        $length = self::getUInt2d($this->data, $this->pos + 2);
2734 74
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2735
2736
        // move stream pointer to next record
2737 74
        $this->pos += 4 + $length;
2738
2739
        // external sheet references provided for named cells
2740 74
        if ($this->version == self::XLS_BIFF8) {
2741
            // offset: 0; size: 2; number of following ref structures
2742 73
            $nm = self::getUInt2d($recordData, 0);
2743 73
            for ($i = 0; $i < $nm; ++$i) {
2744 71
                $this->ref[] = [
2745
                    // offset: 2 + 6 * $i; index to EXTERNALBOOK record
2746 71
                    'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i),
2747
                    // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record
2748 71
                    'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i),
2749
                    // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record
2750 71
                    'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i),
2751 71
                ];
2752
            }
2753
        }
2754
    }
2755
2756
    /**
2757
     * DEFINEDNAME.
2758
     *
2759
     * This record is part of a Link Table. It contains the name
2760
     * and the token array of an internal defined name. Token
2761
     * arrays of defined names contain tokens with aberrant
2762
     * token classes.
2763
     *
2764
     * --    "OpenOffice.org's Documentation of the Microsoft
2765
     *         Excel File Format"
2766
     */
2767 16
    private function readDefinedName(): void
2768
    {
2769 16
        $length = self::getUInt2d($this->data, $this->pos + 2);
2770 16
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2771
2772
        // move stream pointer to next record
2773 16
        $this->pos += 4 + $length;
2774
2775 16
        if ($this->version == self::XLS_BIFF8) {
2776
            // retrieves named cells
2777
2778
            // offset: 0; size: 2; option flags
2779 15
            $opts = self::getUInt2d($recordData, 0);
2780
2781
            // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name
2782 15
            $isBuiltInName = (0x0020 & $opts) >> 5;
2783
2784
            // offset: 2; size: 1; keyboard shortcut
2785
2786
            // offset: 3; size: 1; length of the name (character count)
2787 15
            $nlen = ord($recordData[3]);
2788
2789
            // offset: 4; size: 2; size of the formula data (it can happen that this is zero)
2790
            // note: there can also be additional data, this is not included in $flen
2791 15
            $flen = self::getUInt2d($recordData, 4);
2792
2793
            // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based)
2794 15
            $scope = self::getUInt2d($recordData, 8);
2795
2796
            // offset: 14; size: var; Name (Unicode string without length field)
2797 15
            $string = self::readUnicodeString(substr($recordData, 14), $nlen);
2798
2799
            // offset: var; size: $flen; formula data
2800 15
            $offset = 14 + $string['size'];
2801 15
            $formulaStructure = pack('v', $flen) . substr($recordData, $offset);
2802
2803
            try {
2804 15
                $formula = $this->getFormulaFromStructure($formulaStructure);
2805 1
            } catch (PhpSpreadsheetException) {
2806 1
                $formula = '';
2807
            }
2808
2809 15
            $this->definedname[] = [
2810 15
                'isBuiltInName' => $isBuiltInName,
2811 15
                'name' => $string['value'],
2812 15
                'formula' => $formula,
2813 15
                'scope' => $scope,
2814 15
            ];
2815
        }
2816
    }
2817
2818
    /**
2819
     * Read MSODRAWINGGROUP record.
2820
     */
2821 17
    private function readMsoDrawingGroup(): void
2822
    {
2823 17
        $length = self::getUInt2d($this->data, $this->pos + 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $length is dead and can be removed.
Loading history...
2824
2825
        // get spliced record data
2826 17
        $splicedRecordData = $this->getSplicedRecordData();
2827 17
        $recordData = $splicedRecordData['recordData'];
2828
2829 17
        $this->drawingGroupData .= $recordData;
2830
    }
2831
2832
    /**
2833
     * SST - Shared String Table.
2834
     *
2835
     * This record contains a list of all strings used anywhere
2836
     * in the workbook. Each string occurs only once. The
2837
     * workbook uses indexes into the list to reference the
2838
     * strings.
2839
     *
2840
     * --    "OpenOffice.org's Documentation of the Microsoft
2841
     *         Excel File Format"
2842
     */
2843 89
    private function readSst(): void
2844
    {
2845
        // offset within (spliced) record data
2846 89
        $pos = 0;
2847
2848
        // Limit global SST position, further control for bad SST Length in BIFF8 data
2849 89
        $limitposSST = 0;
2850
2851
        // get spliced record data
2852 89
        $splicedRecordData = $this->getSplicedRecordData();
2853
2854 89
        $recordData = $splicedRecordData['recordData'];
2855 89
        $spliceOffsets = $splicedRecordData['spliceOffsets'];
2856
2857
        // offset: 0; size: 4; total number of strings in the workbook
2858 89
        $pos += 4;
2859
2860
        // offset: 4; size: 4; number of following strings ($nm)
2861 89
        $nm = self::getInt4d($recordData, 4);
2862 89
        $pos += 4;
2863
2864
        // look up limit position
2865 89
        foreach ($spliceOffsets as $spliceOffset) {
2866
            // it can happen that the string is empty, therefore we need
2867
            // <= and not just <
2868 89
            if ($pos <= $spliceOffset) {
2869 89
                $limitposSST = $spliceOffset;
2870
            }
2871
        }
2872
2873
        // loop through the Unicode strings (16-bit length)
2874 89
        for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) {
2875
            // number of characters in the Unicode string
2876 59
            $numChars = self::getUInt2d($recordData, $pos);
2877 59
            $pos += 2;
2878
2879
            // option flags
2880 59
            $optionFlags = ord($recordData[$pos]);
2881 59
            ++$pos;
2882
2883
            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
2884 59
            $isCompressed = (($optionFlags & 0x01) == 0);
2885
2886
            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
2887 59
            $hasAsian = (($optionFlags & 0x04) != 0);
2888
2889
            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
2890 59
            $hasRichText = (($optionFlags & 0x08) != 0);
2891
2892 59
            $formattingRuns = 0;
2893 59
            if ($hasRichText) {
2894
                // number of Rich-Text formatting runs
2895 5
                $formattingRuns = self::getUInt2d($recordData, $pos);
2896 5
                $pos += 2;
2897
            }
2898
2899 59
            $extendedRunLength = 0;
2900 59
            if ($hasAsian) {
2901
                // size of Asian phonetic setting
2902
                $extendedRunLength = self::getInt4d($recordData, $pos);
2903
                $pos += 4;
2904
            }
2905
2906
            // expected byte length of character array if not split
2907 59
            $len = ($isCompressed) ? $numChars : $numChars * 2;
2908
2909
            // look up limit position - Check it again to be sure that no error occurs when parsing SST structure
2910 59
            $limitpos = null;
2911 59
            foreach ($spliceOffsets as $spliceOffset) {
2912
                // it can happen that the string is empty, therefore we need
2913
                // <= and not just <
2914 59
                if ($pos <= $spliceOffset) {
2915 59
                    $limitpos = $spliceOffset;
2916
2917 59
                    break;
2918
                }
2919
            }
2920
2921 59
            if ($pos + $len <= $limitpos) {
2922
                // character array is not split between records
2923
2924 59
                $retstr = substr($recordData, $pos, $len);
2925 59
                $pos += $len;
2926
            } else {
2927
                // character array is split between records
2928
2929
                // first part of character array
2930 1
                $retstr = substr($recordData, $pos, $limitpos - $pos);
2931
2932 1
                $bytesRead = $limitpos - $pos;
2933
2934
                // remaining characters in Unicode string
2935 1
                $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));
2936
2937 1
                $pos = $limitpos;
2938
2939
                // keep reading the characters
2940 1
                while ($charsLeft > 0) {
2941
                    // look up next limit position, in case the string span more than one continue record
2942 1
                    foreach ($spliceOffsets as $spliceOffset) {
2943 1
                        if ($pos < $spliceOffset) {
2944 1
                            $limitpos = $spliceOffset;
2945
2946 1
                            break;
2947
                        }
2948
                    }
2949
2950
                    // repeated option flags
2951
                    // OpenOffice.org documentation 5.21
2952 1
                    $option = ord($recordData[$pos]);
2953 1
                    ++$pos;
2954
2955 1
                    if ($isCompressed && ($option == 0)) {
2956
                        // 1st fragment compressed
2957
                        // this fragment compressed
2958
                        $len = min($charsLeft, $limitpos - $pos);
2959
                        $retstr .= substr($recordData, $pos, $len);
2960
                        $charsLeft -= $len;
2961
                        $isCompressed = true;
2962 1
                    } elseif (!$isCompressed && ($option != 0)) {
2963
                        // 1st fragment uncompressed
2964
                        // this fragment uncompressed
2965 1
                        $len = min($charsLeft * 2, $limitpos - $pos);
2966 1
                        $retstr .= substr($recordData, $pos, $len);
2967 1
                        $charsLeft -= $len / 2;
2968 1
                        $isCompressed = false;
2969
                    } elseif (!$isCompressed && ($option == 0)) {
2970
                        // 1st fragment uncompressed
2971
                        // this fragment compressed
2972
                        $len = min($charsLeft, $limitpos - $pos);
2973
                        for ($j = 0; $j < $len; ++$j) {
2974
                            $retstr .= $recordData[$pos + $j]
2975
                                . chr(0);
2976
                        }
2977
                        $charsLeft -= $len;
2978
                        $isCompressed = false;
2979
                    } else {
2980
                        // 1st fragment compressed
2981
                        // this fragment uncompressed
2982
                        $newstr = '';
2983
                        $jMax = strlen($retstr);
2984
                        for ($j = 0; $j < $jMax; ++$j) {
2985
                            $newstr .= $retstr[$j] . chr(0);
2986
                        }
2987
                        $retstr = $newstr;
2988
                        $len = min($charsLeft * 2, $limitpos - $pos);
2989
                        $retstr .= substr($recordData, $pos, $len);
2990
                        $charsLeft -= $len / 2;
2991
                        $isCompressed = false;
2992
                    }
2993
2994 1
                    $pos += $len;
2995
                }
2996
            }
2997
2998
            // convert to UTF-8
2999 59
            $retstr = self::encodeUTF16($retstr, $isCompressed);
3000
3001
            // read additional Rich-Text information, if any
3002 59
            $fmtRuns = [];
3003 59
            if ($hasRichText) {
3004
                // list of formatting runs
3005 5
                for ($j = 0; $j < $formattingRuns; ++$j) {
3006
                    // first formatted character; zero-based
3007 5
                    $charPos = self::getUInt2d($recordData, $pos + $j * 4);
3008
3009
                    // index to font record
3010 5
                    $fontIndex = self::getUInt2d($recordData, $pos + 2 + $j * 4);
3011
3012 5
                    $fmtRuns[] = [
3013 5
                        'charPos' => $charPos,
3014 5
                        'fontIndex' => $fontIndex,
3015 5
                    ];
3016
                }
3017 5
                $pos += 4 * $formattingRuns;
3018
            }
3019
3020
            // read additional Asian phonetics information, if any
3021 59
            if ($hasAsian) {
3022
                // For Asian phonetic settings, we skip the extended string data
3023
                $pos += $extendedRunLength;
3024
            }
3025
3026
            // store the shared sting
3027 59
            $this->sst[] = [
3028 59
                'value' => $retstr,
3029 59
                'fmtRuns' => $fmtRuns,
3030 59
            ];
3031
        }
3032
3033
        // getSplicedRecordData() takes care of moving current position in data stream
3034
    }
3035
3036
    /**
3037
     * Read PRINTGRIDLINES record.
3038
     */
3039 90
    private function readPrintGridlines(): void
3040
    {
3041 90
        $length = self::getUInt2d($this->data, $this->pos + 2);
3042 90
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3043
3044
        // move stream pointer to next record
3045 90
        $this->pos += 4 + $length;
3046
3047 90
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3048
            // offset: 0; size: 2; 0 = do not print sheet grid lines; 1 = print sheet gridlines
3049 87
            $printGridlines = (bool) self::getUInt2d($recordData, 0);
3050 87
            $this->phpSheet->setPrintGridlines($printGridlines);
3051
        }
3052
    }
3053
3054
    /**
3055
     * Read DEFAULTROWHEIGHT record.
3056
     */
3057 51
    private function readDefaultRowHeight(): void
3058
    {
3059 51
        $length = self::getUInt2d($this->data, $this->pos + 2);
3060 51
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3061
3062
        // move stream pointer to next record
3063 51
        $this->pos += 4 + $length;
3064
3065
        // offset: 0; size: 2; option flags
3066
        // offset: 2; size: 2; default height for unused rows, (twips 1/20 point)
3067 51
        $height = self::getUInt2d($recordData, 2);
3068 51
        $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20);
3069
    }
3070
3071
    /**
3072
     * Read SHEETPR record.
3073
     */
3074 92
    private function readSheetPr(): void
3075
    {
3076 92
        $length = self::getUInt2d($this->data, $this->pos + 2);
3077 92
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3078
3079
        // move stream pointer to next record
3080 92
        $this->pos += 4 + $length;
3081
3082
        // offset: 0; size: 2
3083
3084
        // bit: 6; mask: 0x0040; 0 = outline buttons above outline group
3085 92
        $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6;
3086 92
        $this->phpSheet->setShowSummaryBelow((bool) $isSummaryBelow);
3087
3088
        // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group
3089 92
        $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7;
3090 92
        $this->phpSheet->setShowSummaryRight((bool) $isSummaryRight);
3091
3092
        // bit: 8; mask: 0x100; 0 = scale printout in percent, 1 = fit printout to number of pages
3093
        // this corresponds to radio button setting in page setup dialog in Excel
3094 92
        $this->isFitToPages = (bool) ((0x0100 & self::getUInt2d($recordData, 0)) >> 8);
3095
    }
3096
3097
    /**
3098
     * Read HORIZONTALPAGEBREAKS record.
3099
     */
3100 4
    private function readHorizontalPageBreaks(): void
3101
    {
3102 4
        $length = self::getUInt2d($this->data, $this->pos + 2);
3103 4
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3104
3105
        // move stream pointer to next record
3106 4
        $this->pos += 4 + $length;
3107
3108 4
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3109
            // offset: 0; size: 2; number of the following row index structures
3110 4
            $nm = self::getUInt2d($recordData, 0);
3111
3112
            // offset: 2; size: 6 * $nm; list of $nm row index structures
3113 4
            for ($i = 0; $i < $nm; ++$i) {
3114 2
                $r = self::getUInt2d($recordData, 2 + 6 * $i);
3115 2
                $cf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3116 2
                $cl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
0 ignored issues
show
Unused Code introduced by
The assignment to $cl is dead and can be removed.
Loading history...
3117
3118
                // not sure why two column indexes are necessary?
3119 2
                $this->phpSheet->setBreak([$cf + 1, $r], Worksheet::BREAK_ROW);
3120
            }
3121
        }
3122
    }
3123
3124
    /**
3125
     * Read VERTICALPAGEBREAKS record.
3126
     */
3127 4
    private function readVerticalPageBreaks(): void
3128
    {
3129 4
        $length = self::getUInt2d($this->data, $this->pos + 2);
3130 4
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3131
3132
        // move stream pointer to next record
3133 4
        $this->pos += 4 + $length;
3134
3135 4
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3136
            // offset: 0; size: 2; number of the following column index structures
3137 4
            $nm = self::getUInt2d($recordData, 0);
3138
3139
            // offset: 2; size: 6 * $nm; list of $nm row index structures
3140 4
            for ($i = 0; $i < $nm; ++$i) {
3141 2
                $c = self::getUInt2d($recordData, 2 + 6 * $i);
3142 2
                $rf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3143
                //$rl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
3144
3145
                // not sure why two row indexes are necessary?
3146 2
                $this->phpSheet->setBreak([$c + 1, ($rf > 0) ? $rf : 1], Worksheet::BREAK_COLUMN);
3147
            }
3148
        }
3149
    }
3150
3151
    /**
3152
     * Read HEADER record.
3153
     */
3154 90
    private function readHeader(): void
3155
    {
3156 90
        $length = self::getUInt2d($this->data, $this->pos + 2);
3157 90
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3158
3159
        // move stream pointer to next record
3160 90
        $this->pos += 4 + $length;
3161
3162 90
        if (!$this->readDataOnly) {
3163
            // offset: 0; size: var
3164
            // realized that $recordData can be empty even when record exists
3165 89
            if ($recordData) {
3166 54
                if ($this->version == self::XLS_BIFF8) {
3167 53
                    $string = self::readUnicodeStringLong($recordData);
3168
                } else {
3169 1
                    $string = $this->readByteStringShort($recordData);
3170
                }
3171
3172 54
                $this->phpSheet->getHeaderFooter()->setOddHeader($string['value']);
3173 54
                $this->phpSheet->getHeaderFooter()->setEvenHeader($string['value']);
3174
            }
3175
        }
3176
    }
3177
3178
    /**
3179
     * Read FOOTER record.
3180
     */
3181 90
    private function readFooter(): void
3182
    {
3183 90
        $length = self::getUInt2d($this->data, $this->pos + 2);
3184 90
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3185
3186
        // move stream pointer to next record
3187 90
        $this->pos += 4 + $length;
3188
3189 90
        if (!$this->readDataOnly) {
3190
            // offset: 0; size: var
3191
            // realized that $recordData can be empty even when record exists
3192 89
            if ($recordData) {
3193 56
                if ($this->version == self::XLS_BIFF8) {
3194 54
                    $string = self::readUnicodeStringLong($recordData);
3195
                } else {
3196 2
                    $string = $this->readByteStringShort($recordData);
3197
                }
3198 56
                $this->phpSheet->getHeaderFooter()->setOddFooter($string['value']);
3199 56
                $this->phpSheet->getHeaderFooter()->setEvenFooter($string['value']);
3200
            }
3201
        }
3202
    }
3203
3204
    /**
3205
     * Read HCENTER record.
3206
     */
3207 90
    private function readHcenter(): void
3208
    {
3209 90
        $length = self::getUInt2d($this->data, $this->pos + 2);
3210 90
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3211
3212
        // move stream pointer to next record
3213 90
        $this->pos += 4 + $length;
3214
3215 90
        if (!$this->readDataOnly) {
3216
            // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally
3217 89
            $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0);
3218
3219 89
            $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered);
3220
        }
3221
    }
3222
3223
    /**
3224
     * Read VCENTER record.
3225
     */
3226 90
    private function readVcenter(): void
3227
    {
3228 90
        $length = self::getUInt2d($this->data, $this->pos + 2);
3229 90
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3230
3231
        // move stream pointer to next record
3232 90
        $this->pos += 4 + $length;
3233
3234 90
        if (!$this->readDataOnly) {
3235
            // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered
3236 89
            $isVerticalCentered = (bool) self::getUInt2d($recordData, 0);
3237
3238 89
            $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered);
3239
        }
3240
    }
3241
3242
    /**
3243
     * Read LEFTMARGIN record.
3244
     */
3245 86
    private function readLeftMargin(): void
3246
    {
3247 86
        $length = self::getUInt2d($this->data, $this->pos + 2);
3248 86
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3249
3250
        // move stream pointer to next record
3251 86
        $this->pos += 4 + $length;
3252
3253 86
        if (!$this->readDataOnly) {
3254
            // offset: 0; size: 8
3255 85
            $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData));
3256
        }
3257
    }
3258
3259
    /**
3260
     * Read RIGHTMARGIN record.
3261
     */
3262 86
    private function readRightMargin(): void
3263
    {
3264 86
        $length = self::getUInt2d($this->data, $this->pos + 2);
3265 86
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3266
3267
        // move stream pointer to next record
3268 86
        $this->pos += 4 + $length;
3269
3270 86
        if (!$this->readDataOnly) {
3271
            // offset: 0; size: 8
3272 85
            $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData));
3273
        }
3274
    }
3275
3276
    /**
3277
     * Read TOPMARGIN record.
3278
     */
3279 86
    private function readTopMargin(): void
3280
    {
3281 86
        $length = self::getUInt2d($this->data, $this->pos + 2);
3282 86
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3283
3284
        // move stream pointer to next record
3285 86
        $this->pos += 4 + $length;
3286
3287 86
        if (!$this->readDataOnly) {
3288
            // offset: 0; size: 8
3289 85
            $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData));
3290
        }
3291
    }
3292
3293
    /**
3294
     * Read BOTTOMMARGIN record.
3295
     */
3296 86
    private function readBottomMargin(): void
3297
    {
3298 86
        $length = self::getUInt2d($this->data, $this->pos + 2);
3299 86
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3300
3301
        // move stream pointer to next record
3302 86
        $this->pos += 4 + $length;
3303
3304 86
        if (!$this->readDataOnly) {
3305
            // offset: 0; size: 8
3306 85
            $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData));
3307
        }
3308
    }
3309
3310
    /**
3311
     * Read PAGESETUP record.
3312
     */
3313 92
    private function readPageSetup(): void
3314
    {
3315 92
        $length = self::getUInt2d($this->data, $this->pos + 2);
3316 92
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3317
3318
        // move stream pointer to next record
3319 92
        $this->pos += 4 + $length;
3320
3321 92
        if (!$this->readDataOnly) {
3322
            // offset: 0; size: 2; paper size
3323 91
            $paperSize = self::getUInt2d($recordData, 0);
3324
3325
            // offset: 2; size: 2; scaling factor
3326 91
            $scale = self::getUInt2d($recordData, 2);
3327
3328
            // offset: 6; size: 2; fit worksheet width to this number of pages, 0 = use as many as needed
3329 91
            $fitToWidth = self::getUInt2d($recordData, 6);
3330
3331
            // offset: 8; size: 2; fit worksheet height to this number of pages, 0 = use as many as needed
3332 91
            $fitToHeight = self::getUInt2d($recordData, 8);
3333
3334
            // offset: 10; size: 2; option flags
3335
3336
            // bit: 0; mask: 0x0001; 0=down then over, 1=over then down
3337 91
            $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10));
3338
3339
            // bit: 1; mask: 0x0002; 0=landscape, 1=portrait
3340 91
            $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1;
3341
3342
            // bit: 2; mask: 0x0004; 1= paper size, scaling factor, paper orient. not init
3343
            // when this bit is set, do not use flags for those properties
3344 91
            $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2;
3345
3346 91
            if (!$isNotInit) {
3347 84
                $this->phpSheet->getPageSetup()->setPaperSize($paperSize);
3348 84
                $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER);
3349 84
                $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE);
3350
3351 84
                $this->phpSheet->getPageSetup()->setScale($scale, false);
3352 84
                $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages);
3353 84
                $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false);
3354 84
                $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false);
3355
            }
3356
3357
            // offset: 16; size: 8; header margin (IEEE 754 floating-point value)
3358 91
            $marginHeader = self::extractNumber(substr($recordData, 16, 8));
3359 91
            $this->phpSheet->getPageMargins()->setHeader($marginHeader);
3360
3361
            // offset: 24; size: 8; footer margin (IEEE 754 floating-point value)
3362 91
            $marginFooter = self::extractNumber(substr($recordData, 24, 8));
3363 91
            $this->phpSheet->getPageMargins()->setFooter($marginFooter);
3364
        }
3365
    }
3366
3367
    /**
3368
     * PROTECT - Sheet protection (BIFF2 through BIFF8)
3369
     *   if this record is omitted, then it also means no sheet protection.
3370
     */
3371 6
    private function readProtect(): void
3372
    {
3373 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
3374 6
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3375
3376
        // move stream pointer to next record
3377 6
        $this->pos += 4 + $length;
3378
3379 6
        if ($this->readDataOnly) {
3380
            return;
3381
        }
3382
3383
        // offset: 0; size: 2;
3384
3385
        // bit 0, mask 0x01; 1 = sheet is protected
3386 6
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3387 6
        $this->phpSheet->getProtection()->setSheet((bool) $bool);
3388
    }
3389
3390
    /**
3391
     * SCENPROTECT.
3392
     */
3393
    private function readScenProtect(): void
3394
    {
3395
        $length = self::getUInt2d($this->data, $this->pos + 2);
3396
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3397
3398
        // move stream pointer to next record
3399
        $this->pos += 4 + $length;
3400
3401
        if ($this->readDataOnly) {
3402
            return;
3403
        }
3404
3405
        // offset: 0; size: 2;
3406
3407
        // bit: 0, mask 0x01; 1 = scenarios are protected
3408
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3409
3410
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
3411
    }
3412
3413
    /**
3414
     * OBJECTPROTECT.
3415
     */
3416 1
    private function readObjectProtect(): void
3417
    {
3418 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
3419 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3420
3421
        // move stream pointer to next record
3422 1
        $this->pos += 4 + $length;
3423
3424 1
        if ($this->readDataOnly) {
3425
            return;
3426
        }
3427
3428
        // offset: 0; size: 2;
3429
3430
        // bit: 0, mask 0x01; 1 = objects are protected
3431 1
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3432
3433 1
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
3434
    }
3435
3436
    /**
3437
     * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8).
3438
     */
3439 2
    private function readPassword(): void
3440
    {
3441 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
3442 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3443
3444
        // move stream pointer to next record
3445 2
        $this->pos += 4 + $length;
3446
3447 2
        if (!$this->readDataOnly) {
3448
            // offset: 0; size: 2; 16-bit hash value of password
3449 2
            $password = strtoupper(dechex(self::getUInt2d($recordData, 0))); // the hashed password
3450 2
            $this->phpSheet->getProtection()->setPassword($password, true);
3451
        }
3452
    }
3453
3454
    /**
3455
     * Read DEFCOLWIDTH record.
3456
     */
3457 91
    private function readDefColWidth(): void
3458
    {
3459 91
        $length = self::getUInt2d($this->data, $this->pos + 2);
3460 91
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3461
3462
        // move stream pointer to next record
3463 91
        $this->pos += 4 + $length;
3464
3465
        // offset: 0; size: 2; default column width
3466 91
        $width = self::getUInt2d($recordData, 0);
3467 91
        if ($width != 8) {
3468 4
            $this->phpSheet->getDefaultColumnDimension()->setWidth($width);
3469
        }
3470
    }
3471
3472
    /**
3473
     * Read COLINFO record.
3474
     */
3475 84
    private function readColInfo(): void
3476
    {
3477 84
        $length = self::getUInt2d($this->data, $this->pos + 2);
3478 84
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3479
3480
        // move stream pointer to next record
3481 84
        $this->pos += 4 + $length;
3482
3483 84
        if (!$this->readDataOnly) {
3484
            // offset: 0; size: 2; index to first column in range
3485 83
            $firstColumnIndex = self::getUInt2d($recordData, 0);
3486
3487
            // offset: 2; size: 2; index to last column in range
3488 83
            $lastColumnIndex = self::getUInt2d($recordData, 2);
3489
3490
            // offset: 4; size: 2; width of the column in 1/256 of the width of the zero character
3491 83
            $width = self::getUInt2d($recordData, 4);
3492
3493
            // offset: 6; size: 2; index to XF record for default column formatting
3494 83
            $xfIndex = self::getUInt2d($recordData, 6);
3495
3496
            // offset: 8; size: 2; option flags
3497
            // bit: 0; mask: 0x0001; 1= columns are hidden
3498 83
            $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0;
3499
3500
            // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline)
3501 83
            $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8;
3502
3503
            // bit: 12; mask: 0x1000; 1 = collapsed
3504 83
            $isCollapsed = (bool) ((0x1000 & self::getUInt2d($recordData, 8)) >> 12);
3505
3506
            // offset: 10; size: 2; not used
3507
3508 83
            for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
3509 83
                if ($lastColumnIndex == 255 || $lastColumnIndex == 256) {
3510 13
                    $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);
3511
3512 13
                    break;
3513
                }
3514 75
                $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256);
3515 75
                $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden);
3516 75
                $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level);
3517 75
                $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed);
3518 75
                if (isset($this->mapCellXfIndex[$xfIndex])) {
3519 73
                    $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3520
                }
3521
            }
3522
        }
3523
    }
3524
3525
    /**
3526
     * ROW.
3527
     *
3528
     * This record contains the properties of a single row in a
3529
     * sheet. Rows and cells in a sheet are divided into blocks
3530
     * of 32 rows.
3531
     *
3532
     * --    "OpenOffice.org's Documentation of the Microsoft
3533
     *         Excel File Format"
3534
     */
3535 58
    private function readRow(): void
3536
    {
3537 58
        $length = self::getUInt2d($this->data, $this->pos + 2);
3538 58
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3539
3540
        // move stream pointer to next record
3541 58
        $this->pos += 4 + $length;
3542
3543 58
        if (!$this->readDataOnly) {
3544
            // offset: 0; size: 2; index of this row
3545 57
            $r = self::getUInt2d($recordData, 0);
3546
3547
            // offset: 2; size: 2; index to column of the first cell which is described by a cell record
3548
3549
            // offset: 4; size: 2; index to column of the last cell which is described by a cell record, increased by 1
3550
3551
            // offset: 6; size: 2;
3552
3553
            // bit: 14-0; mask: 0x7FFF; height of the row, in twips = 1/20 of a point
3554 57
            $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0;
3555
3556
            // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height
3557 57
            $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;
3558
3559 57
            if (!$useDefaultHeight) {
3560 55
                $this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
3561
            }
3562
3563
            // offset: 8; size: 2; not used
3564
3565
            // offset: 10; size: 2; not used in BIFF5-BIFF8
3566
3567
            // offset: 12; size: 4; option flags and default row formatting
3568
3569
            // bit: 2-0: mask: 0x00000007; outline level of the row
3570 57
            $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0;
3571 57
            $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level);
3572
3573
            // bit: 4; mask: 0x00000010; 1 = outline group start or ends here... and is collapsed
3574 57
            $isCollapsed = (bool) ((0x00000010 & self::getInt4d($recordData, 12)) >> 4);
3575 57
            $this->phpSheet->getRowDimension($r + 1)->setCollapsed($isCollapsed);
3576
3577
            // bit: 5; mask: 0x00000020; 1 = row is hidden
3578 57
            $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5;
3579 57
            $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden);
3580
3581
            // bit: 7; mask: 0x00000080; 1 = row has explicit format
3582 57
            $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7;
3583
3584
            // bit: 27-16; mask: 0x0FFF0000; only applies when hasExplicitFormat = 1; index to XF record
3585 57
            $xfIndex = (0x0FFF0000 & self::getInt4d($recordData, 12)) >> 16;
3586
3587 57
            if ($hasExplicitFormat && isset($this->mapCellXfIndex[$xfIndex])) {
3588 6
                $this->phpSheet->getRowDimension($r + 1)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3589
            }
3590
        }
3591
    }
3592
3593
    /**
3594
     * Read RK record
3595
     * This record represents a cell that contains an RK value
3596
     * (encoded integer or floating-point value). If a
3597
     * floating-point value cannot be encoded to an RK value,
3598
     * a NUMBER record will be written. This record replaces the
3599
     * record INTEGER written in BIFF2.
3600
     *
3601
     * --    "OpenOffice.org's Documentation of the Microsoft
3602
     *         Excel File Format"
3603
     */
3604 27
    private function readRk(): void
3605
    {
3606 27
        $length = self::getUInt2d($this->data, $this->pos + 2);
3607 27
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3608
3609
        // move stream pointer to next record
3610 27
        $this->pos += 4 + $length;
3611
3612
        // offset: 0; size: 2; index to row
3613 27
        $row = self::getUInt2d($recordData, 0);
3614
3615
        // offset: 2; size: 2; index to column
3616 27
        $column = self::getUInt2d($recordData, 2);
3617 27
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3618
3619
        // Read cell?
3620 27
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3621
            // offset: 4; size: 2; index to XF record
3622 27
            $xfIndex = self::getUInt2d($recordData, 4);
3623
3624
            // offset: 6; size: 4; RK value
3625 27
            $rknum = self::getInt4d($recordData, 6);
3626 27
            $numValue = self::getIEEE754($rknum);
3627
3628 27
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3629 27
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3630
                // add style information
3631 24
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3632
            }
3633
3634
            // add cell
3635 27
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3636
        }
3637
    }
3638
3639
    /**
3640
     * Read LABELSST record
3641
     * This record represents a cell that contains a string. It
3642
     * replaces the LABEL record and RSTRING record used in
3643
     * BIFF2-BIFF5.
3644
     *
3645
     * --    "OpenOffice.org's Documentation of the Microsoft
3646
     *         Excel File Format"
3647
     */
3648 58
    private function readLabelSst(): void
3649
    {
3650 58
        $length = self::getUInt2d($this->data, $this->pos + 2);
3651 58
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3652
3653
        // move stream pointer to next record
3654 58
        $this->pos += 4 + $length;
3655
3656
        // offset: 0; size: 2; index to row
3657 58
        $row = self::getUInt2d($recordData, 0);
3658
3659
        // offset: 2; size: 2; index to column
3660 58
        $column = self::getUInt2d($recordData, 2);
3661 58
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3662
3663 58
        $emptyCell = true;
3664
        // Read cell?
3665 58
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3666
            // offset: 4; size: 2; index to XF record
3667 58
            $xfIndex = self::getUInt2d($recordData, 4);
3668
3669
            // offset: 6; size: 4; index to SST record
3670 58
            $index = self::getInt4d($recordData, 6);
3671
3672
            // add cell
3673 58
            if (($fmtRuns = $this->sst[$index]['fmtRuns']) && !$this->readDataOnly) {
3674
                // then we should treat as rich text
3675 5
                $richText = new RichText();
3676 5
                $charPos = 0;
3677 5
                $sstCount = count($this->sst[$index]['fmtRuns']);
3678 5
                for ($i = 0; $i <= $sstCount; ++$i) {
3679 5
                    if (isset($fmtRuns[$i])) {
3680 5
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, $fmtRuns[$i]['charPos'] - $charPos);
3681 5
                        $charPos = $fmtRuns[$i]['charPos'];
3682
                    } else {
3683 5
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, StringHelper::countCharacters($this->sst[$index]['value']));
3684
                    }
3685
3686 5
                    if (StringHelper::countCharacters($text) > 0) {
3687 5
                        if ($i == 0) { // first text run, no style
3688 3
                            $richText->createText($text);
3689
                        } else {
3690 5
                            $textRun = $richText->createTextRun($text);
3691 5
                            if (isset($fmtRuns[$i - 1])) {
3692 5
                                if ($fmtRuns[$i - 1]['fontIndex'] < 4) {
3693 4
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'];
3694
                                } else {
3695
                                    // this has to do with that index 4 is omitted in all BIFF versions for some stra          nge reason
3696
                                    // check the OpenOffice documentation of the FONT record
3697 4
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'] - 1;
3698
                                }
3699 5
                                if (array_key_exists($fontIndex, $this->objFonts) === false) {
3700 1
                                    $fontIndex = count($this->objFonts) - 1;
3701
                                }
3702 5
                                $textRun->setFont(clone $this->objFonts[$fontIndex]);
3703
                            }
3704
                        }
3705
                    }
3706
                }
3707 5
                if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') {
3708 5
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3709 5
                    $cell->setValueExplicit($richText, DataType::TYPE_STRING);
3710 5
                    $emptyCell = false;
3711
                }
3712
            } else {
3713 58
                if ($this->readEmptyCells || trim($this->sst[$index]['value']) !== '') {
3714 58
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3715 58
                    $cell->setValueExplicit($this->sst[$index]['value'], DataType::TYPE_STRING);
3716 58
                    $emptyCell = false;
3717
                }
3718
            }
3719
3720 58
            if (!$this->readDataOnly && !$emptyCell && isset($this->mapCellXfIndex[$xfIndex])) {
3721
                // add style information
3722 57
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cell does not seem to be defined for all execution paths leading up to this point.
Loading history...
3723
            }
3724
        }
3725
    }
3726
3727
    /**
3728
     * Read MULRK record
3729
     * This record represents a cell range containing RK value
3730
     * cells. All cells are located in the same row.
3731
     *
3732
     * --    "OpenOffice.org's Documentation of the Microsoft
3733
     *         Excel File Format"
3734
     */
3735 21
    private function readMulRk(): void
3736
    {
3737 21
        $length = self::getUInt2d($this->data, $this->pos + 2);
3738 21
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3739
3740
        // move stream pointer to next record
3741 21
        $this->pos += 4 + $length;
3742
3743
        // offset: 0; size: 2; index to row
3744 21
        $row = self::getUInt2d($recordData, 0);
3745
3746
        // offset: 2; size: 2; index to first column
3747 21
        $colFirst = self::getUInt2d($recordData, 2);
3748
3749
        // offset: var; size: 2; index to last column
3750 21
        $colLast = self::getUInt2d($recordData, $length - 2);
3751 21
        $columns = $colLast - $colFirst + 1;
3752
3753
        // offset within record data
3754 21
        $offset = 4;
3755
3756 21
        for ($i = 1; $i <= $columns; ++$i) {
3757 21
            $columnString = Coordinate::stringFromColumnIndex($colFirst + $i);
3758
3759
            // Read cell?
3760 21
            if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3761
                // offset: var; size: 2; index to XF record
3762 21
                $xfIndex = self::getUInt2d($recordData, $offset);
3763
3764
                // offset: var; size: 4; RK value
3765 21
                $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
3766 21
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3767 21
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3768
                    // add style
3769 20
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3770
                }
3771
3772
                // add cell value
3773 21
                $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3774
            }
3775
3776 21
            $offset += 6;
3777
        }
3778
    }
3779
3780
    /**
3781
     * Read NUMBER record
3782
     * This record represents a cell that contains a
3783
     * floating-point value.
3784
     *
3785
     * --    "OpenOffice.org's Documentation of the Microsoft
3786
     *         Excel File Format"
3787
     */
3788 46
    private function readNumber(): void
3789
    {
3790 46
        $length = self::getUInt2d($this->data, $this->pos + 2);
3791 46
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3792
3793
        // move stream pointer to next record
3794 46
        $this->pos += 4 + $length;
3795
3796
        // offset: 0; size: 2; index to row
3797 46
        $row = self::getUInt2d($recordData, 0);
3798
3799
        // offset: 2; size 2; index to column
3800 46
        $column = self::getUInt2d($recordData, 2);
3801 46
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3802
3803
        // Read cell?
3804 46
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3805
            // offset 4; size: 2; index to XF record
3806 46
            $xfIndex = self::getUInt2d($recordData, 4);
3807
3808 46
            $numValue = self::extractNumber(substr($recordData, 6, 8));
3809
3810 46
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3811 46
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3812
                // add cell style
3813 45
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3814
            }
3815
3816
            // add cell value
3817 46
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3818
        }
3819
    }
3820
3821
    /**
3822
     * Read FORMULA record + perhaps a following STRING record if formula result is a string
3823
     * This record contains the token array and the result of a
3824
     * formula cell.
3825
     *
3826
     * --    "OpenOffice.org's Documentation of the Microsoft
3827
     *         Excel File Format"
3828
     */
3829 29
    private function readFormula(): void
3830
    {
3831 29
        $length = self::getUInt2d($this->data, $this->pos + 2);
3832 29
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3833
3834
        // move stream pointer to next record
3835 29
        $this->pos += 4 + $length;
3836
3837
        // offset: 0; size: 2; row index
3838 29
        $row = self::getUInt2d($recordData, 0);
3839
3840
        // offset: 2; size: 2; col index
3841 29
        $column = self::getUInt2d($recordData, 2);
3842 29
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3843
3844
        // offset: 20: size: variable; formula structure
3845 29
        $formulaStructure = substr($recordData, 20);
3846
3847
        // offset: 14: size: 2; option flags, recalculate always, recalculate on open etc.
3848 29
        $options = self::getUInt2d($recordData, 14);
3849
3850
        // bit: 0; mask: 0x0001; 1 = recalculate always
3851
        // bit: 1; mask: 0x0002; 1 = calculate on open
3852
        // bit: 2; mask: 0x0008; 1 = part of a shared formula
3853 29
        $isPartOfSharedFormula = (bool) (0x0008 & $options);
3854
3855
        // WARNING:
3856
        // We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true
3857
        // the formula data may be ordinary formula data, therefore we need to check
3858
        // explicitly for the tExp token (0x01)
3859 29
        $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01;
3860
3861 29
        if ($isPartOfSharedFormula) {
3862
            // part of shared formula which means there will be a formula with a tExp token and nothing else
3863
            // get the base cell, grab tExp token
3864
            $baseRow = self::getUInt2d($formulaStructure, 3);
3865
            $baseCol = self::getUInt2d($formulaStructure, 5);
3866
            $this->baseCell = Coordinate::stringFromColumnIndex($baseCol + 1) . ($baseRow + 1);
3867
        }
3868
3869
        // Read cell?
3870 29
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3871 29
            if ($isPartOfSharedFormula) {
3872
                // formula is added to this cell after the sheet has been read
3873
                $this->sharedFormulaParts[$columnString . ($row + 1)] = $this->baseCell;
3874
            }
3875
3876
            // offset: 16: size: 4; not used
3877
3878
            // offset: 4; size: 2; XF index
3879 29
            $xfIndex = self::getUInt2d($recordData, 4);
3880
3881
            // offset: 6; size: 8; result of the formula
3882 29
            if ((ord($recordData[6]) == 0) && (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255)) {
3883
                // String formula. Result follows in appended STRING record
3884 7
                $dataType = DataType::TYPE_STRING;
3885
3886
                // read possible SHAREDFMLA record
3887 7
                $code = self::getUInt2d($this->data, $this->pos);
3888 7
                if ($code == self::XLS_TYPE_SHAREDFMLA) {
3889
                    $this->readSharedFmla();
3890
                }
3891
3892
                // read STRING record
3893 7
                $value = $this->readString();
3894
            } elseif (
3895 26
                (ord($recordData[6]) == 1)
3896 26
                && (ord($recordData[12]) == 255)
3897 26
                && (ord($recordData[13]) == 255)
3898
            ) {
3899
                // Boolean formula. Result is in +2; 0=false, 1=true
3900 2
                $dataType = DataType::TYPE_BOOL;
3901 2
                $value = (bool) ord($recordData[8]);
3902
            } elseif (
3903 25
                (ord($recordData[6]) == 2)
3904 25
                && (ord($recordData[12]) == 255)
3905 25
                && (ord($recordData[13]) == 255)
3906
            ) {
3907
                // Error formula. Error code is in +2
3908 10
                $dataType = DataType::TYPE_ERROR;
3909 10
                $value = Xls\ErrorCode::lookup(ord($recordData[8]));
3910
            } elseif (
3911 25
                (ord($recordData[6]) == 3)
3912 25
                && (ord($recordData[12]) == 255)
3913 25
                && (ord($recordData[13]) == 255)
3914
            ) {
3915
                // Formula result is a null string
3916 2
                $dataType = DataType::TYPE_NULL;
3917 2
                $value = '';
3918
            } else {
3919
                // forumla result is a number, first 14 bytes like _NUMBER record
3920 25
                $dataType = DataType::TYPE_NUMERIC;
3921 25
                $value = self::extractNumber(substr($recordData, 6, 8));
3922
            }
3923
3924 29
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3925 29
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3926
                // add cell style
3927 28
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3928
            }
3929
3930
            // store the formula
3931 29
            if (!$isPartOfSharedFormula) {
3932
                // not part of shared formula
3933
                // add cell value. If we can read formula, populate with formula, otherwise just used cached value
3934
                try {
3935 29
                    if ($this->version != self::XLS_BIFF8) {
3936 1
                        throw new Exception('Not BIFF8. Can only read BIFF8 formulas');
3937
                    }
3938 28
                    $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language
3939 28
                    $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
3940 2
                } catch (PhpSpreadsheetException) {
3941 29
                    $cell->setValueExplicit($value, $dataType);
3942
                }
3943
            } else {
3944
                if ($this->version == self::XLS_BIFF8) {
3945
                    // do nothing at this point, formula id added later in the code
3946
                } else {
3947
                    $cell->setValueExplicit($value, $dataType);
3948
                }
3949
            }
3950
3951
            // store the cached calculated value
3952 29
            $cell->setCalculatedValue($value, $dataType === DataType::TYPE_NUMERIC);
3953
        }
3954
    }
3955
3956
    /**
3957
     * Read a SHAREDFMLA record. This function just stores the binary shared formula in the reader,
3958
     * which usually contains relative references.
3959
     * These will be used to construct the formula in each shared formula part after the sheet is read.
3960
     */
3961
    private function readSharedFmla(): void
3962
    {
3963
        $length = self::getUInt2d($this->data, $this->pos + 2);
3964
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3965
3966
        // move stream pointer to next record
3967
        $this->pos += 4 + $length;
3968
3969
        // offset: 0, size: 6; cell range address of the area used by the shared formula, not used for anything
3970
        $cellRange = substr($recordData, 0, 6);
3971
        $cellRange = $this->readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax
0 ignored issues
show
Unused Code introduced by
The assignment to $cellRange is dead and can be removed.
Loading history...
3972
3973
        // offset: 6, size: 1; not used
3974
3975
        // offset: 7, size: 1; number of existing FORMULA records for this shared formula
3976
        $no = ord($recordData[7]);
0 ignored issues
show
Unused Code introduced by
The assignment to $no is dead and can be removed.
Loading history...
3977
3978
        // offset: 8, size: var; Binary token array of the shared formula
3979
        $formula = substr($recordData, 8);
3980
3981
        // at this point we only store the shared formula for later use
3982
        $this->sharedFormulas[$this->baseCell] = $formula;
3983
    }
3984
3985
    /**
3986
     * Read a STRING record from current stream position and advance the stream pointer to next record
3987
     * This record is used for storing result from FORMULA record when it is a string, and
3988
     * it occurs directly after the FORMULA record.
3989
     *
3990
     * @return string The string contents as UTF-8
3991
     */
3992 7
    private function readString()
3993
    {
3994 7
        $length = self::getUInt2d($this->data, $this->pos + 2);
3995 7
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3996
3997
        // move stream pointer to next record
3998 7
        $this->pos += 4 + $length;
3999
4000 7
        if ($this->version == self::XLS_BIFF8) {
4001 7
            $string = self::readUnicodeStringLong($recordData);
4002 7
            $value = $string['value'];
4003
        } else {
4004
            $string = $this->readByteStringLong($recordData);
4005
            $value = $string['value'];
4006
        }
4007
4008 7
        return $value;
4009
    }
4010
4011
    /**
4012
     * Read BOOLERR record
4013
     * This record represents a Boolean value or error value
4014
     * cell.
4015
     *
4016
     * --    "OpenOffice.org's Documentation of the Microsoft
4017
     *         Excel File Format"
4018
     */
4019 10
    private function readBoolErr(): void
4020
    {
4021 10
        $length = self::getUInt2d($this->data, $this->pos + 2);
4022 10
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4023
4024
        // move stream pointer to next record
4025 10
        $this->pos += 4 + $length;
4026
4027
        // offset: 0; size: 2; row index
4028 10
        $row = self::getUInt2d($recordData, 0);
4029
4030
        // offset: 2; size: 2; column index
4031 10
        $column = self::getUInt2d($recordData, 2);
4032 10
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4033
4034
        // Read cell?
4035 10
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4036
            // offset: 4; size: 2; index to XF record
4037 10
            $xfIndex = self::getUInt2d($recordData, 4);
4038
4039
            // offset: 6; size: 1; the boolean value or error value
4040 10
            $boolErr = ord($recordData[6]);
4041
4042
            // offset: 7; size: 1; 0=boolean; 1=error
4043 10
            $isError = ord($recordData[7]);
4044
4045 10
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4046
            switch ($isError) {
4047 10
                case 0: // boolean
4048 10
                    $value = (bool) $boolErr;
4049
4050
                    // add cell value
4051 10
                    $cell->setValueExplicit($value, DataType::TYPE_BOOL);
4052
4053 10
                    break;
4054
                case 1: // error type
4055
                    $value = Xls\ErrorCode::lookup($boolErr);
4056
4057
                    // add cell value
4058
                    $cell->setValueExplicit($value, DataType::TYPE_ERROR);
4059
4060
                    break;
4061
            }
4062
4063 10
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4064
                // add cell style
4065 9
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4066
            }
4067
        }
4068
    }
4069
4070
    /**
4071
     * Read MULBLANK record
4072
     * This record represents a cell range of empty cells. All
4073
     * cells are located in the same row.
4074
     *
4075
     * --    "OpenOffice.org's Documentation of the Microsoft
4076
     *         Excel File Format"
4077
     */
4078 25
    private function readMulBlank(): void
4079
    {
4080 25
        $length = self::getUInt2d($this->data, $this->pos + 2);
4081 25
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4082
4083
        // move stream pointer to next record
4084 25
        $this->pos += 4 + $length;
4085
4086
        // offset: 0; size: 2; index to row
4087 25
        $row = self::getUInt2d($recordData, 0);
4088
4089
        // offset: 2; size: 2; index to first column
4090 25
        $fc = self::getUInt2d($recordData, 2);
4091
4092
        // offset: 4; size: 2 x nc; list of indexes to XF records
4093
        // add style information
4094 25
        if (!$this->readDataOnly && $this->readEmptyCells) {
4095 24
            for ($i = 0; $i < $length / 2 - 3; ++$i) {
4096 24
                $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);
4097
4098
                // Read cell?
4099 24
                if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4100 24
                    $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
4101 24
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
4102 24
                        $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4103
                    }
4104
                }
4105
            }
4106
        }
4107
4108
        // offset: 6; size 2; index to last column (not needed)
4109
    }
4110
4111
    /**
4112
     * Read LABEL record
4113
     * This record represents a cell that contains a string. In
4114
     * BIFF8 it is usually replaced by the LABELSST record.
4115
     * Excel still uses this record, if it copies unformatted
4116
     * text cells to the clipboard.
4117
     *
4118
     * --    "OpenOffice.org's Documentation of the Microsoft
4119
     *         Excel File Format"
4120
     */
4121 3
    private function readLabel(): void
4122
    {
4123 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
4124 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4125
4126
        // move stream pointer to next record
4127 3
        $this->pos += 4 + $length;
4128
4129
        // offset: 0; size: 2; index to row
4130 3
        $row = self::getUInt2d($recordData, 0);
4131
4132
        // offset: 2; size: 2; index to column
4133 3
        $column = self::getUInt2d($recordData, 2);
4134 3
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4135
4136
        // Read cell?
4137 3
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4138
            // offset: 4; size: 2; XF index
4139 3
            $xfIndex = self::getUInt2d($recordData, 4);
4140
4141
            // add cell value
4142
            // todo: what if string is very long? continue record
4143 3
            if ($this->version == self::XLS_BIFF8) {
4144 1
                $string = self::readUnicodeStringLong(substr($recordData, 6));
4145 1
                $value = $string['value'];
4146
            } else {
4147 2
                $string = $this->readByteStringLong(substr($recordData, 6));
4148 2
                $value = $string['value'];
4149
            }
4150 3
            if ($this->readEmptyCells || trim($value) !== '') {
4151 3
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4152 3
                $cell->setValueExplicit($value, DataType::TYPE_STRING);
4153
4154 3
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4155
                    // add cell style
4156 3
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4157
                }
4158
            }
4159
        }
4160
    }
4161
4162
    /**
4163
     * Read BLANK record.
4164
     */
4165 24
    private function readBlank(): void
4166
    {
4167 24
        $length = self::getUInt2d($this->data, $this->pos + 2);
4168 24
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4169
4170
        // move stream pointer to next record
4171 24
        $this->pos += 4 + $length;
4172
4173
        // offset: 0; size: 2; row index
4174 24
        $row = self::getUInt2d($recordData, 0);
4175
4176
        // offset: 2; size: 2; col index
4177 24
        $col = self::getUInt2d($recordData, 2);
4178 24
        $columnString = Coordinate::stringFromColumnIndex($col + 1);
4179
4180
        // Read cell?
4181 24
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4182
            // offset: 4; size: 2; XF index
4183 24
            $xfIndex = self::getUInt2d($recordData, 4);
4184
4185
            // add style information
4186 24
            if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
4187 24
                $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4188
            }
4189
        }
4190
    }
4191
4192
    /**
4193
     * Read MSODRAWING record.
4194
     */
4195 16
    private function readMsoDrawing(): void
4196
    {
4197 16
        $length = self::getUInt2d($this->data, $this->pos + 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $length is dead and can be removed.
Loading history...
4198
4199
        // get spliced record data
4200 16
        $splicedRecordData = $this->getSplicedRecordData();
4201 16
        $recordData = $splicedRecordData['recordData'];
4202
4203 16
        $this->drawingData .= $recordData;
4204
    }
4205
4206
    /**
4207
     * Read OBJ record.
4208
     */
4209 12
    private function readObj(): void
4210
    {
4211 12
        $length = self::getUInt2d($this->data, $this->pos + 2);
4212 12
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4213
4214
        // move stream pointer to next record
4215 12
        $this->pos += 4 + $length;
4216
4217 12
        if ($this->readDataOnly || $this->version != self::XLS_BIFF8) {
4218 1
            return;
4219
        }
4220
4221
        // recordData consists of an array of subrecords looking like this:
4222
        //    ft: 2 bytes; ftCmo type (0x15)
4223
        //    cb: 2 bytes; size in bytes of ftCmo data
4224
        //    ot: 2 bytes; Object Type
4225
        //    id: 2 bytes; Object id number
4226
        //    grbit: 2 bytes; Option Flags
4227
        //    data: var; subrecord data
4228
4229
        // for now, we are just interested in the second subrecord containing the object type
4230 11
        $ftCmoType = self::getUInt2d($recordData, 0);
4231 11
        $cbCmoSize = self::getUInt2d($recordData, 2);
4232 11
        $otObjType = self::getUInt2d($recordData, 4);
4233 11
        $idObjID = self::getUInt2d($recordData, 6);
4234 11
        $grbitOpts = self::getUInt2d($recordData, 6);
4235
4236 11
        $this->objs[] = [
4237 11
            'ftCmoType' => $ftCmoType,
4238 11
            'cbCmoSize' => $cbCmoSize,
4239 11
            'otObjType' => $otObjType,
4240 11
            'idObjID' => $idObjID,
4241 11
            'grbitOpts' => $grbitOpts,
4242 11
        ];
4243 11
        $this->textObjRef = $idObjID;
4244
    }
4245
4246
    /**
4247
     * Read WINDOW2 record.
4248
     */
4249 93
    private function readWindow2(): void
4250
    {
4251 93
        $length = self::getUInt2d($this->data, $this->pos + 2);
4252 93
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4253
4254
        // move stream pointer to next record
4255 93
        $this->pos += 4 + $length;
4256
4257
        // offset: 0; size: 2; option flags
4258 93
        $options = self::getUInt2d($recordData, 0);
4259
4260
        // offset: 2; size: 2; index to first visible row
4261 93
        $firstVisibleRow = self::getUInt2d($recordData, 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $firstVisibleRow is dead and can be removed.
Loading history...
4262
4263
        // offset: 4; size: 2; index to first visible colum
4264 93
        $firstVisibleColumn = self::getUInt2d($recordData, 4);
0 ignored issues
show
Unused Code introduced by
The assignment to $firstVisibleColumn is dead and can be removed.
Loading history...
4265 93
        $zoomscaleInPageBreakPreview = 0;
4266 93
        $zoomscaleInNormalView = 0;
4267 93
        if ($this->version === self::XLS_BIFF8) {
4268
            // offset:  8; size: 2; not used
4269
            // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%)
4270
            // offset: 12; size: 2; cached magnification factor in normal view (in percent); 0 = Default (100%)
4271
            // offset: 14; size: 4; not used
4272 91
            if (!isset($recordData[10])) {
4273
                $zoomscaleInPageBreakPreview = 0;
4274
            } else {
4275 91
                $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10);
4276
            }
4277
4278 91
            if ($zoomscaleInPageBreakPreview === 0) {
4279 88
                $zoomscaleInPageBreakPreview = 60;
4280
            }
4281
4282 91
            if (!isset($recordData[12])) {
4283
                $zoomscaleInNormalView = 0;
4284
            } else {
4285 91
                $zoomscaleInNormalView = self::getUInt2d($recordData, 12);
4286
            }
4287
4288 91
            if ($zoomscaleInNormalView === 0) {
4289 40
                $zoomscaleInNormalView = 100;
4290
            }
4291
        }
4292
4293
        // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines
4294 93
        $showGridlines = (bool) ((0x0002 & $options) >> 1);
4295 93
        $this->phpSheet->setShowGridlines($showGridlines);
4296
4297
        // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers
4298 93
        $showRowColHeaders = (bool) ((0x0004 & $options) >> 2);
4299 93
        $this->phpSheet->setShowRowColHeaders($showRowColHeaders);
4300
4301
        // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen
4302 93
        $this->frozen = (bool) ((0x0008 & $options) >> 3);
4303
4304
        // bit: 6; mask: 0x0040; 0 = columns from left to right, 1 = columns from right to left
4305 93
        $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6));
4306
4307
        // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active
4308 93
        $isActive = (bool) ((0x0400 & $options) >> 10);
4309 93
        if ($isActive) {
4310 89
            $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet));
4311 89
            $this->activeSheetSet = true;
4312
        }
4313
4314
        // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view
4315 93
        $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11);
4316
4317
        //FIXME: set $firstVisibleRow and $firstVisibleColumn
4318
4319 93
        if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) {
4320
            //NOTE: this setting is inferior to page layout view(Excel2007-)
4321 93
            $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL;
4322 93
            $this->phpSheet->getSheetView()->setView($view);
4323 93
            if ($this->version === self::XLS_BIFF8) {
4324 91
                $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView;
4325 91
                $this->phpSheet->getSheetView()->setZoomScale($zoomScale);
4326 91
                $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView);
4327
            }
4328
        }
4329
    }
4330
4331
    /**
4332
     * Read PLV Record(Created by Excel2007 or upper).
4333
     */
4334 81
    private function readPageLayoutView(): void
4335
    {
4336 81
        $length = self::getUInt2d($this->data, $this->pos + 2);
4337 81
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4338
4339
        // move stream pointer to next record
4340 81
        $this->pos += 4 + $length;
4341
4342
        // offset: 0; size: 2; rt
4343
        //->ignore
4344 81
        $rt = self::getUInt2d($recordData, 0);
0 ignored issues
show
Unused Code introduced by
The assignment to $rt is dead and can be removed.
Loading history...
4345
        // offset: 2; size: 2; grbitfr
4346
        //->ignore
4347 81
        $grbitFrt = self::getUInt2d($recordData, 2);
0 ignored issues
show
Unused Code introduced by
The assignment to $grbitFrt is dead and can be removed.
Loading history...
4348
        // offset: 4; size: 8; reserved
4349
        //->ignore
4350
4351
        // offset: 12; size 2; zoom scale
4352 81
        $wScalePLV = self::getUInt2d($recordData, 12);
4353
        // offset: 14; size 2; grbit
4354 81
        $grbit = self::getUInt2d($recordData, 14);
4355
4356
        // decomprise grbit
4357 81
        $fPageLayoutView = $grbit & 0x01;
4358 81
        $fRulerVisible = ($grbit >> 1) & 0x01; //no support
0 ignored issues
show
Unused Code introduced by
The assignment to $fRulerVisible is dead and can be removed.
Loading history...
4359 81
        $fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support
0 ignored issues
show
Unused Code introduced by
The assignment to $fWhitespaceHidden is dead and can be removed.
Loading history...
4360
4361 81
        if ($fPageLayoutView === 1) {
4362
            $this->phpSheet->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_LAYOUT);
4363
            $this->phpSheet->getSheetView()->setZoomScale($wScalePLV); //set by Excel2007 only if SHEETVIEW_PAGE_LAYOUT
4364
        }
4365
        //otherwise, we cannot know whether SHEETVIEW_PAGE_LAYOUT or SHEETVIEW_PAGE_BREAK_PREVIEW.
4366
    }
4367
4368
    /**
4369
     * Read SCL record.
4370
     */
4371 5
    private function readScl(): void
4372
    {
4373 5
        $length = self::getUInt2d($this->data, $this->pos + 2);
4374 5
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4375
4376
        // move stream pointer to next record
4377 5
        $this->pos += 4 + $length;
4378
4379
        // offset: 0; size: 2; numerator of the view magnification
4380 5
        $numerator = self::getUInt2d($recordData, 0);
4381
4382
        // offset: 2; size: 2; numerator of the view magnification
4383 5
        $denumerator = self::getUInt2d($recordData, 2);
4384
4385
        // set the zoom scale (in percent)
4386 5
        $this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator);
4387
    }
4388
4389
    /**
4390
     * Read PANE record.
4391
     */
4392 8
    private function readPane(): void
4393
    {
4394 8
        $length = self::getUInt2d($this->data, $this->pos + 2);
4395 8
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4396
4397
        // move stream pointer to next record
4398 8
        $this->pos += 4 + $length;
4399
4400 8
        if (!$this->readDataOnly) {
4401
            // offset: 0; size: 2; position of vertical split
4402 8
            $px = self::getUInt2d($recordData, 0);
4403
4404
            // offset: 2; size: 2; position of horizontal split
4405 8
            $py = self::getUInt2d($recordData, 2);
4406
4407
            // offset: 4; size: 2; top most visible row in the bottom pane
4408 8
            $rwTop = self::getUInt2d($recordData, 4);
4409
4410
            // offset: 6; size: 2; first visible left column in the right pane
4411 8
            $colLeft = self::getUInt2d($recordData, 6);
4412
4413 8
            if ($this->frozen) {
4414
                // frozen panes
4415 8
                $cell = Coordinate::stringFromColumnIndex($px + 1) . ($py + 1);
4416 8
                $topLeftCell = Coordinate::stringFromColumnIndex($colLeft + 1) . ($rwTop + 1);
4417 8
                $this->phpSheet->freezePane($cell, $topLeftCell);
4418
            }
4419
            // unfrozen panes; split windows; not supported by PhpSpreadsheet core
4420
        }
4421
    }
4422
4423
    /**
4424
     * Read SELECTION record. There is one such record for each pane in the sheet.
4425
     */
4426 90
    private function readSelection(): void
4427
    {
4428 90
        $length = self::getUInt2d($this->data, $this->pos + 2);
4429 90
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4430
4431
        // move stream pointer to next record
4432 90
        $this->pos += 4 + $length;
4433
4434 90
        if (!$this->readDataOnly) {
4435
            // offset: 0; size: 1; pane identifier
4436 89
            $paneId = ord($recordData[0]);
0 ignored issues
show
Unused Code introduced by
The assignment to $paneId is dead and can be removed.
Loading history...
4437
4438
            // offset: 1; size: 2; index to row of the active cell
4439 89
            $r = self::getUInt2d($recordData, 1);
0 ignored issues
show
Unused Code introduced by
The assignment to $r is dead and can be removed.
Loading history...
4440
4441
            // offset: 3; size: 2; index to column of the active cell
4442 89
            $c = self::getUInt2d($recordData, 3);
0 ignored issues
show
Unused Code introduced by
The assignment to $c is dead and can be removed.
Loading history...
4443
4444
            // offset: 5; size: 2; index into the following cell range list to the
4445
            //  entry that contains the active cell
4446 89
            $index = self::getUInt2d($recordData, 5);
0 ignored issues
show
Unused Code introduced by
The assignment to $index is dead and can be removed.
Loading history...
4447
4448
            // offset: 7; size: var; cell range address list containing all selected cell ranges
4449 89
            $data = substr($recordData, 7);
4450 89
            $cellRangeAddressList = $this->readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax
4451
4452 89
            $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];
4453
4454
            // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!)
4455 89
            if (preg_match('/^([A-Z]+1\:[A-Z]+)16384$/', $selectedCells)) {
4456
                $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells);
4457
            }
4458
4459
            // first row '1' + last row '65536' indicates that full column is selected
4460 89
            if (preg_match('/^([A-Z]+1\:[A-Z]+)65536$/', $selectedCells)) {
4461
                $selectedCells = (string) preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells);
4462
            }
4463
4464
            // first column 'A' + last column 'IV' indicates that full row is selected
4465 89
            if (preg_match('/^(A\d+\:)IV(\d+)$/', $selectedCells)) {
4466 2
                $selectedCells = (string) preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells);
4467
            }
4468
4469 89
            $this->phpSheet->setSelectedCells($selectedCells);
4470
        }
4471
    }
4472
4473 17
    private function includeCellRangeFiltered(string $cellRangeAddress): bool
4474
    {
4475 17
        $includeCellRange = true;
4476 17
        if ($this->getReadFilter() !== null) {
4477 17
            $includeCellRange = false;
4478 17
            $rangeBoundaries = Coordinate::getRangeBoundaries($cellRangeAddress);
4479 17
            ++$rangeBoundaries[1][0];
4480 17
            for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) {
4481 17
                for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; ++$column) {
4482 17
                    if ($this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
4483 17
                        $includeCellRange = true;
4484
4485 17
                        break 2;
4486
                    }
4487
                }
4488
            }
4489
        }
4490
4491 17
        return $includeCellRange;
4492
    }
4493
4494
    /**
4495
     * MERGEDCELLS.
4496
     *
4497
     * This record contains the addresses of merged cell ranges
4498
     * in the current sheet.
4499
     *
4500
     * --    "OpenOffice.org's Documentation of the Microsoft
4501
     *         Excel File Format"
4502
     */
4503 18
    private function readMergedCells(): void
4504
    {
4505 18
        $length = self::getUInt2d($this->data, $this->pos + 2);
4506 18
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4507
4508
        // move stream pointer to next record
4509 18
        $this->pos += 4 + $length;
4510
4511 18
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
4512 17
            $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($recordData);
4513 17
            foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) {
4514
                if (
4515 17
                    (str_contains($cellRangeAddress, ':')) &&
4516 17
                    ($this->includeCellRangeFiltered($cellRangeAddress))
4517
                ) {
4518 17
                    $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE);
4519
                }
4520
            }
4521
        }
4522
    }
4523
4524
    /**
4525
     * Read HYPERLINK record.
4526
     */
4527 6
    private function readHyperLink(): void
4528
    {
4529 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
4530 6
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4531
4532
        // move stream pointer forward to next record
4533 6
        $this->pos += 4 + $length;
4534
4535 6
        if (!$this->readDataOnly) {
4536
            // offset: 0; size: 8; cell range address of all cells containing this hyperlink
4537
            try {
4538 6
                $cellRange = $this->readBIFF8CellRangeAddressFixed($recordData);
4539
            } catch (PhpSpreadsheetException) {
4540
                return;
4541
            }
4542
4543
            // offset: 8, size: 16; GUID of StdLink
4544
4545
            // offset: 24, size: 4; unknown value
4546
4547
            // offset: 28, size: 4; option flags
4548
            // bit: 0; mask: 0x00000001; 0 = no link or extant, 1 = file link or URL
4549 6
            $isFileLinkOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 0;
4550
4551
            // bit: 1; mask: 0x00000002; 0 = relative path, 1 = absolute path or URL
4552 6
            $isAbsPathOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 1;
0 ignored issues
show
Unused Code introduced by
The assignment to $isAbsPathOrUrl is dead and can be removed.
Loading history...
4553
4554
            // bit: 2 (and 4); mask: 0x00000014; 0 = no description
4555 6
            $hasDesc = (0x00000014 & self::getUInt2d($recordData, 28)) >> 2;
4556
4557
            // bit: 3; mask: 0x00000008; 0 = no text, 1 = has text
4558 6
            $hasText = (0x00000008 & self::getUInt2d($recordData, 28)) >> 3;
4559
4560
            // bit: 7; mask: 0x00000080; 0 = no target frame, 1 = has target frame
4561 6
            $hasFrame = (0x00000080 & self::getUInt2d($recordData, 28)) >> 7;
4562
4563
            // bit: 8; mask: 0x00000100; 0 = file link or URL, 1 = UNC path (inc. server name)
4564 6
            $isUNC = (0x00000100 & self::getUInt2d($recordData, 28)) >> 8;
4565
4566
            // offset within record data
4567 6
            $offset = 32;
4568
4569 6
            if ($hasDesc) {
4570
                // offset: 32; size: var; character count of description text
4571 3
                $dl = self::getInt4d($recordData, 32);
4572
                // offset: 36; size: var; character array of description text, no Unicode string header, always 16-bit characters, zero terminated
4573 3
                $desc = self::encodeUTF16(substr($recordData, 36, 2 * ($dl - 1)), false);
0 ignored issues
show
Unused Code introduced by
The assignment to $desc is dead and can be removed.
Loading history...
4574 3
                $offset += 4 + 2 * $dl;
4575
            }
4576 6
            if ($hasFrame) {
4577
                $fl = self::getInt4d($recordData, $offset);
4578
                $offset += 4 + 2 * $fl;
4579
            }
4580
4581
            // detect type of hyperlink (there are 4 types)
4582 6
            $hyperlinkType = null;
4583
4584 6
            if ($isUNC) {
4585
                $hyperlinkType = 'UNC';
4586 6
            } elseif (!$isFileLinkOrUrl) {
4587 3
                $hyperlinkType = 'workbook';
4588 6
            } elseif (ord($recordData[$offset]) == 0x03) {
4589
                $hyperlinkType = 'local';
4590 6
            } elseif (ord($recordData[$offset]) == 0xE0) {
4591 6
                $hyperlinkType = 'URL';
4592
            }
4593
4594
            switch ($hyperlinkType) {
4595 6
                case 'URL':
4596
                    // section 5.58.2: Hyperlink containing a URL
4597
                    // e.g. http://example.org/index.php
4598
4599
                    // offset: var; size: 16; GUID of URL Moniker
4600 6
                    $offset += 16;
4601
                    // offset: var; size: 4; size (in bytes) of character array of the URL including trailing zero word
4602 6
                    $us = self::getInt4d($recordData, $offset);
4603 6
                    $offset += 4;
4604
                    // offset: var; size: $us; character array of the URL, no Unicode string header, always 16-bit characters, zero-terminated
4605 6
                    $url = self::encodeUTF16(substr($recordData, $offset, $us - 2), false);
4606 6
                    $nullOffset = strpos($url, chr(0x00));
4607 6
                    if ($nullOffset) {
4608 3
                        $url = substr($url, 0, $nullOffset);
4609
                    }
4610 6
                    $url .= $hasText ? '#' : '';
4611 6
                    $offset += $us;
4612
4613 6
                    break;
4614 3
                case 'local':
4615
                    // section 5.58.3: Hyperlink to local file
4616
                    // examples:
4617
                    //   mydoc.txt
4618
                    //   ../../somedoc.xls#Sheet!A1
4619
4620
                    // offset: var; size: 16; GUI of File Moniker
4621
                    $offset += 16;
4622
4623
                    // offset: var; size: 2; directory up-level count.
4624
                    $upLevelCount = self::getUInt2d($recordData, $offset);
4625
                    $offset += 2;
4626
4627
                    // offset: var; size: 4; character count of the shortened file path and name, including trailing zero word
4628
                    $sl = self::getInt4d($recordData, $offset);
4629
                    $offset += 4;
4630
4631
                    // offset: var; size: sl; character array of the shortened file path and name in 8.3-DOS-format (compressed Unicode string)
4632
                    $shortenedFilePath = substr($recordData, $offset, $sl);
4633
                    $shortenedFilePath = self::encodeUTF16($shortenedFilePath, true);
4634
                    $shortenedFilePath = substr($shortenedFilePath, 0, -1); // remove trailing zero
4635
4636
                    $offset += $sl;
4637
4638
                    // offset: var; size: 24; unknown sequence
4639
                    $offset += 24;
4640
4641
                    // extended file path
4642
                    // offset: var; size: 4; size of the following file link field including string lenth mark
4643
                    $sz = self::getInt4d($recordData, $offset);
4644
                    $offset += 4;
4645
4646
                    // only present if $sz > 0
4647
                    if ($sz > 0) {
4648
                        // offset: var; size: 4; size of the character array of the extended file path and name
4649
                        $xl = self::getInt4d($recordData, $offset);
4650
                        $offset += 4;
4651
4652
                        // offset: var; size 2; unknown
4653
                        $offset += 2;
4654
4655
                        // offset: var; size $xl; character array of the extended file path and name.
4656
                        $extendedFilePath = substr($recordData, $offset, $xl);
4657
                        $extendedFilePath = self::encodeUTF16($extendedFilePath, false);
4658
                        $offset += $xl;
4659
                    }
4660
4661
                    // construct the path
4662
                    $url = str_repeat('..\\', $upLevelCount);
4663
                    $url .= ($sz > 0) ? $extendedFilePath : $shortenedFilePath; // use extended path if available
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $extendedFilePath does not seem to be defined for all execution paths leading up to this point.
Loading history...
4664
                    $url .= $hasText ? '#' : '';
4665
4666
                    break;
4667 3
                case 'UNC':
4668
                    // section 5.58.4: Hyperlink to a File with UNC (Universal Naming Convention) Path
4669
                    // todo: implement
4670
                    return;
4671 3
                case 'workbook':
4672
                    // section 5.58.5: Hyperlink to the Current Workbook
4673
                    // e.g. Sheet2!B1:C2, stored in text mark field
4674 3
                    $url = 'sheet://';
4675
4676 3
                    break;
4677
                default:
4678
                    return;
4679
            }
4680
4681 6
            if ($hasText) {
4682
                // offset: var; size: 4; character count of text mark including trailing zero word
4683 3
                $tl = self::getInt4d($recordData, $offset);
4684 3
                $offset += 4;
4685
                // offset: var; size: var; character array of the text mark without the # sign, no Unicode header, always 16-bit characters, zero-terminated
4686 3
                $text = self::encodeUTF16(substr($recordData, $offset, 2 * ($tl - 1)), false);
4687 3
                $url .= $text;
4688
            }
4689
4690
            // apply the hyperlink to all the relevant cells
4691 6
            foreach (Coordinate::extractAllCellReferencesInRange($cellRange) as $coordinate) {
4692 6
                $this->phpSheet->getCell($coordinate)->getHyperLink()->setUrl($url);
4693
            }
4694
        }
4695
    }
4696
4697
    /**
4698
     * Read DATAVALIDATIONS record.
4699
     */
4700 3
    private function readDataValidations(): void
4701
    {
4702 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
4703 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
0 ignored issues
show
Unused Code introduced by
The assignment to $recordData is dead and can be removed.
Loading history...
4704
4705
        // move stream pointer forward to next record
4706 3
        $this->pos += 4 + $length;
4707
    }
4708
4709
    /**
4710
     * Read DATAVALIDATION record.
4711
     */
4712 3
    private function readDataValidation(): void
4713
    {
4714 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
4715 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4716
4717
        // move stream pointer forward to next record
4718 3
        $this->pos += 4 + $length;
4719
4720 3
        if ($this->readDataOnly) {
4721
            return;
4722
        }
4723
4724
        // offset: 0; size: 4; Options
4725 3
        $options = self::getInt4d($recordData, 0);
4726
4727
        // bit: 0-3; mask: 0x0000000F; type
4728 3
        $type = (0x0000000F & $options) >> 0;
4729 3
        $type = Xls\DataValidationHelper::type($type);
4730
4731
        // bit: 4-6; mask: 0x00000070; error type
4732 3
        $errorStyle = (0x00000070 & $options) >> 4;
4733 3
        $errorStyle = Xls\DataValidationHelper::errorStyle($errorStyle);
4734
4735
        // bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list)
4736
        // I have only seen cases where this is 1
4737 3
        $explicitFormula = (0x00000080 & $options) >> 7;
0 ignored issues
show
Unused Code introduced by
The assignment to $explicitFormula is dead and can be removed.
Loading history...
4738
4739
        // bit: 8; mask: 0x00000100; 1= empty cells allowed
4740 3
        $allowBlank = (0x00000100 & $options) >> 8;
4741
4742
        // bit: 9; mask: 0x00000200; 1= suppress drop down arrow in list type validity
4743 3
        $suppressDropDown = (0x00000200 & $options) >> 9;
4744
4745
        // bit: 18; mask: 0x00040000; 1= show prompt box if cell selected
4746 3
        $showInputMessage = (0x00040000 & $options) >> 18;
4747
4748
        // bit: 19; mask: 0x00080000; 1= show error box if invalid values entered
4749 3
        $showErrorMessage = (0x00080000 & $options) >> 19;
4750
4751
        // bit: 20-23; mask: 0x00F00000; condition operator
4752 3
        $operator = (0x00F00000 & $options) >> 20;
4753 3
        $operator = Xls\DataValidationHelper::operator($operator);
4754
4755 3
        if ($type === null || $errorStyle === null || $operator === null) {
4756
            return;
4757
        }
4758
4759
        // offset: 4; size: var; title of the prompt box
4760 3
        $offset = 4;
4761 3
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4762 3
        $promptTitle = $string['value'] !== chr(0) ? $string['value'] : '';
4763 3
        $offset += $string['size'];
4764
4765
        // offset: var; size: var; title of the error box
4766 3
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4767 3
        $errorTitle = $string['value'] !== chr(0) ? $string['value'] : '';
4768 3
        $offset += $string['size'];
4769
4770
        // offset: var; size: var; text of the prompt box
4771 3
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4772 3
        $prompt = $string['value'] !== chr(0) ? $string['value'] : '';
4773 3
        $offset += $string['size'];
4774
4775
        // offset: var; size: var; text of the error box
4776 3
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4777 3
        $error = $string['value'] !== chr(0) ? $string['value'] : '';
4778 3
        $offset += $string['size'];
4779
4780
        // offset: var; size: 2; size of the formula data for the first condition
4781 3
        $sz1 = self::getUInt2d($recordData, $offset);
4782 3
        $offset += 2;
4783
4784
        // offset: var; size: 2; not used
4785 3
        $offset += 2;
4786
4787
        // offset: var; size: $sz1; formula data for first condition (without size field)
4788 3
        $formula1 = substr($recordData, $offset, $sz1);
4789 3
        $formula1 = pack('v', $sz1) . $formula1; // prepend the length
4790
4791
        try {
4792 3
            $formula1 = $this->getFormulaFromStructure($formula1);
4793
4794
            // in list type validity, null characters are used as item separators
4795 3
            if ($type == DataValidation::TYPE_LIST) {
4796 3
                $formula1 = str_replace(chr(0), ',', $formula1);
4797
            }
4798
        } catch (PhpSpreadsheetException $e) {
4799
            return;
4800
        }
4801 3
        $offset += $sz1;
4802
4803
        // offset: var; size: 2; size of the formula data for the first condition
4804 3
        $sz2 = self::getUInt2d($recordData, $offset);
4805 3
        $offset += 2;
4806
4807
        // offset: var; size: 2; not used
4808 3
        $offset += 2;
4809
4810
        // offset: var; size: $sz2; formula data for second condition (without size field)
4811 3
        $formula2 = substr($recordData, $offset, $sz2);
4812 3
        $formula2 = pack('v', $sz2) . $formula2; // prepend the length
4813
4814
        try {
4815 3
            $formula2 = $this->getFormulaFromStructure($formula2);
4816
        } catch (PhpSpreadsheetException) {
4817
            return;
4818
        }
4819 3
        $offset += $sz2;
4820
4821
        // offset: var; size: var; cell range address list with
4822 3
        $cellRangeAddressList = $this->readBIFF8CellRangeAddressList(substr($recordData, $offset));
4823 3
        $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
4824
4825 3
        foreach ($cellRangeAddresses as $cellRange) {
4826 3
            $stRange = $this->phpSheet->shrinkRangeToFit($cellRange);
4827 3
            foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) {
4828 3
                $objValidation = $this->phpSheet->getCell($coordinate)->getDataValidation();
4829 3
                $objValidation->setType($type);
4830 3
                $objValidation->setErrorStyle($errorStyle);
4831 3
                $objValidation->setAllowBlank((bool) $allowBlank);
4832 3
                $objValidation->setShowInputMessage((bool) $showInputMessage);
4833 3
                $objValidation->setShowErrorMessage((bool) $showErrorMessage);
4834 3
                $objValidation->setShowDropDown(!$suppressDropDown);
4835 3
                $objValidation->setOperator($operator);
4836 3
                $objValidation->setErrorTitle($errorTitle);
4837 3
                $objValidation->setError($error);
4838 3
                $objValidation->setPromptTitle($promptTitle);
4839 3
                $objValidation->setPrompt($prompt);
4840 3
                $objValidation->setFormula1($formula1);
4841 3
                $objValidation->setFormula2($formula2);
4842
            }
4843
        }
4844
    }
4845
4846
    /**
4847
     * Read SHEETLAYOUT record. Stores sheet tab color information.
4848
     */
4849 5
    private function readSheetLayout(): void
4850
    {
4851 5
        $length = self::getUInt2d($this->data, $this->pos + 2);
4852 5
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4853
4854
        // move stream pointer to next record
4855 5
        $this->pos += 4 + $length;
4856
4857
        // local pointer in record data
4858 5
        $offset = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $offset is dead and can be removed.
Loading history...
4859
4860 5
        if (!$this->readDataOnly) {
4861
            // offset: 0; size: 2; repeated record identifier 0x0862
4862
4863
            // offset: 2; size: 10; not used
4864
4865
            // offset: 12; size: 4; size of record data
4866
            // Excel 2003 uses size of 0x14 (documented), Excel 2007 uses size of 0x28 (not documented?)
4867 5
            $sz = self::getInt4d($recordData, 12);
4868
4869
            switch ($sz) {
4870 5
                case 0x14:
4871
                    // offset: 16; size: 2; color index for sheet tab
4872 1
                    $colorIndex = self::getUInt2d($recordData, 16);
4873 1
                    $color = Xls\Color::map($colorIndex, $this->palette, $this->version);
4874 1
                    $this->phpSheet->getTabColor()->setRGB($color['rgb']);
4875
4876 1
                    break;
4877 4
                case 0x28:
4878
                    // TODO: Investigate structure for .xls SHEETLAYOUT record as saved by MS Office Excel 2007
4879 4
                    return;
4880
            }
4881
        }
4882
    }
4883
4884
    /**
4885
     * Read SHEETPROTECTION record (FEATHEADR).
4886
     */
4887 85
    private function readSheetProtection(): void
4888
    {
4889 85
        $length = self::getUInt2d($this->data, $this->pos + 2);
4890 85
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4891
4892
        // move stream pointer to next record
4893 85
        $this->pos += 4 + $length;
4894
4895 85
        if ($this->readDataOnly) {
4896 1
            return;
4897
        }
4898
4899
        // offset: 0; size: 2; repeated record header
4900
4901
        // offset: 2; size: 2; FRT cell reference flag (=0 currently)
4902
4903
        // offset: 4; size: 8; Currently not used and set to 0
4904
4905
        // offset: 12; size: 2; Shared feature type index (2=Enhanced Protetion, 4=SmartTag)
4906 84
        $isf = self::getUInt2d($recordData, 12);
4907 84
        if ($isf != 2) {
4908
            return;
4909
        }
4910
4911
        // offset: 14; size: 1; =1 since this is a feat header
4912
4913
        // offset: 15; size: 4; size of rgbHdrSData
4914
4915
        // rgbHdrSData, assume "Enhanced Protection"
4916
        // offset: 19; size: 2; option flags
4917 84
        $options = self::getUInt2d($recordData, 19);
4918
4919
        // bit: 0; mask 0x0001; 1 = user may edit objects, 0 = users must not edit objects
4920
        // Note - do not negate $bool
4921 84
        $bool = (0x0001 & $options) >> 0;
4922 84
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
4923
4924
        // bit: 1; mask 0x0002; edit scenarios
4925
        // Note - do not negate $bool
4926 84
        $bool = (0x0002 & $options) >> 1;
4927 84
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
4928
4929
        // bit: 2; mask 0x0004; format cells
4930 84
        $bool = (0x0004 & $options) >> 2;
4931 84
        $this->phpSheet->getProtection()->setFormatCells(!$bool);
4932
4933
        // bit: 3; mask 0x0008; format columns
4934 84
        $bool = (0x0008 & $options) >> 3;
4935 84
        $this->phpSheet->getProtection()->setFormatColumns(!$bool);
4936
4937
        // bit: 4; mask 0x0010; format rows
4938 84
        $bool = (0x0010 & $options) >> 4;
4939 84
        $this->phpSheet->getProtection()->setFormatRows(!$bool);
4940
4941
        // bit: 5; mask 0x0020; insert columns
4942 84
        $bool = (0x0020 & $options) >> 5;
4943 84
        $this->phpSheet->getProtection()->setInsertColumns(!$bool);
4944
4945
        // bit: 6; mask 0x0040; insert rows
4946 84
        $bool = (0x0040 & $options) >> 6;
4947 84
        $this->phpSheet->getProtection()->setInsertRows(!$bool);
4948
4949
        // bit: 7; mask 0x0080; insert hyperlinks
4950 84
        $bool = (0x0080 & $options) >> 7;
4951 84
        $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool);
4952
4953
        // bit: 8; mask 0x0100; delete columns
4954 84
        $bool = (0x0100 & $options) >> 8;
4955 84
        $this->phpSheet->getProtection()->setDeleteColumns(!$bool);
4956
4957
        // bit: 9; mask 0x0200; delete rows
4958 84
        $bool = (0x0200 & $options) >> 9;
4959 84
        $this->phpSheet->getProtection()->setDeleteRows(!$bool);
4960
4961
        // bit: 10; mask 0x0400; select locked cells
4962
        // Note that this is opposite of most of above.
4963 84
        $bool = (0x0400 & $options) >> 10;
4964 84
        $this->phpSheet->getProtection()->setSelectLockedCells((bool) $bool);
4965
4966
        // bit: 11; mask 0x0800; sort cell range
4967 84
        $bool = (0x0800 & $options) >> 11;
4968 84
        $this->phpSheet->getProtection()->setSort(!$bool);
4969
4970
        // bit: 12; mask 0x1000; auto filter
4971 84
        $bool = (0x1000 & $options) >> 12;
4972 84
        $this->phpSheet->getProtection()->setAutoFilter(!$bool);
4973
4974
        // bit: 13; mask 0x2000; pivot tables
4975 84
        $bool = (0x2000 & $options) >> 13;
4976 84
        $this->phpSheet->getProtection()->setPivotTables(!$bool);
4977
4978
        // bit: 14; mask 0x4000; select unlocked cells
4979
        // Note that this is opposite of most of above.
4980 84
        $bool = (0x4000 & $options) >> 14;
4981 84
        $this->phpSheet->getProtection()->setSelectUnlockedCells((bool) $bool);
4982
4983
        // offset: 21; size: 2; not used
4984
    }
4985
4986
    /**
4987
     * Read RANGEPROTECTION record
4988
     * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification,
4989
     * where it is referred to as FEAT record.
4990
     */
4991 1
    private function readRangeProtection(): void
4992
    {
4993 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
4994 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4995
4996
        // move stream pointer to next record
4997 1
        $this->pos += 4 + $length;
4998
4999
        // local pointer in record data
5000 1
        $offset = 0;
5001
5002 1
        if (!$this->readDataOnly) {
5003 1
            $offset += 12;
5004
5005
            // offset: 12; size: 2; shared feature type, 2 = enhanced protection, 4 = smart tag
5006 1
            $isf = self::getUInt2d($recordData, 12);
5007 1
            if ($isf != 2) {
5008
                // we only read FEAT records of type 2
5009
                return;
5010
            }
5011 1
            $offset += 2;
5012
5013 1
            $offset += 5;
5014
5015
            // offset: 19; size: 2; count of ref ranges this feature is on
5016 1
            $cref = self::getUInt2d($recordData, 19);
5017 1
            $offset += 2;
5018
5019 1
            $offset += 6;
5020
5021
            // offset: 27; size: 8 * $cref; list of cell ranges (like in hyperlink record)
5022 1
            $cellRanges = [];
5023 1
            for ($i = 0; $i < $cref; ++$i) {
5024
                try {
5025 1
                    $cellRange = $this->readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8));
5026
                } catch (PhpSpreadsheetException) {
5027
                    return;
5028
                }
5029 1
                $cellRanges[] = $cellRange;
5030 1
                $offset += 8;
5031
            }
5032
5033
            // offset: var; size: var; variable length of feature specific data
5034 1
            $rgbFeat = substr($recordData, $offset);
0 ignored issues
show
Unused Code introduced by
The assignment to $rgbFeat is dead and can be removed.
Loading history...
5035 1
            $offset += 4;
5036
5037
            // offset: var; size: 4; the encrypted password (only 16-bit although field is 32-bit)
5038 1
            $wPassword = self::getInt4d($recordData, $offset);
5039 1
            $offset += 4;
5040
5041
            // Apply range protection to sheet
5042 1
            if ($cellRanges) {
5043 1
                $this->phpSheet->protectCells(implode(' ', $cellRanges), strtoupper(dechex($wPassword)), true);
5044
            }
5045
        }
5046
    }
5047
5048
    /**
5049
     * Read a free CONTINUE record. Free CONTINUE record may be a camouflaged MSODRAWING record
5050
     * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented.
5051
     * In this case, we must treat the CONTINUE record as a MSODRAWING record.
5052
     */
5053 1
    private function readContinue(): void
5054
    {
5055 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
5056 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5057
5058
        // check if we are reading drawing data
5059
        // this is in case a free CONTINUE record occurs in other circumstances we are unaware of
5060 1
        if ($this->drawingData == '') {
5061
            // move stream pointer to next record
5062 1
            $this->pos += 4 + $length;
5063
5064 1
            return;
5065
        }
5066
5067
        // check if record data is at least 4 bytes long, otherwise there is no chance this is MSODRAWING data
5068
        if ($length < 4) {
5069
            // move stream pointer to next record
5070
            $this->pos += 4 + $length;
5071
5072
            return;
5073
        }
5074
5075
        // dirty check to see if CONTINUE record could be a camouflaged MSODRAWING record
5076
        // look inside CONTINUE record to see if it looks like a part of an Escher stream
5077
        // we know that Escher stream may be split at least at
5078
        //        0xF003 MsofbtSpgrContainer
5079
        //        0xF004 MsofbtSpContainer
5080
        //        0xF00D MsofbtClientTextbox
5081
        $validSplitPoints = [0xF003, 0xF004, 0xF00D]; // add identifiers if we find more
5082
5083
        $splitPoint = self::getUInt2d($recordData, 2);
5084
        if (in_array($splitPoint, $validSplitPoints)) {
5085
            // get spliced record data (and move pointer to next record)
5086
            $splicedRecordData = $this->getSplicedRecordData();
5087
            $this->drawingData .= $splicedRecordData['recordData'];
5088
5089
            return;
5090
        }
5091
5092
        // move stream pointer to next record
5093
        $this->pos += 4 + $length;
5094
    }
5095
5096
    /**
5097
     * Reads a record from current position in data stream and continues reading data as long as CONTINUE
5098
     * records are found. Splices the record data pieces and returns the combined string as if record data
5099
     * is in one piece.
5100
     * Moves to next current position in data stream to start of next record different from a CONtINUE record.
5101
     */
5102 91
    private function getSplicedRecordData(): array
5103
    {
5104 91
        $data = '';
5105 91
        $spliceOffsets = [];
5106
5107 91
        $i = 0;
5108 91
        $spliceOffsets[0] = 0;
5109
5110
        do {
5111 91
            ++$i;
5112
5113
            // offset: 0; size: 2; identifier
5114 91
            $identifier = self::getUInt2d($this->data, $this->pos);
0 ignored issues
show
Unused Code introduced by
The assignment to $identifier is dead and can be removed.
Loading history...
5115
            // offset: 2; size: 2; length
5116 91
            $length = self::getUInt2d($this->data, $this->pos + 2);
5117 91
            $data .= $this->readRecordData($this->data, $this->pos + 4, $length);
5118
5119 91
            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
5120
5121 91
            $this->pos += 4 + $length;
5122 91
            $nextIdentifier = self::getUInt2d($this->data, $this->pos);
5123 91
        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
5124
5125 91
        return [
5126 91
            'recordData' => $data,
5127 91
            'spliceOffsets' => $spliceOffsets,
5128 91
        ];
5129
    }
5130
5131
    /**
5132
     * Convert formula structure into human readable Excel formula like 'A3+A5*5'.
5133
     *
5134
     * @param string $formulaStructure The complete binary data for the formula
5135
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5136
     *
5137
     * @return string Human readable formula
5138
     */
5139 47
    private function getFormulaFromStructure($formulaStructure, $baseCell = 'A1'): string
5140
    {
5141
        // offset: 0; size: 2; size of the following formula data
5142 47
        $sz = self::getUInt2d($formulaStructure, 0);
5143
5144
        // offset: 2; size: sz
5145 47
        $formulaData = substr($formulaStructure, 2, $sz);
5146
5147
        // offset: 2 + sz; size: variable (optional)
5148 47
        if (strlen($formulaStructure) > 2 + $sz) {
5149
            $additionalData = substr($formulaStructure, 2 + $sz);
5150
        } else {
5151 47
            $additionalData = '';
5152
        }
5153
5154 47
        return $this->getFormulaFromData($formulaData, $additionalData, $baseCell);
5155
    }
5156
5157
    /**
5158
     * Take formula data and additional data for formula and return human readable formula.
5159
     *
5160
     * @param string $formulaData The binary data for the formula itself
5161
     * @param string $additionalData Additional binary data going with the formula
5162
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5163
     *
5164
     * @return string Human readable formula
5165
     */
5166 47
    private function getFormulaFromData(string $formulaData, string $additionalData = '', string $baseCell = 'A1'): string
5167
    {
5168
        // start parsing the formula data
5169 47
        $tokens = [];
5170
5171 47
        while (strlen($formulaData) > 0 && $token = $this->getNextToken($formulaData, $baseCell)) {
5172 47
            $tokens[] = $token;
5173 47
            $formulaData = substr($formulaData, $token['size']);
5174
        }
5175
5176 47
        $formulaString = $this->createFormulaFromTokens($tokens, $additionalData);
5177
5178 47
        return $formulaString;
5179
    }
5180
5181
    /**
5182
     * Take array of tokens together with additional data for formula and return human readable formula.
5183
     *
5184
     * @param string $additionalData Additional binary data going with the formula
5185
     *
5186
     * @return string Human readable formula
5187
     */
5188 47
    private function createFormulaFromTokens(array $tokens, string $additionalData): string
5189
    {
5190
        // empty formula?
5191 47
        if (empty($tokens)) {
5192 3
            return '';
5193
        }
5194
5195 47
        $formulaStrings = [];
5196 47
        foreach ($tokens as $token) {
5197
            // initialize spaces
5198 47
            $space0 = $space0 ?? ''; // spaces before next token, not tParen
5199 47
            $space1 = $space1 ?? ''; // carriage returns before next token, not tParen
5200 47
            $space2 = $space2 ?? ''; // spaces before opening parenthesis
5201 47
            $space3 = $space3 ?? ''; // carriage returns before opening parenthesis
5202 47
            $space4 = $space4 ?? ''; // spaces before closing parenthesis
5203 47
            $space5 = $space5 ?? ''; // carriage returns before closing parenthesis
5204
5205 47
            switch ($token['name']) {
5206 47
                case 'tAdd': // addition
5207 47
                case 'tConcat': // addition
5208 47
                case 'tDiv': // division
5209 47
                case 'tEQ': // equality
5210 47
                case 'tGE': // greater than or equal
5211 47
                case 'tGT': // greater than
5212 47
                case 'tIsect': // intersection
5213 47
                case 'tLE': // less than or equal
5214 47
                case 'tList': // less than or equal
5215 47
                case 'tLT': // less than
5216 47
                case 'tMul': // multiplication
5217 47
                case 'tNE': // multiplication
5218 47
                case 'tPower': // power
5219 47
                case 'tRange': // range
5220 47
                case 'tSub': // subtraction
5221 28
                    $op2 = array_pop($formulaStrings);
5222 28
                    $op1 = array_pop($formulaStrings);
5223 28
                    $formulaStrings[] = "$op1$space1$space0{$token['data']}$op2";
5224 28
                    unset($space0, $space1);
5225
5226 28
                    break;
5227 47
                case 'tUplus': // unary plus
5228 47
                case 'tUminus': // unary minus
5229 3
                    $op = array_pop($formulaStrings);
5230 3
                    $formulaStrings[] = "$space1$space0{$token['data']}$op";
5231 3
                    unset($space0, $space1);
5232
5233 3
                    break;
5234 47
                case 'tPercent': // percent sign
5235 1
                    $op = array_pop($formulaStrings);
5236 1
                    $formulaStrings[] = "$op$space1$space0{$token['data']}";
5237 1
                    unset($space0, $space1);
5238
5239 1
                    break;
5240 47
                case 'tAttrVolatile': // indicates volatile function
5241 47
                case 'tAttrIf':
5242 47
                case 'tAttrSkip':
5243 47
                case 'tAttrChoose':
5244
                    // token is only important for Excel formula evaluator
5245
                    // do nothing
5246 3
                    break;
5247 47
                case 'tAttrSpace': // space / carriage return
5248
                    // space will be used when next token arrives, do not alter formulaString stack
5249
                    switch ($token['data']['spacetype']) {
5250
                        case 'type0':
5251
                            $space0 = str_repeat(' ', $token['data']['spacecount']);
5252
5253
                            break;
5254
                        case 'type1':
5255
                            $space1 = str_repeat("\n", $token['data']['spacecount']);
5256
5257
                            break;
5258
                        case 'type2':
5259
                            $space2 = str_repeat(' ', $token['data']['spacecount']);
5260
5261
                            break;
5262
                        case 'type3':
5263
                            $space3 = str_repeat("\n", $token['data']['spacecount']);
5264
5265
                            break;
5266
                        case 'type4':
5267
                            $space4 = str_repeat(' ', $token['data']['spacecount']);
5268
5269
                            break;
5270
                        case 'type5':
5271
                            $space5 = str_repeat("\n", $token['data']['spacecount']);
5272
5273
                            break;
5274
                    }
5275
5276
                    break;
5277 47
                case 'tAttrSum': // SUM function with one parameter
5278 12
                    $op = array_pop($formulaStrings);
5279 12
                    $formulaStrings[] = "{$space1}{$space0}SUM($op)";
5280 12
                    unset($space0, $space1);
5281
5282 12
                    break;
5283 47
                case 'tFunc': // function with fixed number of arguments
5284 47
                case 'tFuncV': // function with variable number of arguments
5285 31
                    if ($token['data']['function'] != '') {
5286
                        // normal function
5287 31
                        $ops = []; // array of operators
5288 31
                        for ($i = 0; $i < $token['data']['args']; ++$i) {
5289 23
                            $ops[] = array_pop($formulaStrings);
5290
                        }
5291 31
                        $ops = array_reverse($ops);
5292 31
                        $formulaStrings[] = "$space1$space0{$token['data']['function']}(" . implode(',', $ops) . ')';
5293 31
                        unset($space0, $space1);
5294
                    } else {
5295
                        // add-in function
5296
                        $ops = []; // array of operators
5297
                        for ($i = 0; $i < $token['data']['args'] - 1; ++$i) {
5298
                            $ops[] = array_pop($formulaStrings);
5299
                        }
5300
                        $ops = array_reverse($ops);
5301
                        $function = array_pop($formulaStrings);
5302
                        $formulaStrings[] = "$space1$space0$function(" . implode(',', $ops) . ')';
5303
                        unset($space0, $space1);
5304
                    }
5305
5306 31
                    break;
5307 47
                case 'tParen': // parenthesis
5308 1
                    $expression = array_pop($formulaStrings);
5309 1
                    $formulaStrings[] = "$space3$space2($expression$space5$space4)";
5310 1
                    unset($space2, $space3, $space4, $space5);
5311
5312 1
                    break;
5313 47
                case 'tArray': // array constant
5314
                    $constantArray = self::readBIFF8ConstantArray($additionalData);
5315
                    $formulaStrings[] = $space1 . $space0 . $constantArray['value'];
5316
                    $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data
5317
                    unset($space0, $space1);
5318
5319
                    break;
5320 47
                case 'tMemArea':
5321
                    // bite off chunk of additional data
5322
                    $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($additionalData);
5323
                    $additionalData = substr($additionalData, $cellRangeAddressList['size']);
5324
                    $formulaStrings[] = "$space1$space0{$token['data']}";
5325
                    unset($space0, $space1);
5326
5327
                    break;
5328 47
                case 'tArea': // cell range address
5329 45
                case 'tBool': // boolean
5330 44
                case 'tErr': // error code
5331 43
                case 'tInt': // integer
5332 34
                case 'tMemErr':
5333 34
                case 'tMemFunc':
5334 34
                case 'tMissArg':
5335 34
                case 'tName':
5336 34
                case 'tNameX':
5337 34
                case 'tNum': // number
5338 34
                case 'tRef': // single cell reference
5339 29
                case 'tRef3d': // 3d cell reference
5340 27
                case 'tArea3d': // 3d cell range reference
5341 19
                case 'tRefN':
5342 19
                case 'tAreaN':
5343 19
                case 'tStr': // string
5344 47
                    $formulaStrings[] = "$space1$space0{$token['data']}";
5345 47
                    unset($space0, $space1);
5346
5347 47
                    break;
5348
            }
5349
        }
5350 47
        $formulaString = $formulaStrings[0];
5351
5352 47
        return $formulaString;
5353
    }
5354
5355
    /**
5356
     * Fetch next token from binary formula data.
5357
     *
5358
     * @param string $formulaData Formula data
5359
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5360
     */
5361 47
    private function getNextToken(string $formulaData, string $baseCell = 'A1'): array
5362
    {
5363
        // offset: 0; size: 1; token id
5364 47
        $id = ord($formulaData[0]); // token id
5365 47
        $name = false; // initialize token name
5366
5367
        switch ($id) {
5368 47
            case 0x03:
5369 10
                $name = 'tAdd';
5370 10
                $size = 1;
5371 10
                $data = '+';
5372
5373 10
                break;
5374 47
            case 0x04:
5375 9
                $name = 'tSub';
5376 9
                $size = 1;
5377 9
                $data = '-';
5378
5379 9
                break;
5380 47
            case 0x05:
5381 4
                $name = 'tMul';
5382 4
                $size = 1;
5383 4
                $data = '*';
5384
5385 4
                break;
5386 47
            case 0x06:
5387 12
                $name = 'tDiv';
5388 12
                $size = 1;
5389 12
                $data = '/';
5390
5391 12
                break;
5392 47
            case 0x07:
5393 1
                $name = 'tPower';
5394 1
                $size = 1;
5395 1
                $data = '^';
5396
5397 1
                break;
5398 47
            case 0x08:
5399 4
                $name = 'tConcat';
5400 4
                $size = 1;
5401 4
                $data = '&';
5402
5403 4
                break;
5404 47
            case 0x09:
5405 1
                $name = 'tLT';
5406 1
                $size = 1;
5407 1
                $data = '<';
5408
5409 1
                break;
5410 47
            case 0x0A:
5411 1
                $name = 'tLE';
5412 1
                $size = 1;
5413 1
                $data = '<=';
5414
5415 1
                break;
5416 47
            case 0x0B:
5417 3
                $name = 'tEQ';
5418 3
                $size = 1;
5419 3
                $data = '=';
5420
5421 3
                break;
5422 47
            case 0x0C:
5423 1
                $name = 'tGE';
5424 1
                $size = 1;
5425 1
                $data = '>=';
5426
5427 1
                break;
5428 47
            case 0x0D:
5429 1
                $name = 'tGT';
5430 1
                $size = 1;
5431 1
                $data = '>';
5432
5433 1
                break;
5434 47
            case 0x0E:
5435 2
                $name = 'tNE';
5436 2
                $size = 1;
5437 2
                $data = '<>';
5438
5439 2
                break;
5440 47
            case 0x0F:
5441
                $name = 'tIsect';
5442
                $size = 1;
5443
                $data = ' ';
5444
5445
                break;
5446 47
            case 0x10:
5447 1
                $name = 'tList';
5448 1
                $size = 1;
5449 1
                $data = ',';
5450
5451 1
                break;
5452 47
            case 0x11:
5453
                $name = 'tRange';
5454
                $size = 1;
5455
                $data = ':';
5456
5457
                break;
5458 47
            case 0x12:
5459 1
                $name = 'tUplus';
5460 1
                $size = 1;
5461 1
                $data = '+';
5462
5463 1
                break;
5464 47
            case 0x13:
5465 3
                $name = 'tUminus';
5466 3
                $size = 1;
5467 3
                $data = '-';
5468
5469 3
                break;
5470 47
            case 0x14:
5471 1
                $name = 'tPercent';
5472 1
                $size = 1;
5473 1
                $data = '%';
5474
5475 1
                break;
5476 47
            case 0x15:    //    parenthesis
5477 1
                $name = 'tParen';
5478 1
                $size = 1;
5479 1
                $data = null;
5480
5481 1
                break;
5482 47
            case 0x16:    //    missing argument
5483
                $name = 'tMissArg';
5484
                $size = 1;
5485
                $data = '';
5486
5487
                break;
5488 47
            case 0x17:    //    string
5489 19
                $name = 'tStr';
5490
                // offset: 1; size: var; Unicode string, 8-bit string length
5491 19
                $string = self::readUnicodeStringShort(substr($formulaData, 1));
5492 19
                $size = 1 + $string['size'];
5493 19
                $data = self::UTF8toExcelDoubleQuoted($string['value']);
5494
5495 19
                break;
5496 47
            case 0x19:    //    Special attribute
5497
                // offset: 1; size: 1; attribute type flags:
5498 14
                switch (ord($formulaData[1])) {
5499 14
                    case 0x01:
5500 3
                        $name = 'tAttrVolatile';
5501 3
                        $size = 4;
5502 3
                        $data = null;
5503
5504 3
                        break;
5505 12
                    case 0x02:
5506 1
                        $name = 'tAttrIf';
5507 1
                        $size = 4;
5508 1
                        $data = null;
5509
5510 1
                        break;
5511 12
                    case 0x04:
5512 1
                        $name = 'tAttrChoose';
5513
                        // offset: 2; size: 2; number of choices in the CHOOSE function ($nc, number of parameters decreased by 1)
5514 1
                        $nc = self::getUInt2d($formulaData, 2);
5515
                        // offset: 4; size: 2 * $nc
5516
                        // offset: 4 + 2 * $nc; size: 2
5517 1
                        $size = 2 * $nc + 6;
5518 1
                        $data = null;
5519
5520 1
                        break;
5521 12
                    case 0x08:
5522 1
                        $name = 'tAttrSkip';
5523 1
                        $size = 4;
5524 1
                        $data = null;
5525
5526 1
                        break;
5527 12
                    case 0x10:
5528 12
                        $name = 'tAttrSum';
5529 12
                        $size = 4;
5530 12
                        $data = null;
5531
5532 12
                        break;
5533
                    case 0x40:
5534
                    case 0x41:
5535
                        $name = 'tAttrSpace';
5536
                        $size = 4;
5537
                        // offset: 2; size: 2; space type and position
5538
                        $spacetype = match (ord($formulaData[2])) {
5539
                            0x00 => 'type0',
5540
                            0x01 => 'type1',
5541
                            0x02 => 'type2',
5542
                            0x03 => 'type3',
5543
                            0x04 => 'type4',
5544
                            0x05 => 'type5',
5545
                            default => throw new Exception('Unrecognized space type in tAttrSpace token'),
5546
                        };
5547
                        // offset: 3; size: 1; number of inserted spaces/carriage returns
5548
                        $spacecount = ord($formulaData[3]);
5549
5550
                        $data = ['spacetype' => $spacetype, 'spacecount' => $spacecount];
5551
5552
                        break;
5553
                    default:
5554
                        throw new Exception('Unrecognized attribute flag in tAttr token');
5555
                }
5556
5557 14
                break;
5558 47
            case 0x1C:    //    error code
5559
                // offset: 1; size: 1; error code
5560 4
                $name = 'tErr';
5561 4
                $size = 2;
5562 4
                $data = Xls\ErrorCode::lookup(ord($formulaData[1]));
5563
5564 4
                break;
5565 46
            case 0x1D:    //    boolean
5566
                // offset: 1; size: 1; 0 = false, 1 = true;
5567 1
                $name = 'tBool';
5568 1
                $size = 2;
5569 1
                $data = ord($formulaData[1]) ? 'TRUE' : 'FALSE';
5570
5571 1
                break;
5572 46
            case 0x1E:    //    integer
5573
                // offset: 1; size: 2; unsigned 16-bit integer
5574 26
                $name = 'tInt';
5575 26
                $size = 3;
5576 26
                $data = self::getUInt2d($formulaData, 1);
5577
5578 26
                break;
5579 44
            case 0x1F:    //    number
5580
                // offset: 1; size: 8;
5581 7
                $name = 'tNum';
5582 7
                $size = 9;
5583 7
                $data = self::extractNumber(substr($formulaData, 1));
5584 7
                $data = str_replace(',', '.', (string) $data); // in case non-English locale
5585
5586 7
                break;
5587 44
            case 0x20:    //    array constant
5588 44
            case 0x40:
5589 44
            case 0x60:
5590
                // offset: 1; size: 7; not used
5591
                $name = 'tArray';
5592
                $size = 8;
5593
                $data = null;
5594
5595
                break;
5596 44
            case 0x21:    //    function with fixed number of arguments
5597 44
            case 0x41:
5598 43
            case 0x61:
5599 17
                $name = 'tFunc';
5600 17
                $size = 3;
5601
                // offset: 1; size: 2; index to built-in sheet function
5602 17
                switch (self::getUInt2d($formulaData, 1)) {
5603 17
                    case 2:
5604 1
                        $function = 'ISNA';
5605 1
                        $args = 1;
5606
5607 1
                        break;
5608 17
                    case 3:
5609 1
                        $function = 'ISERROR';
5610 1
                        $args = 1;
5611
5612 1
                        break;
5613 17
                    case 10:
5614 9
                        $function = 'NA';
5615 9
                        $args = 0;
5616
5617 9
                        break;
5618 9
                    case 15:
5619 2
                        $function = 'SIN';
5620 2
                        $args = 1;
5621
5622 2
                        break;
5623 8
                    case 16:
5624 1
                        $function = 'COS';
5625 1
                        $args = 1;
5626
5627 1
                        break;
5628 8
                    case 17:
5629 1
                        $function = 'TAN';
5630 1
                        $args = 1;
5631
5632 1
                        break;
5633 8
                    case 18:
5634 1
                        $function = 'ATAN';
5635 1
                        $args = 1;
5636
5637 1
                        break;
5638 8
                    case 19:
5639 1
                        $function = 'PI';
5640 1
                        $args = 0;
5641
5642 1
                        break;
5643 8
                    case 20:
5644 1
                        $function = 'SQRT';
5645 1
                        $args = 1;
5646
5647 1
                        break;
5648 8
                    case 21:
5649 1
                        $function = 'EXP';
5650 1
                        $args = 1;
5651
5652 1
                        break;
5653 8
                    case 22:
5654 1
                        $function = 'LN';
5655 1
                        $args = 1;
5656
5657 1
                        break;
5658 8
                    case 23:
5659 1
                        $function = 'LOG10';
5660 1
                        $args = 1;
5661
5662 1
                        break;
5663 8
                    case 24:
5664 1
                        $function = 'ABS';
5665 1
                        $args = 1;
5666
5667 1
                        break;
5668 8
                    case 25:
5669 1
                        $function = 'INT';
5670 1
                        $args = 1;
5671
5672 1
                        break;
5673 8
                    case 26:
5674 1
                        $function = 'SIGN';
5675 1
                        $args = 1;
5676
5677 1
                        break;
5678 8
                    case 27:
5679 1
                        $function = 'ROUND';
5680 1
                        $args = 2;
5681
5682 1
                        break;
5683 8
                    case 30:
5684 2
                        $function = 'REPT';
5685 2
                        $args = 2;
5686
5687 2
                        break;
5688 8
                    case 31:
5689 1
                        $function = 'MID';
5690 1
                        $args = 3;
5691
5692 1
                        break;
5693 8
                    case 32:
5694 1
                        $function = 'LEN';
5695 1
                        $args = 1;
5696
5697 1
                        break;
5698 8
                    case 33:
5699 1
                        $function = 'VALUE';
5700 1
                        $args = 1;
5701
5702 1
                        break;
5703 8
                    case 34:
5704 3
                        $function = 'TRUE';
5705 3
                        $args = 0;
5706
5707 3
                        break;
5708 8
                    case 35:
5709 3
                        $function = 'FALSE';
5710 3
                        $args = 0;
5711
5712 3
                        break;
5713 7
                    case 38:
5714 1
                        $function = 'NOT';
5715 1
                        $args = 1;
5716
5717 1
                        break;
5718 7
                    case 39:
5719 1
                        $function = 'MOD';
5720 1
                        $args = 2;
5721
5722 1
                        break;
5723 7
                    case 40:
5724 1
                        $function = 'DCOUNT';
5725 1
                        $args = 3;
5726
5727 1
                        break;
5728 7
                    case 41:
5729 1
                        $function = 'DSUM';
5730 1
                        $args = 3;
5731
5732 1
                        break;
5733 7
                    case 42:
5734 1
                        $function = 'DAVERAGE';
5735 1
                        $args = 3;
5736
5737 1
                        break;
5738 7
                    case 43:
5739 1
                        $function = 'DMIN';
5740 1
                        $args = 3;
5741
5742 1
                        break;
5743 7
                    case 44:
5744 1
                        $function = 'DMAX';
5745 1
                        $args = 3;
5746
5747 1
                        break;
5748 7
                    case 45:
5749 1
                        $function = 'DSTDEV';
5750 1
                        $args = 3;
5751
5752 1
                        break;
5753 7
                    case 48:
5754 1
                        $function = 'TEXT';
5755 1
                        $args = 2;
5756
5757 1
                        break;
5758 7
                    case 61:
5759 1
                        $function = 'MIRR';
5760 1
                        $args = 3;
5761
5762 1
                        break;
5763 7
                    case 63:
5764 1
                        $function = 'RAND';
5765 1
                        $args = 0;
5766
5767 1
                        break;
5768 7
                    case 65:
5769 1
                        $function = 'DATE';
5770 1
                        $args = 3;
5771
5772 1
                        break;
5773 7
                    case 66:
5774 1
                        $function = 'TIME';
5775 1
                        $args = 3;
5776
5777 1
                        break;
5778 7
                    case 67:
5779 1
                        $function = 'DAY';
5780 1
                        $args = 1;
5781
5782 1
                        break;
5783 7
                    case 68:
5784 1
                        $function = 'MONTH';
5785 1
                        $args = 1;
5786
5787 1
                        break;
5788 7
                    case 69:
5789 1
                        $function = 'YEAR';
5790 1
                        $args = 1;
5791
5792 1
                        break;
5793 7
                    case 71:
5794 1
                        $function = 'HOUR';
5795 1
                        $args = 1;
5796
5797 1
                        break;
5798 7
                    case 72:
5799 1
                        $function = 'MINUTE';
5800 1
                        $args = 1;
5801
5802 1
                        break;
5803 7
                    case 73:
5804 1
                        $function = 'SECOND';
5805 1
                        $args = 1;
5806
5807 1
                        break;
5808 7
                    case 74:
5809 1
                        $function = 'NOW';
5810 1
                        $args = 0;
5811
5812 1
                        break;
5813 7
                    case 75:
5814 1
                        $function = 'AREAS';
5815 1
                        $args = 1;
5816
5817 1
                        break;
5818 7
                    case 76:
5819 1
                        $function = 'ROWS';
5820 1
                        $args = 1;
5821
5822 1
                        break;
5823 7
                    case 77:
5824 1
                        $function = 'COLUMNS';
5825 1
                        $args = 1;
5826
5827 1
                        break;
5828 7
                    case 83:
5829 1
                        $function = 'TRANSPOSE';
5830 1
                        $args = 1;
5831
5832 1
                        break;
5833 7
                    case 86:
5834 1
                        $function = 'TYPE';
5835 1
                        $args = 1;
5836
5837 1
                        break;
5838 7
                    case 97:
5839 1
                        $function = 'ATAN2';
5840 1
                        $args = 2;
5841
5842 1
                        break;
5843 7
                    case 98:
5844 1
                        $function = 'ASIN';
5845 1
                        $args = 1;
5846
5847 1
                        break;
5848 7
                    case 99:
5849 1
                        $function = 'ACOS';
5850 1
                        $args = 1;
5851
5852 1
                        break;
5853 7
                    case 105:
5854 1
                        $function = 'ISREF';
5855 1
                        $args = 1;
5856
5857 1
                        break;
5858 7
                    case 111:
5859 2
                        $function = 'CHAR';
5860 2
                        $args = 1;
5861
5862 2
                        break;
5863 6
                    case 112:
5864 1
                        $function = 'LOWER';
5865 1
                        $args = 1;
5866
5867 1
                        break;
5868 6
                    case 113:
5869 1
                        $function = 'UPPER';
5870 1
                        $args = 1;
5871
5872 1
                        break;
5873 6
                    case 114:
5874 1
                        $function = 'PROPER';
5875 1
                        $args = 1;
5876
5877 1
                        break;
5878 6
                    case 117:
5879 1
                        $function = 'EXACT';
5880 1
                        $args = 2;
5881
5882 1
                        break;
5883 6
                    case 118:
5884 1
                        $function = 'TRIM';
5885 1
                        $args = 1;
5886
5887 1
                        break;
5888 6
                    case 119:
5889 1
                        $function = 'REPLACE';
5890 1
                        $args = 4;
5891
5892 1
                        break;
5893 6
                    case 121:
5894 1
                        $function = 'CODE';
5895 1
                        $args = 1;
5896
5897 1
                        break;
5898 6
                    case 126:
5899 1
                        $function = 'ISERR';
5900 1
                        $args = 1;
5901
5902 1
                        break;
5903 6
                    case 127:
5904 1
                        $function = 'ISTEXT';
5905 1
                        $args = 1;
5906
5907 1
                        break;
5908 6
                    case 128:
5909 1
                        $function = 'ISNUMBER';
5910 1
                        $args = 1;
5911
5912 1
                        break;
5913 6
                    case 129:
5914 1
                        $function = 'ISBLANK';
5915 1
                        $args = 1;
5916
5917 1
                        break;
5918 6
                    case 130:
5919 1
                        $function = 'T';
5920 1
                        $args = 1;
5921
5922 1
                        break;
5923 6
                    case 131:
5924 1
                        $function = 'N';
5925 1
                        $args = 1;
5926
5927 1
                        break;
5928 6
                    case 140:
5929 1
                        $function = 'DATEVALUE';
5930 1
                        $args = 1;
5931
5932 1
                        break;
5933 6
                    case 141:
5934 1
                        $function = 'TIMEVALUE';
5935 1
                        $args = 1;
5936
5937 1
                        break;
5938 6
                    case 142:
5939 1
                        $function = 'SLN';
5940 1
                        $args = 3;
5941
5942 1
                        break;
5943 6
                    case 143:
5944 1
                        $function = 'SYD';
5945 1
                        $args = 4;
5946
5947 1
                        break;
5948 6
                    case 162:
5949 1
                        $function = 'CLEAN';
5950 1
                        $args = 1;
5951
5952 1
                        break;
5953 6
                    case 163:
5954 1
                        $function = 'MDETERM';
5955 1
                        $args = 1;
5956
5957 1
                        break;
5958 6
                    case 164:
5959 1
                        $function = 'MINVERSE';
5960 1
                        $args = 1;
5961
5962 1
                        break;
5963 6
                    case 165:
5964 1
                        $function = 'MMULT';
5965 1
                        $args = 2;
5966
5967 1
                        break;
5968 6
                    case 184:
5969 1
                        $function = 'FACT';
5970 1
                        $args = 1;
5971
5972 1
                        break;
5973 6
                    case 189:
5974 1
                        $function = 'DPRODUCT';
5975 1
                        $args = 3;
5976
5977 1
                        break;
5978 6
                    case 190:
5979 1
                        $function = 'ISNONTEXT';
5980 1
                        $args = 1;
5981
5982 1
                        break;
5983 6
                    case 195:
5984 1
                        $function = 'DSTDEVP';
5985 1
                        $args = 3;
5986
5987 1
                        break;
5988 6
                    case 196:
5989 1
                        $function = 'DVARP';
5990 1
                        $args = 3;
5991
5992 1
                        break;
5993 6
                    case 198:
5994 1
                        $function = 'ISLOGICAL';
5995 1
                        $args = 1;
5996
5997 1
                        break;
5998 6
                    case 199:
5999 1
                        $function = 'DCOUNTA';
6000 1
                        $args = 3;
6001
6002 1
                        break;
6003 6
                    case 207:
6004 1
                        $function = 'REPLACEB';
6005 1
                        $args = 4;
6006
6007 1
                        break;
6008 6
                    case 210:
6009 1
                        $function = 'MIDB';
6010 1
                        $args = 3;
6011
6012 1
                        break;
6013 6
                    case 211:
6014 1
                        $function = 'LENB';
6015 1
                        $args = 1;
6016
6017 1
                        break;
6018 6
                    case 212:
6019 1
                        $function = 'ROUNDUP';
6020 1
                        $args = 2;
6021
6022 1
                        break;
6023 6
                    case 213:
6024 1
                        $function = 'ROUNDDOWN';
6025 1
                        $args = 2;
6026
6027 1
                        break;
6028 6
                    case 214:
6029 1
                        $function = 'ASC';
6030 1
                        $args = 1;
6031
6032 1
                        break;
6033 6
                    case 215:
6034 1
                        $function = 'DBCS';
6035 1
                        $args = 1;
6036
6037 1
                        break;
6038 6
                    case 221:
6039 1
                        $function = 'TODAY';
6040 1
                        $args = 0;
6041
6042 1
                        break;
6043 6
                    case 229:
6044 1
                        $function = 'SINH';
6045 1
                        $args = 1;
6046
6047 1
                        break;
6048 6
                    case 230:
6049 1
                        $function = 'COSH';
6050 1
                        $args = 1;
6051
6052 1
                        break;
6053 6
                    case 231:
6054 1
                        $function = 'TANH';
6055 1
                        $args = 1;
6056
6057 1
                        break;
6058 6
                    case 232:
6059 1
                        $function = 'ASINH';
6060 1
                        $args = 1;
6061
6062 1
                        break;
6063 6
                    case 233:
6064 1
                        $function = 'ACOSH';
6065 1
                        $args = 1;
6066
6067 1
                        break;
6068 6
                    case 234:
6069 1
                        $function = 'ATANH';
6070 1
                        $args = 1;
6071
6072 1
                        break;
6073 6
                    case 235:
6074 1
                        $function = 'DGET';
6075 1
                        $args = 3;
6076
6077 1
                        break;
6078 6
                    case 244:
6079 2
                        $function = 'INFO';
6080 2
                        $args = 1;
6081
6082 2
                        break;
6083 5
                    case 252:
6084 1
                        $function = 'FREQUENCY';
6085 1
                        $args = 2;
6086
6087 1
                        break;
6088 4
                    case 261:
6089 1
                        $function = 'ERROR.TYPE';
6090 1
                        $args = 1;
6091
6092 1
                        break;
6093 4
                    case 271:
6094 1
                        $function = 'GAMMALN';
6095 1
                        $args = 1;
6096
6097 1
                        break;
6098 4
                    case 273:
6099 1
                        $function = 'BINOMDIST';
6100 1
                        $args = 4;
6101
6102 1
                        break;
6103 4
                    case 274:
6104 1
                        $function = 'CHIDIST';
6105 1
                        $args = 2;
6106
6107 1
                        break;
6108 4
                    case 275:
6109 1
                        $function = 'CHIINV';
6110 1
                        $args = 2;
6111
6112 1
                        break;
6113 4
                    case 276:
6114 1
                        $function = 'COMBIN';
6115 1
                        $args = 2;
6116
6117 1
                        break;
6118 4
                    case 277:
6119 1
                        $function = 'CONFIDENCE';
6120 1
                        $args = 3;
6121
6122 1
                        break;
6123 4
                    case 278:
6124 1
                        $function = 'CRITBINOM';
6125 1
                        $args = 3;
6126
6127 1
                        break;
6128 4
                    case 279:
6129 1
                        $function = 'EVEN';
6130 1
                        $args = 1;
6131
6132 1
                        break;
6133 4
                    case 280:
6134 1
                        $function = 'EXPONDIST';
6135 1
                        $args = 3;
6136
6137 1
                        break;
6138 4
                    case 281:
6139 1
                        $function = 'FDIST';
6140 1
                        $args = 3;
6141
6142 1
                        break;
6143 4
                    case 282:
6144 1
                        $function = 'FINV';
6145 1
                        $args = 3;
6146
6147 1
                        break;
6148 4
                    case 283:
6149 1
                        $function = 'FISHER';
6150 1
                        $args = 1;
6151
6152 1
                        break;
6153 4
                    case 284:
6154 1
                        $function = 'FISHERINV';
6155 1
                        $args = 1;
6156
6157 1
                        break;
6158 4
                    case 285:
6159 1
                        $function = 'FLOOR';
6160 1
                        $args = 2;
6161
6162 1
                        break;
6163 4
                    case 286:
6164 1
                        $function = 'GAMMADIST';
6165 1
                        $args = 4;
6166
6167 1
                        break;
6168 4
                    case 287:
6169 1
                        $function = 'GAMMAINV';
6170 1
                        $args = 3;
6171
6172 1
                        break;
6173 4
                    case 288:
6174 1
                        $function = 'CEILING';
6175 1
                        $args = 2;
6176
6177 1
                        break;
6178 4
                    case 289:
6179 1
                        $function = 'HYPGEOMDIST';
6180 1
                        $args = 4;
6181
6182 1
                        break;
6183 4
                    case 290:
6184 1
                        $function = 'LOGNORMDIST';
6185 1
                        $args = 3;
6186
6187 1
                        break;
6188 4
                    case 291:
6189 1
                        $function = 'LOGINV';
6190 1
                        $args = 3;
6191
6192 1
                        break;
6193 4
                    case 292:
6194 1
                        $function = 'NEGBINOMDIST';
6195 1
                        $args = 3;
6196
6197 1
                        break;
6198 4
                    case 293:
6199 1
                        $function = 'NORMDIST';
6200 1
                        $args = 4;
6201
6202 1
                        break;
6203 4
                    case 294:
6204 1
                        $function = 'NORMSDIST';
6205 1
                        $args = 1;
6206
6207 1
                        break;
6208 4
                    case 295:
6209 1
                        $function = 'NORMINV';
6210 1
                        $args = 3;
6211
6212 1
                        break;
6213 4
                    case 296:
6214 1
                        $function = 'NORMSINV';
6215 1
                        $args = 1;
6216
6217 1
                        break;
6218 4
                    case 297:
6219 1
                        $function = 'STANDARDIZE';
6220 1
                        $args = 3;
6221
6222 1
                        break;
6223 4
                    case 298:
6224 1
                        $function = 'ODD';
6225 1
                        $args = 1;
6226
6227 1
                        break;
6228 4
                    case 299:
6229 1
                        $function = 'PERMUT';
6230 1
                        $args = 2;
6231
6232 1
                        break;
6233 4
                    case 300:
6234 1
                        $function = 'POISSON';
6235 1
                        $args = 3;
6236
6237 1
                        break;
6238 4
                    case 301:
6239 1
                        $function = 'TDIST';
6240 1
                        $args = 3;
6241
6242 1
                        break;
6243 4
                    case 302:
6244 1
                        $function = 'WEIBULL';
6245 1
                        $args = 4;
6246
6247 1
                        break;
6248 3
                    case 303:
6249 1
                        $function = 'SUMXMY2';
6250 1
                        $args = 2;
6251
6252 1
                        break;
6253 3
                    case 304:
6254 1
                        $function = 'SUMX2MY2';
6255 1
                        $args = 2;
6256
6257 1
                        break;
6258 3
                    case 305:
6259 1
                        $function = 'SUMX2PY2';
6260 1
                        $args = 2;
6261
6262 1
                        break;
6263 3
                    case 306:
6264 1
                        $function = 'CHITEST';
6265 1
                        $args = 2;
6266
6267 1
                        break;
6268 3
                    case 307:
6269 1
                        $function = 'CORREL';
6270 1
                        $args = 2;
6271
6272 1
                        break;
6273 3
                    case 308:
6274 1
                        $function = 'COVAR';
6275 1
                        $args = 2;
6276
6277 1
                        break;
6278 3
                    case 309:
6279 1
                        $function = 'FORECAST';
6280 1
                        $args = 3;
6281
6282 1
                        break;
6283 3
                    case 310:
6284 1
                        $function = 'FTEST';
6285 1
                        $args = 2;
6286
6287 1
                        break;
6288 3
                    case 311:
6289 1
                        $function = 'INTERCEPT';
6290 1
                        $args = 2;
6291
6292 1
                        break;
6293 3
                    case 312:
6294 1
                        $function = 'PEARSON';
6295 1
                        $args = 2;
6296
6297 1
                        break;
6298 3
                    case 313:
6299 1
                        $function = 'RSQ';
6300 1
                        $args = 2;
6301
6302 1
                        break;
6303 3
                    case 314:
6304 1
                        $function = 'STEYX';
6305 1
                        $args = 2;
6306
6307 1
                        break;
6308 3
                    case 315:
6309 1
                        $function = 'SLOPE';
6310 1
                        $args = 2;
6311
6312 1
                        break;
6313 3
                    case 316:
6314 1
                        $function = 'TTEST';
6315 1
                        $args = 4;
6316
6317 1
                        break;
6318 3
                    case 325:
6319 1
                        $function = 'LARGE';
6320 1
                        $args = 2;
6321
6322 1
                        break;
6323 3
                    case 326:
6324 1
                        $function = 'SMALL';
6325 1
                        $args = 2;
6326
6327 1
                        break;
6328 3
                    case 327:
6329 1
                        $function = 'QUARTILE';
6330 1
                        $args = 2;
6331
6332 1
                        break;
6333 3
                    case 328:
6334 1
                        $function = 'PERCENTILE';
6335 1
                        $args = 2;
6336
6337 1
                        break;
6338 3
                    case 331:
6339 1
                        $function = 'TRIMMEAN';
6340 1
                        $args = 2;
6341
6342 1
                        break;
6343 3
                    case 332:
6344 1
                        $function = 'TINV';
6345 1
                        $args = 2;
6346
6347 1
                        break;
6348 3
                    case 337:
6349 1
                        $function = 'POWER';
6350 1
                        $args = 2;
6351
6352 1
                        break;
6353 3
                    case 342:
6354 1
                        $function = 'RADIANS';
6355 1
                        $args = 1;
6356
6357 1
                        break;
6358 3
                    case 343:
6359 1
                        $function = 'DEGREES';
6360 1
                        $args = 1;
6361
6362 1
                        break;
6363 3
                    case 346:
6364 1
                        $function = 'COUNTIF';
6365 1
                        $args = 2;
6366
6367 1
                        break;
6368 3
                    case 347:
6369 1
                        $function = 'COUNTBLANK';
6370 1
                        $args = 1;
6371
6372 1
                        break;
6373 3
                    case 350:
6374 1
                        $function = 'ISPMT';
6375 1
                        $args = 4;
6376
6377 1
                        break;
6378 3
                    case 351:
6379 1
                        $function = 'DATEDIF';
6380 1
                        $args = 3;
6381
6382 1
                        break;
6383 3
                    case 352:
6384 1
                        $function = 'DATESTRING';
6385 1
                        $args = 1;
6386
6387 1
                        break;
6388 3
                    case 353:
6389 1
                        $function = 'NUMBERSTRING';
6390 1
                        $args = 2;
6391
6392 1
                        break;
6393 3
                    case 360:
6394 1
                        $function = 'PHONETIC';
6395 1
                        $args = 1;
6396
6397 1
                        break;
6398 2
                    case 368:
6399 1
                        $function = 'BAHTTEXT';
6400 1
                        $args = 1;
6401
6402 1
                        break;
6403
                    default:
6404 1
                        throw new Exception('Unrecognized function in formula');
6405
                }
6406 17
                $data = ['function' => $function, 'args' => $args];
6407
6408 17
                break;
6409 43
            case 0x22:    //    function with variable number of arguments
6410 43
            case 0x42:
6411 41
            case 0x62:
6412 19
                $name = 'tFuncV';
6413 19
                $size = 4;
6414
                // offset: 1; size: 1; number of arguments
6415 19
                $args = ord($formulaData[1]);
6416
                // offset: 2: size: 2; index to built-in sheet function
6417 19
                $index = self::getUInt2d($formulaData, 2);
6418 19
                $function = match ($index) {
6419 19
                    0 => 'COUNT',
6420 19
                    1 => 'IF',
6421 19
                    4 => 'SUM',
6422 19
                    5 => 'AVERAGE',
6423 19
                    6 => 'MIN',
6424 19
                    7 => 'MAX',
6425 19
                    8 => 'ROW',
6426 19
                    9 => 'COLUMN',
6427 19
                    11 => 'NPV',
6428 19
                    12 => 'STDEV',
6429 19
                    13 => 'DOLLAR',
6430 19
                    14 => 'FIXED',
6431 19
                    28 => 'LOOKUP',
6432 19
                    29 => 'INDEX',
6433 19
                    36 => 'AND',
6434 19
                    37 => 'OR',
6435 19
                    46 => 'VAR',
6436 19
                    49 => 'LINEST',
6437 19
                    50 => 'TREND',
6438 19
                    51 => 'LOGEST',
6439 19
                    52 => 'GROWTH',
6440 19
                    56 => 'PV',
6441 19
                    57 => 'FV',
6442 19
                    58 => 'NPER',
6443 19
                    59 => 'PMT',
6444 19
                    60 => 'RATE',
6445 19
                    62 => 'IRR',
6446 19
                    64 => 'MATCH',
6447 19
                    70 => 'WEEKDAY',
6448 19
                    78 => 'OFFSET',
6449 19
                    82 => 'SEARCH',
6450 19
                    100 => 'CHOOSE',
6451 19
                    101 => 'HLOOKUP',
6452 19
                    102 => 'VLOOKUP',
6453 19
                    109 => 'LOG',
6454 19
                    115 => 'LEFT',
6455 19
                    116 => 'RIGHT',
6456 19
                    120 => 'SUBSTITUTE',
6457 19
                    124 => 'FIND',
6458 19
                    125 => 'CELL',
6459 19
                    144 => 'DDB',
6460 19
                    148 => 'INDIRECT',
6461 19
                    167 => 'IPMT',
6462 19
                    168 => 'PPMT',
6463 19
                    169 => 'COUNTA',
6464 19
                    183 => 'PRODUCT',
6465 19
                    193 => 'STDEVP',
6466 19
                    194 => 'VARP',
6467 19
                    197 => 'TRUNC',
6468 19
                    204 => 'USDOLLAR',
6469 19
                    205 => 'FINDB',
6470 19
                    206 => 'SEARCHB',
6471 19
                    208 => 'LEFTB',
6472 19
                    209 => 'RIGHTB',
6473 19
                    216 => 'RANK',
6474 19
                    219 => 'ADDRESS',
6475 19
                    220 => 'DAYS360',
6476 19
                    222 => 'VDB',
6477 19
                    227 => 'MEDIAN',
6478 19
                    228 => 'SUMPRODUCT',
6479 19
                    247 => 'DB',
6480 19
                    255 => '',
6481 19
                    269 => 'AVEDEV',
6482 19
                    270 => 'BETADIST',
6483 19
                    272 => 'BETAINV',
6484 19
                    317 => 'PROB',
6485 19
                    318 => 'DEVSQ',
6486 19
                    319 => 'GEOMEAN',
6487 19
                    320 => 'HARMEAN',
6488 19
                    321 => 'SUMSQ',
6489 19
                    322 => 'KURT',
6490 19
                    323 => 'SKEW',
6491 19
                    324 => 'ZTEST',
6492 19
                    329 => 'PERCENTRANK',
6493 19
                    330 => 'MODE',
6494 19
                    336 => 'CONCATENATE',
6495 19
                    344 => 'SUBTOTAL',
6496 19
                    345 => 'SUMIF',
6497 19
                    354 => 'ROMAN',
6498 19
                    358 => 'GETPIVOTDATA',
6499 19
                    359 => 'HYPERLINK',
6500 19
                    361 => 'AVERAGEA',
6501 19
                    362 => 'MAXA',
6502 19
                    363 => 'MINA',
6503 19
                    364 => 'STDEVPA',
6504 19
                    365 => 'VARPA',
6505 19
                    366 => 'STDEVA',
6506 19
                    367 => 'VARA',
6507 19
                    default => throw new Exception('Unrecognized function in formula'),
6508 19
                };
6509 19
                $data = ['function' => $function, 'args' => $args];
6510
6511 19
                break;
6512 41
            case 0x23:    //    index to defined name
6513 41
            case 0x43:
6514 41
            case 0x63:
6515 1
                $name = 'tName';
6516 1
                $size = 5;
6517
                // offset: 1; size: 2; one-based index to definedname record
6518 1
                $definedNameIndex = self::getUInt2d($formulaData, 1) - 1;
6519
                // offset: 2; size: 2; not used
6520 1
                $data = $this->definedname[$definedNameIndex]['name'] ?? '';
6521
6522 1
                break;
6523 40
            case 0x24:    //    single cell reference e.g. A5
6524 40
            case 0x44:
6525 36
            case 0x64:
6526 18
                $name = 'tRef';
6527 18
                $size = 5;
6528 18
                $data = $this->readBIFF8CellAddress(substr($formulaData, 1, 4));
6529
6530 18
                break;
6531 34
            case 0x25:    //    cell range reference to cells in the same sheet (2d)
6532 14
            case 0x45:
6533 14
            case 0x65:
6534 26
                $name = 'tArea';
6535 26
                $size = 9;
6536 26
                $data = $this->readBIFF8CellRangeAddress(substr($formulaData, 1, 8));
6537
6538 26
                break;
6539 13
            case 0x26:    //    Constant reference sub-expression
6540 13
            case 0x46:
6541 13
            case 0x66:
6542
                $name = 'tMemArea';
6543
                // offset: 1; size: 4; not used
6544
                // offset: 5; size: 2; size of the following subexpression
6545
                $subSize = self::getUInt2d($formulaData, 5);
6546
                $size = 7 + $subSize;
6547
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
6548
6549
                break;
6550 13
            case 0x27:    //    Deleted constant reference sub-expression
6551 13
            case 0x47:
6552 13
            case 0x67:
6553
                $name = 'tMemErr';
6554
                // offset: 1; size: 4; not used
6555
                // offset: 5; size: 2; size of the following subexpression
6556
                $subSize = self::getUInt2d($formulaData, 5);
6557
                $size = 7 + $subSize;
6558
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
6559
6560
                break;
6561 13
            case 0x29:    //    Variable reference sub-expression
6562 13
            case 0x49:
6563 13
            case 0x69:
6564
                $name = 'tMemFunc';
6565
                // offset: 1; size: 2; size of the following sub-expression
6566
                $subSize = self::getUInt2d($formulaData, 1);
6567
                $size = 3 + $subSize;
6568
                $data = $this->getFormulaFromData(substr($formulaData, 3, $subSize));
6569
6570
                break;
6571 13
            case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places
6572 13
            case 0x4C:
6573 13
            case 0x6C:
6574 2
                $name = 'tRefN';
6575 2
                $size = 5;
6576 2
                $data = $this->readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell);
6577
6578 2
                break;
6579 11
            case 0x2D:    //    Relative 2d range reference
6580 11
            case 0x4D:
6581 11
            case 0x6D:
6582
                $name = 'tAreaN';
6583
                $size = 9;
6584
                $data = $this->readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell);
6585
6586
                break;
6587 11
            case 0x39:    //    External name
6588 11
            case 0x59:
6589 11
            case 0x79:
6590
                $name = 'tNameX';
6591
                $size = 7;
6592
                // offset: 1; size: 2; index to REF entry in EXTERNSHEET record
6593
                // offset: 3; size: 2; one-based index to DEFINEDNAME or EXTERNNAME record
6594
                $index = self::getUInt2d($formulaData, 3);
6595
                // assume index is to EXTERNNAME record
6596
                $data = $this->externalNames[$index - 1]['name'] ?? '';
6597
6598
                // offset: 5; size: 2; not used
6599
                break;
6600 11
            case 0x3A:    //    3d reference to cell
6601 9
            case 0x5A:
6602 9
            case 0x7A:
6603 2
                $name = 'tRef3d';
6604 2
                $size = 7;
6605
6606
                try {
6607
                    // offset: 1; size: 2; index to REF entry
6608 2
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
6609
                    // offset: 3; size: 4; cell address
6610 2
                    $cellAddress = $this->readBIFF8CellAddress(substr($formulaData, 3, 4));
6611
6612 2
                    $data = "$sheetRange!$cellAddress";
6613
                } catch (PhpSpreadsheetException) {
6614
                    // deleted sheet reference
6615
                    $data = '#REF!';
6616
                }
6617
6618 2
                break;
6619 9
            case 0x3B:    //    3d reference to cell range
6620 1
            case 0x5B:
6621 1
            case 0x7B:
6622 8
                $name = 'tArea3d';
6623 8
                $size = 11;
6624
6625
                try {
6626
                    // offset: 1; size: 2; index to REF entry
6627 8
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
6628
                    // offset: 3; size: 8; cell address
6629 8
                    $cellRangeAddress = $this->readBIFF8CellRangeAddress(substr($formulaData, 3, 8));
6630
6631 8
                    $data = "$sheetRange!$cellRangeAddress";
6632
                } catch (PhpSpreadsheetException) {
6633
                    // deleted sheet reference
6634
                    $data = '#REF!';
6635
                }
6636
6637 8
                break;
6638
                // Unknown cases    // don't know how to deal with
6639
            default:
6640 1
                throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula');
6641
        }
6642
6643 47
        return [
6644 47
            'id' => $id,
6645 47
            'name' => $name,
6646 47
            'size' => $size,
6647 47
            'data' => $data,
6648 47
        ];
6649
    }
6650
6651
    /**
6652
     * Reads a cell address in BIFF8 e.g. 'A2' or '$A$2'
6653
     * section 3.3.4.
6654
     */
6655 21
    private function readBIFF8CellAddress(string $cellAddressStructure): string
6656
    {
6657
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
6658 21
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;
6659
6660
        // offset: 2; size: 2; index to column or column offset + relative flags
6661
        // bit: 7-0; mask 0x00FF; column index
6662 21
        $column = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($cellAddressStructure, 2)) + 1);
6663
6664
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6665 21
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
6666 11
            $column = '$' . $column;
6667
        }
6668
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6669 21
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
6670 11
            $row = '$' . $row;
6671
        }
6672
6673 21
        return $column . $row;
6674
    }
6675
6676
    /**
6677
     * Reads a cell address in BIFF8 for shared formulas. Uses positive and negative values for row and column
6678
     * to indicate offsets from a base cell
6679
     * section 3.3.4.
6680
     *
6681
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
6682
     */
6683 2
    private function readBIFF8CellAddressB(string $cellAddressStructure, $baseCell = 'A1'): string
6684
    {
6685 2
        [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell);
6686 2
        $baseCol = Coordinate::columnIndexFromString($baseCol) - 1;
6687 2
        $baseRow = (int) $baseRow;
6688
6689
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
6690 2
        $rowIndex = self::getUInt2d($cellAddressStructure, 0);
6691 2
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;
6692
6693
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6694 2
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
6695
            // offset: 2; size: 2; index to column or column offset + relative flags
6696
            // bit: 7-0; mask 0x00FF; column index
6697 2
            $colIndex = 0x00FF & self::getUInt2d($cellAddressStructure, 2);
6698
6699 2
            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
6700 2
            $column = '$' . $column;
6701
        } else {
6702
            // offset: 2; size: 2; index to column or column offset + relative flags
6703
            // bit: 7-0; mask 0x00FF; column index
6704
            $relativeColIndex = 0x00FF & self::getInt2d($cellAddressStructure, 2);
6705
            $colIndex = $baseCol + $relativeColIndex;
6706
            $colIndex = ($colIndex < 256) ? $colIndex : $colIndex - 256;
6707
            $colIndex = ($colIndex >= 0) ? $colIndex : $colIndex + 256;
6708
            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
6709
        }
6710
6711
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6712 2
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
6713
            $row = '$' . $row;
6714
        } else {
6715 2
            $rowIndex = ($rowIndex <= 32767) ? $rowIndex : $rowIndex - 65536;
6716 2
            $row = $baseRow + $rowIndex;
6717
        }
6718
6719 2
        return $column . $row;
6720
    }
6721
6722
    /**
6723
     * Reads a cell range address in BIFF5 e.g. 'A2:B6' or 'A1'
6724
     * always fixed range
6725
     * section 2.5.14.
6726
     */
6727 89
    private function readBIFF5CellRangeAddressFixed(string $subData): string
6728
    {
6729
        // offset: 0; size: 2; index to first row
6730 89
        $fr = self::getUInt2d($subData, 0) + 1;
6731
6732
        // offset: 2; size: 2; index to last row
6733 89
        $lr = self::getUInt2d($subData, 2) + 1;
6734
6735
        // offset: 4; size: 1; index to first column
6736 89
        $fc = ord($subData[4]);
6737
6738
        // offset: 5; size: 1; index to last column
6739 89
        $lc = ord($subData[5]);
6740
6741
        // check values
6742 89
        if ($fr > $lr || $fc > $lc) {
6743
            throw new Exception('Not a cell range address');
6744
        }
6745
6746
        // column index to letter
6747 89
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
6748 89
        $lc = Coordinate::stringFromColumnIndex($lc + 1);
6749
6750 89
        if ($fr == $lr && $fc == $lc) {
6751 77
            return "$fc$fr";
6752
        }
6753
6754 26
        return "$fc$fr:$lc$lr";
6755
    }
6756
6757
    /**
6758
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or 'A1'
6759
     * always fixed range
6760
     * section 2.5.14.
6761
     */
6762 30
    private function readBIFF8CellRangeAddressFixed(string $subData): string
6763
    {
6764
        // offset: 0; size: 2; index to first row
6765 30
        $fr = self::getUInt2d($subData, 0) + 1;
6766
6767
        // offset: 2; size: 2; index to last row
6768 30
        $lr = self::getUInt2d($subData, 2) + 1;
6769
6770
        // offset: 4; size: 2; index to first column
6771 30
        $fc = self::getUInt2d($subData, 4);
6772
6773
        // offset: 6; size: 2; index to last column
6774 30
        $lc = self::getUInt2d($subData, 6);
6775
6776
        // check values
6777 30
        if ($fr > $lr || $fc > $lc) {
6778
            throw new Exception('Not a cell range address');
6779
        }
6780
6781
        // column index to letter
6782 30
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
6783 30
        $lc = Coordinate::stringFromColumnIndex($lc + 1);
6784
6785 30
        if ($fr == $lr && $fc == $lc) {
6786 9
            return "$fc$fr";
6787
        }
6788
6789 25
        return "$fc$fr:$lc$lr";
6790
    }
6791
6792
    /**
6793
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or '$A$2:$B$6'
6794
     * there are flags indicating whether column/row index is relative
6795
     * section 3.3.4.
6796
     */
6797 30
    private function readBIFF8CellRangeAddress(string $subData): string
6798
    {
6799
        // todo: if cell range is just a single cell, should this funciton
6800
        // not just return e.g. 'A1' and not 'A1:A1' ?
6801
6802
        // offset: 0; size: 2; index to first row (0... 65535) (or offset (-32768... 32767))
6803 30
        $fr = self::getUInt2d($subData, 0) + 1;
6804
6805
        // offset: 2; size: 2; index to last row (0... 65535) (or offset (-32768... 32767))
6806 30
        $lr = self::getUInt2d($subData, 2) + 1;
6807
6808
        // offset: 4; size: 2; index to first column or column offset + relative flags
6809
6810
        // bit: 7-0; mask 0x00FF; column index
6811 30
        $fc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 4)) + 1);
6812
6813
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6814 30
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
6815 14
            $fc = '$' . $fc;
6816
        }
6817
6818
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6819 30
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
6820 14
            $fr = '$' . $fr;
6821
        }
6822
6823
        // offset: 6; size: 2; index to last column or column offset + relative flags
6824
6825
        // bit: 7-0; mask 0x00FF; column index
6826 30
        $lc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 6)) + 1);
6827
6828
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6829 30
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
6830 14
            $lc = '$' . $lc;
6831
        }
6832
6833
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6834 30
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
6835 14
            $lr = '$' . $lr;
6836
        }
6837
6838 30
        return "$fc$fr:$lc$lr";
6839
    }
6840
6841
    /**
6842
     * Reads a cell range address in BIFF8 for shared formulas. Uses positive and negative values for row and column
6843
     * to indicate offsets from a base cell
6844
     * section 3.3.4.
6845
     *
6846
     * @param string $baseCell Base cell
6847
     *
6848
     * @return string Cell range address
6849
     */
6850
    private function readBIFF8CellRangeAddressB(string $subData, string $baseCell = 'A1'): string
6851
    {
6852
        [$baseCol, $baseRow] = Coordinate::indexesFromString($baseCell);
6853
        $baseCol = $baseCol - 1;
6854
6855
        // TODO: if cell range is just a single cell, should this funciton
6856
        // not just return e.g. 'A1' and not 'A1:A1' ?
6857
6858
        // offset: 0; size: 2; first row
6859
        $frIndex = self::getUInt2d($subData, 0); // adjust below
6860
6861
        // offset: 2; size: 2; relative index to first row (0... 65535) should be treated as offset (-32768... 32767)
6862
        $lrIndex = self::getUInt2d($subData, 2); // adjust below
6863
6864
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6865
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
6866
            // absolute column index
6867
            // offset: 4; size: 2; first column with relative/absolute flags
6868
            // bit: 7-0; mask 0x00FF; column index
6869
            $fcIndex = 0x00FF & self::getUInt2d($subData, 4);
6870
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
6871
            $fc = '$' . $fc;
6872
        } else {
6873
            // column offset
6874
            // offset: 4; size: 2; first column with relative/absolute flags
6875
            // bit: 7-0; mask 0x00FF; column index
6876
            $relativeFcIndex = 0x00FF & self::getInt2d($subData, 4);
6877
            $fcIndex = $baseCol + $relativeFcIndex;
6878
            $fcIndex = ($fcIndex < 256) ? $fcIndex : $fcIndex - 256;
6879
            $fcIndex = ($fcIndex >= 0) ? $fcIndex : $fcIndex + 256;
6880
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
6881
        }
6882
6883
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6884
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
6885
            // absolute row index
6886
            $fr = $frIndex + 1;
6887
            $fr = '$' . $fr;
6888
        } else {
6889
            // row offset
6890
            $frIndex = ($frIndex <= 32767) ? $frIndex : $frIndex - 65536;
6891
            $fr = $baseRow + $frIndex;
6892
        }
6893
6894
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
6895
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
6896
            // absolute column index
6897
            // offset: 6; size: 2; last column with relative/absolute flags
6898
            // bit: 7-0; mask 0x00FF; column index
6899
            $lcIndex = 0x00FF & self::getUInt2d($subData, 6);
6900
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
6901
            $lc = '$' . $lc;
6902
        } else {
6903
            // column offset
6904
            // offset: 4; size: 2; first column with relative/absolute flags
6905
            // bit: 7-0; mask 0x00FF; column index
6906
            $relativeLcIndex = 0x00FF & self::getInt2d($subData, 4);
6907
            $lcIndex = $baseCol + $relativeLcIndex;
6908
            $lcIndex = ($lcIndex < 256) ? $lcIndex : $lcIndex - 256;
6909
            $lcIndex = ($lcIndex >= 0) ? $lcIndex : $lcIndex + 256;
6910
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
6911
        }
6912
6913
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
6914
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
6915
            // absolute row index
6916
            $lr = $lrIndex + 1;
6917
            $lr = '$' . $lr;
6918
        } else {
6919
            // row offset
6920
            $lrIndex = ($lrIndex <= 32767) ? $lrIndex : $lrIndex - 65536;
6921
            $lr = $baseRow + $lrIndex;
6922
        }
6923
6924
        return "$fc$fr:$lc$lr";
6925
    }
6926
6927
    /**
6928
     * Read BIFF8 cell range address list
6929
     * section 2.5.15.
6930
     */
6931 28
    private function readBIFF8CellRangeAddressList(string $subData): array
6932
    {
6933 28
        $cellRangeAddresses = [];
6934
6935
        // offset: 0; size: 2; number of the following cell range addresses
6936 28
        $nm = self::getUInt2d($subData, 0);
6937
6938 28
        $offset = 2;
6939
        // offset: 2; size: 8 * $nm; list of $nm (fixed) cell range addresses
6940 28
        for ($i = 0; $i < $nm; ++$i) {
6941 28
            $cellRangeAddresses[] = $this->readBIFF8CellRangeAddressFixed(substr($subData, $offset, 8));
6942 28
            $offset += 8;
6943
        }
6944
6945 28
        return [
6946 28
            'size' => 2 + 8 * $nm,
6947 28
            'cellRangeAddresses' => $cellRangeAddresses,
6948 28
        ];
6949
    }
6950
6951
    /**
6952
     * Read BIFF5 cell range address list
6953
     * section 2.5.15.
6954
     */
6955 89
    private function readBIFF5CellRangeAddressList(string $subData): array
6956
    {
6957 89
        $cellRangeAddresses = [];
6958
6959
        // offset: 0; size: 2; number of the following cell range addresses
6960 89
        $nm = self::getUInt2d($subData, 0);
6961
6962 89
        $offset = 2;
6963
        // offset: 2; size: 6 * $nm; list of $nm (fixed) cell range addresses
6964 89
        for ($i = 0; $i < $nm; ++$i) {
6965 89
            $cellRangeAddresses[] = $this->readBIFF5CellRangeAddressFixed(substr($subData, $offset, 6));
6966 89
            $offset += 6;
6967
        }
6968
6969 89
        return [
6970 89
            'size' => 2 + 6 * $nm,
6971 89
            'cellRangeAddresses' => $cellRangeAddresses,
6972 89
        ];
6973
    }
6974
6975
    /**
6976
     * Get a sheet range like Sheet1:Sheet3 from REF index
6977
     * Note: If there is only one sheet in the range, one gets e.g Sheet1
6978
     * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets,
6979
     * in which case an Exception is thrown.
6980
     */
6981 10
    private function readSheetRangeByRefIndex(int $index): string|false
6982
    {
6983 10
        if (isset($this->ref[$index])) {
6984 10
            $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type'];
6985
6986
            switch ($type) {
6987 10
                case 'internal':
6988
                    // check if we have a deleted 3d reference
6989 10
                    if ($this->ref[$index]['firstSheetIndex'] == 0xFFFF || $this->ref[$index]['lastSheetIndex'] == 0xFFFF) {
6990
                        throw new Exception('Deleted sheet reference');
6991
                    }
6992
6993
                    // we have normal sheet range (collapsed or uncollapsed)
6994 10
                    $firstSheetName = $this->sheets[$this->ref[$index]['firstSheetIndex']]['name'];
6995 10
                    $lastSheetName = $this->sheets[$this->ref[$index]['lastSheetIndex']]['name'];
6996
6997 10
                    if ($firstSheetName == $lastSheetName) {
6998
                        // collapsed sheet range
6999 10
                        $sheetRange = $firstSheetName;
7000
                    } else {
7001
                        $sheetRange = "$firstSheetName:$lastSheetName";
7002
                    }
7003
7004
                    // escape the single-quotes
7005 10
                    $sheetRange = str_replace("'", "''", $sheetRange);
7006
7007
                    // if there are special characters, we need to enclose the range in single-quotes
7008
                    // todo: check if we have identified the whole set of special characters
7009
                    // it seems that the following characters are not accepted for sheet names
7010
                    // and we may assume that they are not present: []*/:\?
7011 10
                    if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) {
7012 3
                        $sheetRange = "'$sheetRange'";
7013
                    }
7014
7015 10
                    return $sheetRange;
7016
                default:
7017
                    // TODO: external sheet support
7018
                    throw new Exception('Xls reader only supports internal sheets in formulas');
7019
            }
7020
        }
7021
7022
        return false;
7023
    }
7024
7025
    /**
7026
     * read BIFF8 constant value array from array data
7027
     * returns e.g. ['value' => '{1,2;3,4}', 'size' => 40]
7028
     * section 2.5.8.
7029
     *
7030
     * @param string $arrayData
7031
     */
7032
    private static function readBIFF8ConstantArray($arrayData): array
7033
    {
7034
        // offset: 0; size: 1; number of columns decreased by 1
7035
        $nc = ord($arrayData[0]);
7036
7037
        // offset: 1; size: 2; number of rows decreased by 1
7038
        $nr = self::getUInt2d($arrayData, 1);
7039
        $size = 3; // initialize
7040
        $arrayData = substr($arrayData, 3);
7041
7042
        // offset: 3; size: var; list of ($nc + 1) * ($nr + 1) constant values
7043
        $matrixChunks = [];
7044
        for ($r = 1; $r <= $nr + 1; ++$r) {
7045
            $items = [];
7046
            for ($c = 1; $c <= $nc + 1; ++$c) {
7047
                $constant = self::readBIFF8Constant($arrayData);
7048
                $items[] = $constant['value'];
7049
                $arrayData = substr($arrayData, $constant['size']);
7050
                $size += $constant['size'];
7051
            }
7052
            $matrixChunks[] = implode(',', $items); // looks like e.g. '1,"hello"'
7053
        }
7054
        $matrix = '{' . implode(';', $matrixChunks) . '}';
7055
7056
        return [
7057
            'value' => $matrix,
7058
            'size' => $size,
7059
        ];
7060
    }
7061
7062
    /**
7063
     * read BIFF8 constant value which may be 'Empty Value', 'Number', 'String Value', 'Boolean Value', 'Error Value'
7064
     * section 2.5.7
7065
     * returns e.g. ['value' => '5', 'size' => 9].
7066
     *
7067
     * @param string $valueData
7068
     */
7069
    private static function readBIFF8Constant($valueData): array
7070
    {
7071
        // offset: 0; size: 1; identifier for type of constant
7072
        $identifier = ord($valueData[0]);
7073
7074
        switch ($identifier) {
7075
            case 0x00: // empty constant (what is this?)
7076
                $value = '';
7077
                $size = 9;
7078
7079
                break;
7080
            case 0x01: // number
7081
                // offset: 1; size: 8; IEEE 754 floating-point value
7082
                $value = self::extractNumber(substr($valueData, 1, 8));
7083
                $size = 9;
7084
7085
                break;
7086
            case 0x02: // string value
7087
                // offset: 1; size: var; Unicode string, 16-bit string length
7088
                $string = self::readUnicodeStringLong(substr($valueData, 1));
7089
                $value = '"' . $string['value'] . '"';
7090
                $size = 1 + $string['size'];
7091
7092
                break;
7093
            case 0x04: // boolean
7094
                // offset: 1; size: 1; 0 = FALSE, 1 = TRUE
7095
                if (ord($valueData[1])) {
7096
                    $value = 'TRUE';
7097
                } else {
7098
                    $value = 'FALSE';
7099
                }
7100
                $size = 9;
7101
7102
                break;
7103
            case 0x10: // error code
7104
                // offset: 1; size: 1; error code
7105
                $value = Xls\ErrorCode::lookup(ord($valueData[1]));
7106
                $size = 9;
7107
7108
                break;
7109
            default:
7110
                throw new PhpSpreadsheetException('Unsupported BIFF8 constant');
7111
        }
7112
7113
        return [
7114
            'value' => $value,
7115
            'size' => $size,
7116
        ];
7117
    }
7118
7119
    /**
7120
     * Extract RGB color
7121
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.4.
7122
     *
7123
     * @param string $rgb Encoded RGB value (4 bytes)
7124
     */
7125 63
    private static function readRGB($rgb): array
7126
    {
7127
        // offset: 0; size 1; Red component
7128 63
        $r = ord($rgb[0]);
7129
7130
        // offset: 1; size: 1; Green component
7131 63
        $g = ord($rgb[1]);
7132
7133
        // offset: 2; size: 1; Blue component
7134 63
        $b = ord($rgb[2]);
7135
7136
        // HEX notation, e.g. 'FF00FC'
7137 63
        $rgb = sprintf('%02X%02X%02X', $r, $g, $b);
7138
7139 63
        return ['rgb' => $rgb];
7140
    }
7141
7142
    /**
7143
     * Read byte string (8-bit string length)
7144
     * OpenOffice documentation: 2.5.2.
7145
     */
7146 6
    private function readByteStringShort(string $subData): array
7147
    {
7148
        // offset: 0; size: 1; length of the string (character count)
7149 6
        $ln = ord($subData[0]);
7150
7151
        // offset: 1: size: var; character array (8-bit characters)
7152 6
        $value = $this->decodeCodepage(substr($subData, 1, $ln));
7153
7154 6
        return [
7155 6
            'value' => $value,
7156 6
            'size' => 1 + $ln, // size in bytes of data structure
7157 6
        ];
7158
    }
7159
7160
    /**
7161
     * Read byte string (16-bit string length)
7162
     * OpenOffice documentation: 2.5.2.
7163
     */
7164 2
    private function readByteStringLong(string $subData): array
7165
    {
7166
        // offset: 0; size: 2; length of the string (character count)
7167 2
        $ln = self::getUInt2d($subData, 0);
7168
7169
        // offset: 2: size: var; character array (8-bit characters)
7170 2
        $value = $this->decodeCodepage(substr($subData, 2));
7171
7172
        //return $string;
7173 2
        return [
7174 2
            'value' => $value,
7175 2
            'size' => 2 + $ln, // size in bytes of data structure
7176 2
        ];
7177
    }
7178
7179
    /**
7180
     * Extracts an Excel Unicode short string (8-bit string length)
7181
     * OpenOffice documentation: 2.5.3
7182
     * function will automatically find out where the Unicode string ends.
7183
     *
7184
     * @param string $subData
7185
     *
7186
     * @return array
7187
     */
7188 97
    private static function readUnicodeStringShort($subData)
7189
    {
7190 97
        $value = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
7191
7192
        // offset: 0: size: 1; length of the string (character count)
7193 97
        $characterCount = ord($subData[0]);
7194
7195 97
        $string = self::readUnicodeString(substr($subData, 1), $characterCount);
7196
7197
        // add 1 for the string length
7198 97
        ++$string['size'];
7199
7200 97
        return $string;
7201
    }
7202
7203
    /**
7204
     * Extracts an Excel Unicode long string (16-bit string length)
7205
     * OpenOffice documentation: 2.5.3
7206
     * this function is under construction, needs to support rich text, and Asian phonetic settings.
7207
     *
7208
     * @param string $subData
7209
     *
7210
     * @return array
7211
     */
7212 91
    private static function readUnicodeStringLong($subData)
7213
    {
7214 91
        $value = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
7215
7216
        // offset: 0: size: 2; length of the string (character count)
7217 91
        $characterCount = self::getUInt2d($subData, 0);
7218
7219 91
        $string = self::readUnicodeString(substr($subData, 2), $characterCount);
7220
7221
        // add 2 for the string length
7222 91
        $string['size'] += 2;
7223
7224 91
        return $string;
7225
    }
7226
7227
    /**
7228
     * Read Unicode string with no string length field, but with known character count
7229
     * this function is under construction, needs to support rich text, and Asian phonetic settings
7230
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.3.
7231
     *
7232
     * @param string $subData
7233
     * @param int $characterCount
7234
     */
7235 97
    private static function readUnicodeString($subData, $characterCount): array
7236
    {
7237 97
        $value = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
7238
7239
        // offset: 0: size: 1; option flags
7240
        // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit)
7241 97
        $isCompressed = !((0x01 & ord($subData[0])) >> 0);
7242
7243
        // bit: 2; mask: 0x04; Asian phonetic settings
7244 97
        $hasAsian = (0x04) & ord($subData[0]) >> 2;
0 ignored issues
show
Unused Code introduced by
The assignment to $hasAsian is dead and can be removed.
Loading history...
7245
7246
        // bit: 3; mask: 0x08; Rich-Text settings
7247 97
        $hasRichText = (0x08) & ord($subData[0]) >> 3;
0 ignored issues
show
Unused Code introduced by
The assignment to $hasRichText is dead and can be removed.
Loading history...
7248
7249
        // offset: 1: size: var; character array
7250
        // this offset assumes richtext and Asian phonetic settings are off which is generally wrong
7251
        // needs to be fixed
7252 97
        $value = self::encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed);
7253
7254 97
        return [
7255 97
            'value' => $value,
7256 97
            'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags
7257 97
        ];
7258
    }
7259
7260
    /**
7261
     * Convert UTF-8 string to string surounded by double quotes. Used for explicit string tokens in formulas.
7262
     * Example:  hello"world  -->  "hello""world".
7263
     *
7264
     * @param string $value UTF-8 encoded string
7265
     */
7266 19
    private static function UTF8toExcelDoubleQuoted($value): string
7267
    {
7268 19
        return '"' . str_replace('"', '""', $value) . '"';
7269
    }
7270
7271
    /**
7272
     * Reads first 8 bytes of a string and return IEEE 754 float.
7273
     *
7274
     * @param string $data Binary string that is at least 8 bytes long
7275
     *
7276
     * @return float
7277
     */
7278 93
    private static function extractNumber($data): int|float
7279
    {
7280 93
        $rknumhigh = self::getInt4d($data, 4);
7281 93
        $rknumlow = self::getInt4d($data, 0);
7282 93
        $sign = ($rknumhigh & (int) 0x80000000) >> 31;
7283 93
        $exp = (($rknumhigh & 0x7ff00000) >> 20) - 1023;
7284 93
        $mantissa = (0x100000 | ($rknumhigh & 0x000fffff));
7285 93
        $mantissalow1 = ($rknumlow & (int) 0x80000000) >> 31;
7286 93
        $mantissalow2 = ($rknumlow & 0x7fffffff);
7287 93
        $value = $mantissa / 2 ** (20 - $exp);
7288
7289 93
        if ($mantissalow1 != 0) {
7290 26
            $value += 1 / 2 ** (21 - $exp);
7291
        }
7292
7293 93
        $value += $mantissalow2 / 2 ** (52 - $exp);
7294 93
        if ($sign) {
7295 19
            $value *= -1;
7296
        }
7297
7298 93
        return $value;
7299
    }
7300
7301
    /**
7302
     * @param int $rknum
7303
     *
7304
     * @return float
7305
     */
7306 33
    private static function getIEEE754($rknum): float|int
7307
    {
7308 33
        if (($rknum & 0x02) != 0) {
7309 7
            $value = $rknum >> 2;
7310
        } else {
7311
            // changes by mmp, info on IEEE754 encoding from
7312
            // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html
7313
            // The RK format calls for using only the most significant 30 bits
7314
            // of the 64 bit floating point value. The other 34 bits are assumed
7315
            // to be 0 so we use the upper 30 bits of $rknum as follows...
7316 28
            $sign = ($rknum & (int) 0x80000000) >> 31;
7317 28
            $exp = ($rknum & 0x7ff00000) >> 20;
7318 28
            $mantissa = (0x100000 | ($rknum & 0x000ffffc));
7319 28
            $value = $mantissa / 2 ** (20 - ($exp - 1023));
7320 28
            if ($sign) {
7321 11
                $value = -1 * $value;
7322
            }
7323
            //end of changes by mmp
7324
        }
7325 33
        if (($rknum & 0x01) != 0) {
7326 14
            $value /= 100;
7327
        }
7328
7329 33
        return $value;
7330
    }
7331
7332
    /**
7333
     * Get UTF-8 string from (compressed or uncompressed) UTF-16 string.
7334
     *
7335
     * @param string $string
7336
     * @param bool $compressed
7337
     */
7338 97
    private static function encodeUTF16($string, $compressed = false): string
7339
    {
7340 97
        if ($compressed) {
7341 55
            $string = self::uncompressByteString($string);
7342
        }
7343
7344 97
        return StringHelper::convertEncoding($string, 'UTF-8', 'UTF-16LE');
7345
    }
7346
7347
    /**
7348
     * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8.
7349
     *
7350
     * @param string $string
7351
     */
7352 55
    private static function uncompressByteString($string): string
7353
    {
7354 55
        $uncompressedString = '';
7355 55
        $strLen = strlen($string);
7356 55
        for ($i = 0; $i < $strLen; ++$i) {
7357 54
            $uncompressedString .= $string[$i] . "\0";
7358
        }
7359
7360 55
        return $uncompressedString;
7361
    }
7362
7363
    /**
7364
     * Convert string to UTF-8. Only used for BIFF5.
7365
     */
7366 6
    private function decodeCodepage(string $string): string
7367
    {
7368 6
        return StringHelper::convertEncoding($string, 'UTF-8', $this->codepage);
7369
    }
7370
7371
    /**
7372
     * Read 16-bit unsigned integer.
7373
     *
7374
     * @param string $data
7375
     * @param int $pos
7376
     */
7377 103
    public static function getUInt2d($data, $pos): int
7378
    {
7379 103
        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8);
7380
    }
7381
7382
    /**
7383
     * Read 16-bit signed integer.
7384
     *
7385
     * @param string $data
7386
     * @param int $pos
7387
     *
7388
     * @return int
7389
     */
7390
    public static function getInt2d($data, $pos)
7391
    {
7392
        return unpack('s', $data[$pos] . $data[$pos + 1])[1]; // @phpstan-ignore-line
7393
    }
7394
7395
    /**
7396
     * Read 32-bit signed integer.
7397
     *
7398
     * @param string $data
7399
     * @param int $pos
7400
     */
7401 103
    public static function getInt4d($data, $pos): int
7402
    {
7403
        // FIX: represent numbers correctly on 64-bit system
7404
        // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
7405
        // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems
7406 103
        $_or_24 = ord($data[$pos + 3]);
7407 103
        if ($_or_24 >= 128) {
7408
            // negative number
7409 35
            $_ord_24 = -abs((256 - $_or_24) << 24);
7410
        } else {
7411 103
            $_ord_24 = ($_or_24 & 127) << 24;
7412
        }
7413
7414 103
        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24;
7415
    }
7416
7417 3
    private function parseRichText(string $is): RichText
7418
    {
7419 3
        $value = new RichText();
7420 3
        $value->createText($is);
7421
7422 3
        return $value;
7423
    }
7424
7425
    /**
7426
     * Phpstan 1.4.4 complains that this property is never read.
7427
     * So, we might be able to get rid of it altogether.
7428
     * For now, however, this function makes it readable,
7429
     * which satisfies Phpstan.
7430
     *
7431
     * @codeCoverageIgnore
7432
     */
7433
    public function getMapCellStyleXfIndex(): array
7434
    {
7435
        return $this->mapCellStyleXfIndex;
7436
    }
7437
7438 16
    private function readCFHeader(): array
7439
    {
7440 16
        $length = self::getUInt2d($this->data, $this->pos + 2);
7441 16
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7442
7443
        // move stream pointer forward to next record
7444 16
        $this->pos += 4 + $length;
7445
7446 16
        if ($this->readDataOnly) {
7447 1
            return [];
7448
        }
7449
7450
        // offset: 0; size: 2; Rule Count
7451
//        $ruleCount = self::getUInt2d($recordData, 0);
7452
7453
        // offset: var; size: var; cell range address list with
7454 15
        $cellRangeAddressList = ($this->version == self::XLS_BIFF8)
7455 15
            ? $this->readBIFF8CellRangeAddressList(substr($recordData, 12))
7456
            : $this->readBIFF5CellRangeAddressList(substr($recordData, 12));
7457 15
        $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
7458
7459 15
        return $cellRangeAddresses;
7460
    }
7461
7462 16
    private function readCFRule(array $cellRangeAddresses): void
7463
    {
7464 16
        $length = self::getUInt2d($this->data, $this->pos + 2);
7465 16
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7466
7467
        // move stream pointer forward to next record
7468 16
        $this->pos += 4 + $length;
7469
7470 16
        if ($this->readDataOnly) {
7471 1
            return;
7472
        }
7473
7474
        // offset: 0; size: 2; Options
7475 15
        $cfRule = self::getUInt2d($recordData, 0);
7476
7477
        // bit: 8-15; mask: 0x00FF; type
7478 15
        $type = (0x00FF & $cfRule) >> 0;
7479 15
        $type = ConditionalFormatting::type($type);
7480
7481
        // bit: 0-7; mask: 0xFF00; type
7482 15
        $operator = (0xFF00 & $cfRule) >> 8;
7483 15
        $operator = ConditionalFormatting::operator($operator);
7484
7485 15
        if ($type === null || $operator === null) {
7486
            return;
7487
        }
7488
7489
        // offset: 2; size: 2; Size1
7490 15
        $size1 = self::getUInt2d($recordData, 2);
7491
7492
        // offset: 4; size: 2; Size2
7493 15
        $size2 = self::getUInt2d($recordData, 4);
7494
7495
        // offset: 6; size: 4; Options
7496 15
        $options = self::getInt4d($recordData, 6);
7497
7498 15
        $style = new Style(false, true); // non-supervisor, conditional
7499 15
        $this->getCFStyleOptions($options, $style);
7500
7501 15
        $hasFontRecord = (bool) ((0x04000000 & $options) >> 26);
7502 15
        $hasAlignmentRecord = (bool) ((0x08000000 & $options) >> 27);
7503 15
        $hasBorderRecord = (bool) ((0x10000000 & $options) >> 28);
7504 15
        $hasFillRecord = (bool) ((0x20000000 & $options) >> 29);
7505 15
        $hasProtectionRecord = (bool) ((0x40000000 & $options) >> 30);
7506
7507 15
        $offset = 12;
7508
7509 15
        if ($hasFontRecord === true) {
7510 15
            $fontStyle = substr($recordData, $offset, 118);
7511 15
            $this->getCFFontStyle($fontStyle, $style);
7512 15
            $offset += 118;
7513
        }
7514
7515 15
        if ($hasAlignmentRecord === true) {
7516
            $alignmentStyle = substr($recordData, $offset, 8);
7517
            $this->getCFAlignmentStyle($alignmentStyle, $style);
7518
            $offset += 8;
7519
        }
7520
7521 15
        if ($hasBorderRecord === true) {
7522
            $borderStyle = substr($recordData, $offset, 8);
7523
            $this->getCFBorderStyle($borderStyle, $style);
7524
            $offset += 8;
7525
        }
7526
7527 15
        if ($hasFillRecord === true) {
7528 2
            $fillStyle = substr($recordData, $offset, 4);
7529 2
            $this->getCFFillStyle($fillStyle, $style);
7530 2
            $offset += 4;
7531
        }
7532
7533 15
        if ($hasProtectionRecord === true) {
7534
            $protectionStyle = substr($recordData, $offset, 4);
7535
            $this->getCFProtectionStyle($protectionStyle, $style);
7536
            $offset += 2;
7537
        }
7538
7539 15
        $formula1 = $formula2 = null;
7540 15
        if ($size1 > 0) {
7541 15
            $formula1 = $this->readCFFormula($recordData, $offset, $size1);
7542 15
            if ($formula1 === null) {
7543
                return;
7544
            }
7545
7546 15
            $offset += $size1;
7547
        }
7548
7549 15
        if ($size2 > 0) {
7550 6
            $formula2 = $this->readCFFormula($recordData, $offset, $size2);
7551 6
            if ($formula2 === null) {
7552
                return;
7553
            }
7554
7555 6
            $offset += $size2;
7556
        }
7557
7558 15
        $this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2, $style);
7559
    }
7560
7561 15
    private function getCFStyleOptions(int $options, Style $style): void
0 ignored issues
show
Unused Code introduced by
The parameter $style is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

7561
    private function getCFStyleOptions(int $options, /** @scrutinizer ignore-unused */ Style $style): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

7561
    private function getCFStyleOptions(/** @scrutinizer ignore-unused */ int $options, Style $style): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
7562
    {
7563 15
    }
7564
7565 15
    private function getCFFontStyle(string $options, Style $style): void
7566
    {
7567 15
        $fontSize = self::getInt4d($options, 64);
7568 15
        if ($fontSize !== -1) {
7569 8
            $style->getFont()->setSize($fontSize / 20); // Convert twips to points
7570
        }
7571
7572 15
        $bold = self::getUInt2d($options, 72) === 700; // 400 = normal, 700 = bold
7573 15
        $style->getFont()->setBold($bold);
7574
7575 15
        $color = self::getInt4d($options, 80);
7576
7577 15
        if ($color !== -1) {
7578 15
            $style->getFont()->getColor()->setRGB(Xls\Color::map($color, $this->palette, $this->version)['rgb']);
7579
        }
7580
    }
7581
7582
    private function getCFAlignmentStyle(string $options, Style $style): void
0 ignored issues
show
Unused Code introduced by
The parameter $style is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

7582
    private function getCFAlignmentStyle(string $options, /** @scrutinizer ignore-unused */ Style $style): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

7582
    private function getCFAlignmentStyle(/** @scrutinizer ignore-unused */ string $options, Style $style): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
7583
    {
7584
    }
7585
7586
    private function getCFBorderStyle(string $options, Style $style): void
0 ignored issues
show
Unused Code introduced by
The parameter $style is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

7586
    private function getCFBorderStyle(string $options, /** @scrutinizer ignore-unused */ Style $style): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

7586
    private function getCFBorderStyle(/** @scrutinizer ignore-unused */ string $options, Style $style): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
7587
    {
7588
    }
7589
7590 2
    private function getCFFillStyle(string $options, Style $style): void
7591
    {
7592 2
        $fillPattern = self::getUInt2d($options, 0);
7593
        // bit: 10-15; mask: 0xFC00; type
7594 2
        $fillPattern = (0xFC00 & $fillPattern) >> 10;
7595 2
        $fillPattern = FillPattern::lookup($fillPattern);
7596 2
        $fillPattern = $fillPattern === Fill::FILL_NONE ? Fill::FILL_SOLID : $fillPattern;
7597
7598 2
        if ($fillPattern !== Fill::FILL_NONE) {
7599 2
            $style->getFill()->setFillType($fillPattern);
7600
7601 2
            $fillColors = self::getUInt2d($options, 2);
7602
7603
            // bit: 0-6; mask: 0x007F; type
7604 2
            $color1 = (0x007F & $fillColors) >> 0;
7605 2
            $style->getFill()->getStartColor()->setRGB(Xls\Color::map($color1, $this->palette, $this->version)['rgb']);
7606
7607
            // bit: 7-13; mask: 0x3F80; type
7608 2
            $color2 = (0x3F80 & $fillColors) >> 7;
7609 2
            $style->getFill()->getEndColor()->setRGB(Xls\Color::map($color2, $this->palette, $this->version)['rgb']);
7610
        }
7611
    }
7612
7613
    private function getCFProtectionStyle(string $options, Style $style): void
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

7613
    private function getCFProtectionStyle(/** @scrutinizer ignore-unused */ string $options, Style $style): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $style is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

7613
    private function getCFProtectionStyle(string $options, /** @scrutinizer ignore-unused */ Style $style): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
7614
    {
7615
    }
7616
7617 15
    private function readCFFormula(string $recordData, int $offset, int $size): float|int|string|null
7618
    {
7619
        try {
7620 15
            $formula = substr($recordData, $offset, $size);
7621 15
            $formula = pack('v', $size) . $formula; // prepend the length
7622
7623 15
            $formula = $this->getFormulaFromStructure($formula);
7624 15
            if (is_numeric($formula)) {
7625 13
                return (str_contains($formula, '.')) ? (float) $formula : (int) $formula;
7626
            }
7627
7628 8
            return $formula;
7629
        } catch (PhpSpreadsheetException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
7630
        }
7631
7632
        return null;
7633
    }
7634
7635
    /**
7636
     * @param null|float|int|string $formula1
7637
     * @param null|float|int|string $formula2
7638
     */
7639 15
    private function setCFRules(array $cellRanges, string $type, string $operator, $formula1, $formula2, Style $style): void
7640
    {
7641 15
        foreach ($cellRanges as $cellRange) {
7642 15
            $conditional = new Conditional();
7643 15
            $conditional->setConditionType($type);
7644 15
            $conditional->setOperatorType($operator);
7645 15
            if ($formula1 !== null) {
7646 15
                $conditional->addCondition($formula1);
7647
            }
7648 15
            if ($formula2 !== null) {
7649 6
                $conditional->addCondition($formula2);
7650
            }
7651 15
            $conditional->setStyle($style);
7652
7653 15
            $conditionalStyles = $this->phpSheet->getStyle($cellRange)->getConditionalStyles();
7654 15
            $conditionalStyles[] = $conditional;
7655
7656 15
            $this->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles);
7657
        }
7658
    }
7659
7660 5
    public function getVersion(): int
7661
    {
7662 5
        return $this->version;
7663
    }
7664
}
7665