Passed
Push — develop ( 3bea6f...0f8f07 )
by Mark
36:13
created

Xml   F

Complexity

Total Complexity 149

Size/Duplication

Total Lines 861
Duplicated Lines 0 %

Test Coverage

Coverage 65.55%

Importance

Changes 0
Metric Value
eloc 435
dl 0
loc 861
ccs 293
cts 447
cp 0.6555
rs 2
c 0
b 0
f 0
wmc 149

19 Methods

Rating   Name   Duplication   Size   Complexity  
B parseStyleFont() 0 39 9
B parseStyleAlignment() 0 36 7
A convertStringEncoding() 0 7 2
A parseStyleInterior() 0 12 4
F loadIntoExisting() 0 341 75
A listWorksheetNames() 0 20 3
A pixel2WidthUnits() 0 8 1
C parseStyleBorders() 0 27 12
A parseRichText() 0 7 1
A trySimpleXMLLoadString() 0 13 2
A widthUnits2Pixel() 0 7 1
A hex2str() 0 3 1
A parseStyleNumberFormat() 0 16 4
A identifyFixedStyleValue() 0 12 3
A __construct() 0 4 1
A canRead() 0 42 4
B listWorksheetInfo() 0 60 8
A load() 0 8 1
B parseStyles() 0 33 10

How to fix   Complexity   

Complex Class

Complex classes like Xml often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Xml, and based on these observations, apply Extract Interface, too.

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\Document\Properties;
8
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
9
use PhpOffice\PhpSpreadsheet\RichText\RichText;
10
use PhpOffice\PhpSpreadsheet\Settings;
11
use PhpOffice\PhpSpreadsheet\Shared\Date;
12
use PhpOffice\PhpSpreadsheet\Shared\File;
13
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
14
use PhpOffice\PhpSpreadsheet\Spreadsheet;
15
use PhpOffice\PhpSpreadsheet\Style\Alignment;
16
use PhpOffice\PhpSpreadsheet\Style\Border;
17
use PhpOffice\PhpSpreadsheet\Style\Font;
18
use SimpleXMLElement;
19
20
/**
21
 * Reader for SpreadsheetML, the XML schema for Microsoft Office Excel 2003.
22
 */
