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

Xls::readDataValidation()   C

Complexity

Conditions 14
Paths 162

Size

Total Lines 130
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 66
CRAP Score 14.1132

Importance

Changes 0
Metric Value
cc 14
eloc 73
c 0
b 0
f 0
nc 162
nop 0
dl 0
loc 130
ccs 66
cts 72
cp 0.9167
crap 14.1132
rs 5.189

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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