23
class Xml extends BaseReader
24
{
25
    /**
26
     * Formats.
27
     *
28
     * @var array
29
     */
30
    protected $styles = [];
31
32
    /**
33
     * Character set used in the file.
34
     *
35
     * @var string
36
     */
37
    protected $charSet = 'UTF-8';
38
39
    /**
40
     * @var XmlScanner
41
     */
42
    private $securityScanner;
43
44
    /**
45
     * Create a new Excel2003XML Reader instance.
46
     */
47 7
    public function __construct()
48
    {
49 7
        $this->readFilter = new DefaultReadFilter();
50 7
        $this->securityScanner = new XmlScanner();
51 7
    }
52
53
    /**
54
     * Can the current IReader read the file?
55
     *
56
     * @param string $pFilename
57
     *
58
     * @throws Exception
59
     *
60
     * @return bool
61
     */
62 5
    public function canRead($pFilename)
63
    {
64
        //    Office                    xmlns:o="urn:schemas-microsoft-com:office:office"
65
        //    Excel                    xmlns:x="urn:schemas-microsoft-com:office:excel"
66
        //    XML Spreadsheet            xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
67
        //    Spreadsheet component    xmlns:c="urn:schemas-microsoft-com:office:component:spreadsheet"
68
        //    XML schema                 xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882"
69
        //    XML data type            xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
70
        //    MS-persist recordset    xmlns:rs="urn:schemas-microsoft-com:rowset"
71
        //    Rowset                    xmlns:z="#RowsetSchema"
72
        //
73
74
        $signature = [
75 5
            '<?xml version="1.0"',
76
            '<?mso-application progid="Excel.Sheet"?>',
77
        ];
78
79
        // Open file
80 5
        $this->openFile($pFilename);
81 5
        $fileHandle = $this->fileHandle;
82
83
        // Read sample data (first 2 KB will do)
84 5
        $data = fread($fileHandle, 2048);
1 ignored issue
show
Bug introduced by
It seems like $fileHandle can also be of type false; however, parameter $handle of fread() does only seem to accept resource, 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

84
        $data = fread(/** @scrutinizer ignore-type */ $fileHandle, 2048);
Loading history...
85 5
        fclose($fileHandle);
1 ignored issue
show
Bug introduced by
It seems like $fileHandle can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, 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

85
        fclose(/** @scrutinizer ignore-type */ $fileHandle);
Loading history...
86 5
        $data = str_replace("'", '"', $data); // fix headers with single quote
87
88 5
        $valid = true;
89 5
        foreach ($signature as $match) {
90
            // every part of the signature must be present
91 5
            if (strpos($data, $match) === false) {
92
                $valid = false;
93
94 5
                break;
95
            }
96
        }
97
98
        //    Retrieve charset encoding
99 5
        if (preg_match('/<?xml.*encoding=[\'"](.*?)[\'"].*?>/um', $data, $matches)) {
100 5
            $this->charSet = strtoupper($matches[1]);
101
        }
102
103 5
        return $valid;
104
    }
105
106
    /**
107
     * Check if the file is a valid SimpleXML.
108
     *
109
     * @param string $pFilename
110
     *
111
     * @throws Exception
112
     *
113
     * @return false|\SimpleXMLElement
114
     */
115 4
    public function trySimpleXMLLoadString($pFilename)
116
    {
117
        try {
118 4
            $xml = simplexml_load_string(
119 4
                $this->securityScanner->scan(file_get_contents($pFilename)),
120 4
                'SimpleXMLElement',
121 4
                Settings::getLibXmlLoaderOptions()
122
            );
123 1
        } catch (\Exception $e) {
124 1
            throw new Exception('Cannot load invalid XML file: ' . $pFilename, 0, $e);
125
        }
126
127 3
        return $xml;
128
    }
129
130
    /**
131
     * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
132
     *
133
     * @param string $pFilename
134
     *
135
     * @throws Exception
136
     *
137
     * @return array
138
     */
139
    public function listWorksheetNames($pFilename)
140
    {
141
        File::assertFile($pFilename);
142
        if (!$this->canRead($pFilename)) {
143
            throw new Exception($pFilename . ' is an Invalid Spreadsheet file.');
144
        }
145
146
        $worksheetNames = [];
147
148
        $xml = $this->trySimpleXMLLoadString($pFilename);
149
150
        $namespaces = $xml->getNamespaces(true);
151
152
        $xml_ss = $xml->children($namespaces['ss']);
153
        foreach ($xml_ss->Worksheet as $worksheet) {
154
            $worksheet_ss = $worksheet->attributes($namespaces['ss']);
155
            $worksheetNames[] = self::convertStringEncoding((string) $worksheet_ss['Name'], $this->charSet);
156
        }
157
158
        return $worksheetNames;
159
    }
160
161
    /**
162
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
163
     *
164
     * @param string $pFilename
165
     *
166
     * @throws Exception
167
     *
168
     * @return array
169
     */
170
    public function listWorksheetInfo($pFilename)
171
    {
172
        File::assertFile($pFilename);
173
174
        $worksheetInfo = [];
175
176
        $xml = $this->trySimpleXMLLoadString($pFilename);
177
178
        $namespaces = $xml->getNamespaces(true);
179
180
        $worksheetID = 1;
181
        $xml_ss = $xml->children($namespaces['ss']);
182
        foreach ($xml_ss->Worksheet as $worksheet) {
183
            $worksheet_ss = $worksheet->attributes($namespaces['ss']);
184
185
            $tmpInfo = [];
186
            $tmpInfo['worksheetName'] = '';
187
            $tmpInfo['lastColumnLetter'] = 'A';
188
            $tmpInfo['lastColumnIndex'] = 0;
189
            $tmpInfo['totalRows'] = 0;
190
            $tmpInfo['totalColumns'] = 0;
191
192
            if (isset($worksheet_ss['Name'])) {
193
                $tmpInfo['worksheetName'] = (string) $worksheet_ss['Name'];
194
            } else {
195
                $tmpInfo['worksheetName'] = "Worksheet_{$worksheetID}";
196
            }
197
198
            if (isset($worksheet->Table->Row)) {
199
                $rowIndex = 0;
200
201
                foreach ($worksheet->Table->Row as $rowData) {
202
                    $columnIndex = 0;
203
                    $rowHasData = false;
204
205
                    foreach ($rowData->Cell as $cell) {
206
                        if (isset($cell->Data)) {
207
                            $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);
208
                            $rowHasData = true;
209
                        }
210
211
                        ++$columnIndex;
212
                    }
213
214
                    ++$rowIndex;
215
216
                    if ($rowHasData) {
217
                        $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
218
                    }
219
                }
220
            }
221
222
            $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
223
            $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
224
225
            $worksheetInfo[] = $tmpInfo;
226
            ++$worksheetID;
227
        }
228
229
        return $worksheetInfo;
230
    }
231
232
    /**
233
     * Loads Spreadsheet from file.
234
     *
235
     * @param string $pFilename
236
     *
237
     * @throws Exception
238
     *
239
     * @return Spreadsheet
240
     */
241 3
    public function load($pFilename)
242
    {
243
        // Create new Spreadsheet
244 3
        $spreadsheet = new Spreadsheet();
245 3
        $spreadsheet->removeSheetByIndex(0);
246
247
        // Load into this instance
248 3
        return $this->loadIntoExisting($pFilename, $spreadsheet);
249
    }
250
251 2
    private static function identifyFixedStyleValue($styleList, &$styleAttributeValue)
252
    {
253 2
        $styleAttributeValue = strtolower($styleAttributeValue);
254 2
        foreach ($styleList as $style) {
255 2
            if ($styleAttributeValue == strtolower($style)) {
256 2
                $styleAttributeValue = $style;
257
258 2
                return true;
259
            }
260
        }
261
262
        return false;
263
    }
264
265
    /**
266
     * pixel units to excel width units(units of 1/256th of a character width).
267
     *
268
     * @param float $pxs
269
     *
270
     * @return float
271
     */
272
    protected static function pixel2WidthUnits($pxs)
273
    {
274
        $UNIT_OFFSET_MAP = [0, 36, 73, 109, 146, 182, 219];
275
276
        $widthUnits = 256 * ($pxs / 7);
277
        $widthUnits += $UNIT_OFFSET_MAP[($pxs % 7)];
278
279
        return $widthUnits;
280
    }
281
282
    /**
283
     * excel width units(units of 1/256th of a character width) to pixel units.
284
     *
285
     * @param float $widthUnits
286
     *
287
     * @return float
288
     */
289
    protected static function widthUnits2Pixel($widthUnits)
290
    {
291
        $pixels = ($widthUnits / 256) * 7;
292
        $offsetWidthUnits = $widthUnits % 256;
293
        $pixels += round($offsetWidthUnits / (256 / 7));
294
295
        return $pixels;
296
    }
297
298
    protected static function hex2str($hex)
299
    {
300
        return chr(hexdec($hex[1]));
1 ignored issue
show
Bug introduced by
It seems like hexdec($hex[1]) can also be of type double; however, parameter $ascii of chr() does only seem to accept integer, 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

300
        return chr(/** @scrutinizer ignore-type */ hexdec($hex[1]));
Loading history...
301
    }
302
303
    /**
304
     * Loads from file into Spreadsheet instance.
305
     *
306
     * @param string $pFilename
307
     * @param Spreadsheet $spreadsheet
308
     *
309
     * @throws Exception
310
     *
311
     * @return Spreadsheet
312
     */
313 3
    public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet)
314
    {
315 3
        File::assertFile($pFilename);
316 3
        if (!$this->canRead($pFilename)) {
317
            throw new Exception($pFilename . ' is an Invalid Spreadsheet file.');
318
        }
319
320 3
        $xml = $this->trySimpleXMLLoadString($pFilename);
321
322 3
        $namespaces = $xml->getNamespaces(true);
323
324 3
        $docProps = $spreadsheet->getProperties();
325 3
        if (isset($xml->DocumentProperties[0])) {
326
            foreach ($xml->DocumentProperties[0] as $propertyName => $propertyValue) {
327
                switch ($propertyName) {
328
                    case 'Title':
329
                        $docProps->setTitle(self::convertStringEncoding($propertyValue, $this->charSet));
330
331
                        break;
332
                    case 'Subject':
333
                        $docProps->setSubject(self::convertStringEncoding($propertyValue, $this->charSet));
334
335
                        break;
336
                    case 'Author':
337
                        $docProps->setCreator(self::convertStringEncoding($propertyValue, $this->charSet));
338
339
                        break;
340
                    case 'Created':
341
                        $creationDate = strtotime($propertyValue);
342
                        $docProps->setCreated($creationDate);
343
344
                        break;
345
                    case 'LastAuthor':
346
                        $docProps->setLastModifiedBy(self::convertStringEncoding($propertyValue, $this->charSet));
347
348
                        break;
349
                    case 'LastSaved':
350
                        $lastSaveDate = strtotime($propertyValue);
351
                        $docProps->setModified($lastSaveDate);
352
353
                        break;
354
                    case 'Company':
355
                        $docProps->setCompany(self::convertStringEncoding($propertyValue, $this->charSet));
356
357
                        break;
358
                    case 'Category':
359
                        $docProps->setCategory(self::convertStringEncoding($propertyValue, $this->charSet));
360
361
                        break;
362
                    case 'Manager':
363
                        $docProps->setManager(self::convertStringEncoding($propertyValue, $this->charSet));
364
365
                        break;
366
                    case 'Keywords':
367
                        $docProps->setKeywords(self::convertStringEncoding($propertyValue, $this->charSet));
368
369
                        break;
370
                    case 'Description':
371
                        $docProps->setDescription(self::convertStringEncoding($propertyValue, $this->charSet));
372
373
                        break;
374
                }
375
            }
376
        }
377 3
        if (isset($xml->CustomDocumentProperties)) {
378
            foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) {
379
                $propertyAttributes = $propertyValue->attributes($namespaces['dt']);
380
                $propertyName = preg_replace_callback('/_x([0-9a-z]{4})_/', ['self', 'hex2str'], $propertyName);
381
                $propertyType = Properties::PROPERTY_TYPE_UNKNOWN;
382
                switch ((string) $propertyAttributes) {
383
                    case 'string':
384
                        $propertyType = Properties::PROPERTY_TYPE_STRING;
385
                        $propertyValue = trim($propertyValue);
386
387
                        break;
388
                    case 'boolean':
389
                        $propertyType = Properties::PROPERTY_TYPE_BOOLEAN;
390
                        $propertyValue = (bool) $propertyValue;
391
392
                        break;
393
                    case 'integer':
394
                        $propertyType = Properties::PROPERTY_TYPE_INTEGER;
395
                        $propertyValue = (int) $propertyValue;
396
397
                        break;
398
                    case 'float':
399
                        $propertyType = Properties::PROPERTY_TYPE_FLOAT;
400
                        $propertyValue = (float) $propertyValue;
401
402
                        break;
403
                    case 'dateTime.tz':
404
                        $propertyType = Properties::PROPERTY_TYPE_DATE;
405
                        $propertyValue = strtotime(trim($propertyValue));
406
407
                        break;
408
                }
409
                $docProps->setCustomProperty($propertyName, $propertyValue, $propertyType);
410
            }
411
        }
412
413 3
        $this->parseStyles($xml, $namespaces);
1 ignored issue
show
Bug introduced by
It seems like $xml can also be of type false; however, parameter $xml of PhpOffice\PhpSpreadsheet\Reader\Xml::parseStyles() does only seem to accept SimpleXMLElement, 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

413
        $this->parseStyles(/** @scrutinizer ignore-type */ $xml, $namespaces);
Loading history...
414
415 3
        $worksheetID = 0;
416 3
        $xml_ss = $xml->children($namespaces['ss']);
417
418 3
        foreach ($xml_ss->Worksheet as $worksheet) {
419 3
            $worksheet_ss = $worksheet->attributes($namespaces['ss']);
420
421 3
            if ((isset($this->loadSheetsOnly)) && (isset($worksheet_ss['Name'])) &&
422 3
                (!in_array($worksheet_ss['Name'], $this->loadSheetsOnly))) {
423
                continue;
424
            }
425
426
            // Create new Worksheet
427 3
            $spreadsheet->createSheet();
428 3
            $spreadsheet->setActiveSheetIndex($worksheetID);
429 3
            if (isset($worksheet_ss['Name'])) {
430 3
                $worksheetName = self::convertStringEncoding((string) $worksheet_ss['Name'], $this->charSet);
431
                //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in
432
                //        formula cells... during the load, all formulae should be correct, and we're simply bringing
433
                //        the worksheet name in line with the formula, not the reverse
434 3
                $spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false);
435
            }
436
437 3
            $columnID = 'A';
438 3
            if (isset($worksheet->Table->Column)) {
439 3
                foreach ($worksheet->Table->Column as $columnData) {
440 3
                    $columnData_ss = $columnData->attributes($namespaces['ss']);
441 3
                    if (isset($columnData_ss['Index'])) {
442 3
                        $columnID = Coordinate::stringFromColumnIndex((int) $columnData_ss['Index']);
443
                    }
444 3
                    if (isset($columnData_ss['Width'])) {
445 3
                        $columnWidth = $columnData_ss['Width'];
446 3
                        $spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4);
447
                    }
448 3
                    ++$columnID;
449
                }
450
            }
451
452 3
            $rowID = 1;
453 3
            if (isset($worksheet->Table->Row)) {
454 3
                $additionalMergedCells = 0;
455 3
                foreach ($worksheet->Table->Row as $rowData) {
456 3
                    $rowHasData = false;
457 3
                    $row_ss = $rowData->attributes($namespaces['ss']);
458 3
                    if (isset($row_ss['Index'])) {
459 2
                        $rowID = (int) $row_ss['Index'];
460
                    }
461
462 3
                    $columnID = 'A';
463 3
                    foreach ($rowData->Cell as $cell) {
464 3
                        $cell_ss = $cell->attributes($namespaces['ss']);
465 3
                        if (isset($cell_ss['Index'])) {
466 2
                            $columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']);
467
                        }
468 3
                        $cellRange = $columnID . $rowID;
469
470 3
                        if ($this->getReadFilter() !== null) {
471 3
                            if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $worksheetName does not seem to be defined for all execution paths leading up to this point.
Loading history...
472
                                ++$columnID;
473
474
                                continue;
475
                            }
476
                        }
477
478 3
                        if (isset($cell_ss['HRef'])) {
479 2
                            $spreadsheet->getActiveSheet()->getCell($cellRange)->getHyperlink()->setUrl($cell_ss['HRef']);
480
                        }
481
482 3
                        if ((isset($cell_ss['MergeAcross'])) || (isset($cell_ss['MergeDown']))) {
483 2
                            $columnTo = $columnID;
484 2
                            if (isset($cell_ss['MergeAcross'])) {
485 2
                                $additionalMergedCells += (int) $cell_ss['MergeAcross'];
486 2
                                $columnTo = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($columnID) + $cell_ss['MergeAcross']);
487
                            }
488 2
                            $rowTo = $rowID;
489 2
                            if (isset($cell_ss['MergeDown'])) {
490 2
                                $rowTo = $rowTo + $cell_ss['MergeDown'];
491
                            }
492 2
                            $cellRange .= ':' . $columnTo . $rowTo;
493 2
                            $spreadsheet->getActiveSheet()->mergeCells($cellRange);
494
                        }
495
496 3
                        $cellIsSet = $hasCalculatedValue = false;
497 3
                        $cellDataFormula = '';
498 3
                        if (isset($cell_ss['Formula'])) {
499 2
                            $cellDataFormula = $cell_ss['Formula'];
500 2
                            $hasCalculatedValue = true;
501
                        }
502 3
                        if (isset($cell->Data)) {
503 3
                            $cellValue = $cellData = $cell->Data;
504 3
                            $type = DataType::TYPE_NULL;
505 3
                            $cellData_ss = $cellData->attributes($namespaces['ss']);
506 3
                            if (isset($cellData_ss['Type'])) {
507 3
                                $cellDataType = $cellData_ss['Type'];
508 3
                                switch ($cellDataType) {
509
                                    /*
510
                                    const TYPE_STRING        = 's';
511
                                    const TYPE_FORMULA        = 'f';
512
                                    const TYPE_NUMERIC        = 'n';
513
                                    const TYPE_BOOL            = 'b';
514
                                    const TYPE_NULL            = 'null';
515
                                    const TYPE_INLINE        = 'inlineStr';
516
                                    const TYPE_ERROR        = 'e';
517
                                    */
518 3
                                    case 'String':
519 3
                                        $cellValue = self::convertStringEncoding($cellValue, $this->charSet);
520 3
                                        $type = DataType::TYPE_STRING;
521
522 3
                                        break;
523 3
                                    case 'Number':
524 3
                                        $type = DataType::TYPE_NUMERIC;
525 3
                                        $cellValue = (float) $cellValue;
526 3
                                        if (floor($cellValue) == $cellValue) {
527 3
                                            $cellValue = (int) $cellValue;
528
                                        }
529
530 3
                                        break;
531 2
                                    case 'Boolean':
532 2
                                        $type = DataType::TYPE_BOOL;
533 2
                                        $cellValue = ($cellValue != 0);
534
535 2
                                        break;
536 2
                                    case 'DateTime':
537 2
                                        $type = DataType::TYPE_NUMERIC;
538 2
                                        $cellValue = Date::PHPToExcel(strtotime($cellValue));
539
540 2
                                        break;
541
                                    case 'Error':
542
                                        $type = DataType::TYPE_ERROR;
543
544
                                        break;
545
                                }
546
                            }
547
548 3
                            if ($hasCalculatedValue) {
549 2
                                $type = DataType::TYPE_FORMULA;
550 2
                                $columnNumber = Coordinate::columnIndexFromString($columnID);
551 2
                                if (substr($cellDataFormula, 0, 3) == 'of:') {
552 2
                                    $cellDataFormula = substr($cellDataFormula, 3);
553 2
                                    $temp = explode('"', $cellDataFormula);
554 2
                                    $key = false;
555 2
                                    foreach ($temp as &$value) {
556
                                        //    Only replace in alternate array entries (i.e. non-quoted blocks)
557 2
                                        if ($key = !$key) {
0 ignored issues
show
introduced by
The condition $key is always false.
Loading history...
558 2
                                            $value = str_replace(['[.', '.', ']'], '', $value);
559
                                        }
560
                                    }
561
                                } else {
562
                                    //    Convert R1C1 style references to A1 style references (but only when not quoted)
563
                                    $temp = explode('"', $cellDataFormula);
564
                                    $key = false;
565
                                    foreach ($temp as &$value) {
566
                                        //    Only replace in alternate array entries (i.e. non-quoted blocks)
567
                                        if ($key = !$key) {
0 ignored issues
show
introduced by
The condition $key is always false.
Loading history...
568
                                            preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE);
569
                                            //    Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way
570
                                            //        through the formula from left to right. Reversing means that we work right to left.through
571
                                            //        the formula
572
                                            $cellReferences = array_reverse($cellReferences);
573
                                            //    Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent,
574
                                            //        then modify the formula to use that new reference
575
                                            foreach ($cellReferences as $cellReference) {
576
                                                $rowReference = $cellReference[2][0];
577
                                                //    Empty R reference is the current row
578
                                                if ($rowReference == '') {
579
                                                    $rowReference = $rowID;
580
                                                }
581
                                                //    Bracketed R references are relative to the current row
582
                                                if ($rowReference[0] == '[') {
583
                                                    $rowReference = $rowID + trim($rowReference, '[]');
584
                                                }
585
                                                $columnReference = $cellReference[4][0];
586
                                                //    Empty C reference is the current column
587
                                                if ($columnReference == '') {
588
                                                    $columnReference = $columnNumber;
589
                                                }
590
                                                //    Bracketed C references are relative to the current column
591
                                                if ($columnReference[0] == '[') {
592
                                                    $columnReference = $columnNumber + trim($columnReference, '[]');
593
                                                }
594
                                                $A1CellReference = Coordinate::stringFromColumnIndex($columnReference) . $rowReference;
595
                                                $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0]));
596
                                            }
597
                                        }
598
                                    }
599
                                }
600 2
                                unset($value);
601
                                //    Then rebuild the formula string
602 2
                                $cellDataFormula = implode('"', $temp);
603
                            }
604
605 3
                            $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValueExplicit((($hasCalculatedValue) ? $cellDataFormula : $cellValue), $type);
606 3
                            if ($hasCalculatedValue) {
607 2
                                $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setCalculatedValue($cellValue);
608
                            }
609 3
                            $cellIsSet = $rowHasData = true;
610
                        }
611
612 3
                        if (isset($cell->Comment)) {
613 3
                            $commentAttributes = $cell->Comment->attributes($namespaces['ss']);
614 3
                            $author = 'unknown';
615 3
                            if (isset($commentAttributes->Author)) {
616
                                $author = (string) $commentAttributes->Author;
617
                            }
618 3
                            $node = $cell->Comment->Data->asXML();
619 3
                            $annotation = strip_tags($node);
620 3
                            $spreadsheet->getActiveSheet()->getComment($columnID . $rowID)->setAuthor(self::convertStringEncoding($author, $this->charSet))->setText($this->parseRichText($annotation));
621
                        }
622
623 3
                        if (($cellIsSet) && (isset($cell_ss['StyleID']))) {
624 2
                            $style = (string) $cell_ss['StyleID'];
625 2
                            if ((isset($this->styles[$style])) && (!empty($this->styles[$style]))) {
626 2
                                if (!$spreadsheet->getActiveSheet()->cellExists($columnID . $rowID)) {
627
                                    $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValue(null);
628
                                }
629 2
                                $spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($this->styles[$style]);
630
                            }
631
                        }
632 3
                        ++$columnID;
633 3
                        while ($additionalMergedCells > 0) {
634 2
                            ++$columnID;
635 2
                            --$additionalMergedCells;
636
                        }
637
                    }
638
639 3
                    if ($rowHasData) {
640 3
                        if (isset($row_ss['Height'])) {
641 3
                            $rowHeight = $row_ss['Height'];
642 3
                            $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setRowHeight($rowHeight);
643
                        }
644
                    }
645
646 3
                    ++$rowID;
647
                }
648
            }
649 3
            ++$worksheetID;
650
        }
651
652
        // Return
653 3
        return $spreadsheet;
654
    }
655
656 3
    protected static function convertStringEncoding($string, $charset)
657
    {
658 3
        if ($charset != 'UTF-8') {
659
            return StringHelper::convertEncoding($string, 'UTF-8', $charset);
660
        }
661
662 3
        return $string;
663
    }
664
665 3
    protected function parseRichText($is)
666
    {
667 3
        $value = new RichText();
668
669 3
        $value->createText(self::convertStringEncoding($is, $this->charSet));
670
671 3
        return $value;
672
    }
673
674
    /**
675
     * @param SimpleXMLElement $xml
676
     * @param array $namespaces
677
     */
678 3
    private function parseStyles(SimpleXMLElement $xml, array $namespaces)
679
    {
680 3
        if (!isset($xml->Styles)) {
681 1
            return;
682
        }
683
684 2
        foreach ($xml->Styles[0] as $style) {
685 2
            $style_ss = $style->attributes($namespaces['ss']);
686 2
            $styleID = (string) $style_ss['ID'];
687 2
            $this->styles[$styleID] = (isset($this->styles['Default'])) ? $this->styles['Default'] : [];
688 2
            foreach ($style as $styleType => $styleData) {
689 2
                $styleAttributes = $styleData->attributes($namespaces['ss']);
690 2
                switch ($styleType) {
691 2
                    case 'Alignment':
692 2
                        $this->parseStyleAlignment($styleID, $styleAttributes);
693
694 2
                        break;
695 2
                    case 'Borders':
696 2
                        $this->parseStyleBorders($styleID, $styleData, $namespaces);
697
698 2
                        break;
699 2
                    case 'Font':
700 2
                        $this->parseStyleFont($styleID, $styleAttributes);
701
702 2
                        break;
703 2
                    case 'Interior':
704 2
                        $this->parseStyleInterior($styleID, $styleAttributes);
705
706 2
                        break;
707 2
                    case 'NumberFormat':
708 2
                        $this->parseStyleNumberFormat($styleID, $styleAttributes);
709
710 2
                        break;
711
                }
712
            }
713
        }
714 2
    }
715
716
    /**
717
     * @param string $styleID
718
     * @param SimpleXMLElement $styleAttributes
719
     */
720 2
    private function parseStyleAlignment($styleID, SimpleXMLElement $styleAttributes)
721
    {
722
        $verticalAlignmentStyles = [
723 2
            Alignment::VERTICAL_BOTTOM,
724 1
            Alignment::VERTICAL_TOP,
725 1
            Alignment::VERTICAL_CENTER,
726 1
            Alignment::VERTICAL_JUSTIFY,
727
        ];
728
        $horizontalAlignmentStyles = [
729 2
            Alignment::HORIZONTAL_GENERAL,
730 1
            Alignment::HORIZONTAL_LEFT,
731 1
            Alignment::HORIZONTAL_RIGHT,
732 1
            Alignment::HORIZONTAL_CENTER,
733 1
            Alignment::HORIZONTAL_CENTER_CONTINUOUS,
734 1
            Alignment::HORIZONTAL_JUSTIFY,
735
        ];
736
737 2
        foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
738 2
            $styleAttributeValue = (string) $styleAttributeValue;
739 2
            switch ($styleAttributeKey) {
740 2
                case 'Vertical':
741 2
                    if (self::identifyFixedStyleValue($verticalAlignmentStyles, $styleAttributeValue)) {
742 2
                        $this->styles[$styleID]['alignment']['vertical'] = $styleAttributeValue;
743
                    }
744
745 2
                    break;
746 2
                case 'Horizontal':
747 2
                    if (self::identifyFixedStyleValue($horizontalAlignmentStyles, $styleAttributeValue)) {
748 2
                        $this->styles[$styleID]['alignment']['horizontal'] = $styleAttributeValue;
749
                    }
750
751 2
                    break;
752 2
                case 'WrapText':
753 2
                    $this->styles[$styleID]['alignment']['wrapText'] = true;
754
755 2
                    break;
756
            }
757
        }
758 2
    }
759
760
    /**
761
     * @param $styleID
762
     * @param SimpleXMLElement $styleData
763
     * @param array $namespaces
764
     */
765 2
    private function parseStyleBorders($styleID, SimpleXMLElement $styleData, array $namespaces)
766
    {
767 2
        foreach ($styleData->Border as $borderStyle) {
768 2
            $borderAttributes = $borderStyle->attributes($namespaces['ss']);
769 2
            $thisBorder = [];
770 2
            foreach ($borderAttributes as $borderStyleKey => $borderStyleValue) {
771 2
                switch ($borderStyleKey) {
772 2
                    case 'LineStyle':
773 2
                        $thisBorder['borderStyle'] = Border::BORDER_MEDIUM;
774
775 2
                        break;
776 2
                    case 'Weight':
777 2
                        break;
778 2
                    case 'Position':
779 2
                        $borderPosition = strtolower($borderStyleValue);
780
781 2
                        break;
782 2
                    case 'Color':
783 2
                        $borderColour = substr($borderStyleValue, 1);
784 2
                        $thisBorder['color']['rgb'] = $borderColour;
785
786 2
                        break;
787
                }
788
            }
789 2
            if (!empty($thisBorder)) {
790 2
                if (($borderPosition == 'left') || ($borderPosition == 'right') || ($borderPosition == 'top') || ($borderPosition == 'bottom')) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $borderPosition does not seem to be defined for all execution paths leading up to this point.
Loading history...
791 2
                    $this->styles[$styleID]['borders'][$borderPosition] = $thisBorder;
792
                }
793
            }
794
        }
795 2
    }
796
797
    /**
798
     * @param $styleID
799
     * @param SimpleXMLElement $styleAttributes
800
     */
801 2
    private function parseStyleFont($styleID, SimpleXMLElement $styleAttributes)
802
    {
803
        $underlineStyles = [
804 2
            Font::UNDERLINE_NONE,
805 1
            Font::UNDERLINE_DOUBLE,
806 1
            Font::UNDERLINE_DOUBLEACCOUNTING,
807 1
            Font::UNDERLINE_SINGLE,
808 1
            Font::UNDERLINE_SINGLEACCOUNTING,
809
        ];
810
811 2
        foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
812 2
            $styleAttributeValue = (string) $styleAttributeValue;
813 2
            switch ($styleAttributeKey) {
814 2
                case 'FontName':
815 2
                    $this->styles[$styleID]['font']['name'] = $styleAttributeValue;
816
817 2
                    break;
818 2
                case 'Size':
819 2
                    $this->styles[$styleID]['font']['size'] = $styleAttributeValue;
820
821 2
                    break;
822 2
                case 'Color':
823 2
                    $this->styles[$styleID]['font']['color']['rgb'] = substr($styleAttributeValue, 1);
824
825 2
                    break;
826 2
                case 'Bold':
827 2
                    $this->styles[$styleID]['font']['bold'] = true;
828
829 2
                    break;
830 2
                case 'Italic':
831 2
                    $this->styles[$styleID]['font']['italic'] = true;
832
833 2
                    break;
834 2
                case 'Underline':
835 2
                    if (self::identifyFixedStyleValue($underlineStyles, $styleAttributeValue)) {
836 2
                        $this->styles[$styleID]['font']['underline'] = $styleAttributeValue;
837
                    }
838
839 2
                    break;
840
            }
841
        }
842 2
    }
843
844
    /**
845
     * @param $styleID
846
     * @param SimpleXMLElement $styleAttributes
847
     */
848 2
    private function parseStyleInterior($styleID, SimpleXMLElement $styleAttributes)
849
    {
850 2
        foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
851 2
            switch ($styleAttributeKey) {
852 2
                case 'Color':
853 2
                    $this->styles[$styleID]['fill']['color']['rgb'] = substr($styleAttributeValue, 1);
854
855 2
                    break;
856 2
                case 'Pattern':
857 2
                    $this->styles[$styleID]['fill']['fillType'] = strtolower($styleAttributeValue);
858
859 2
                    break;
860
            }
861
        }
862 2
    }
863
864
    /**
865
     * @param $styleID
866
     * @param SimpleXMLElement $styleAttributes
867
     */
868 2
    private function parseStyleNumberFormat($styleID, SimpleXMLElement $styleAttributes)
869
    {
870 2
        $fromFormats = ['\-', '\ '];
871 2
        $toFormats = ['-', ' '];
872
873 2
        foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
874 2
            $styleAttributeValue = str_replace($fromFormats, $toFormats, $styleAttributeValue);
875
            switch ($styleAttributeValue) {
876 2
                case 'Short Date':
877 2
                    $styleAttributeValue = 'dd/mm/yyyy';
878
879 2
                    break;
880
            }
881
882 2
            if ($styleAttributeValue > '') {
883 2
                $this->styles[$styleID]['numberFormat']['formatCode'] = $styleAttributeValue;
884
            }
885
        }
886 2
    }
887
}
888