1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace PhpOffice\PhpSpreadsheet\Reader; |
4
|
|
|
|
5
|
|
|
use DateTimeZone; |
6
|
|
|
use PhpOffice\PhpSpreadsheet\Cell; |
7
|
|
|
use PhpOffice\PhpSpreadsheet\Document\Properties; |
8
|
|
|
use PhpOffice\PhpSpreadsheet\RichText; |
9
|
|
|
use PhpOffice\PhpSpreadsheet\Settings; |
10
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\Date; |
11
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\File; |
12
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; |
13
|
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
14
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Alignment; |
15
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Border; |
16
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Font; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Copyright (c) 2006 - 2016 PhpSpreadsheet. |
20
|
|
|
* |
21
|
|
|
* This library is free software; you can redistribute it and/or |
22
|
|
|
* modify it under the terms of the GNU Lesser General Public |
23
|
|
|
* License as published by the Free Software Foundation; either |
24
|
|
|
* version 2.1 of the License, or (at your option) any later version. |
25
|
|
|
* |
26
|
|
|
* This library is distributed in the hope that it will be useful, |
27
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
28
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
29
|
|
|
* Lesser General Public License for more details. |
30
|
|
|
* |
31
|
|
|
* You should have received a copy of the GNU Lesser General Public |
32
|
|
|
* License along with this library; if not, write to the Free Software |
33
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
34
|
|
|
* |
35
|
|
|
* @category PhpSpreadsheet |
36
|
|
|
* |
37
|
|
|
* @copyright Copyright (c) 2006 - 2016 PhpSpreadsheet (https://github.com/PHPOffice/PhpSpreadsheet) |
38
|
|
|
* @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt LGPL |
39
|
|
|
*/ |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Reader for SpreadsheetML, the XML schema for Microsoft Office Excel 2003. |
43
|
|
|
*/ |
44
|
|
|
class Xml extends BaseReader implements IReader |
45
|
|
|
{ |
46
|
|
|
/** |
47
|
|
|
* Formats. |
48
|
|
|
* |
49
|
|
|
* @var array |
50
|
|
|
*/ |
51
|
|
|
protected $styles = []; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Character set used in the file. |
55
|
|
|
* |
56
|
|
|
* @var string |
57
|
|
|
*/ |
58
|
|
|
protected $charSet = 'UTF-8'; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Create a new Excel2003XML Reader instance. |
62
|
|
|
*/ |
63
|
4 |
|
public function __construct() |
64
|
|
|
{ |
65
|
4 |
|
$this->readFilter = new DefaultReadFilter(); |
66
|
4 |
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Can the current IReader read the file? |
70
|
|
|
* |
71
|
|
|
* @param string $pFilename |
72
|
|
|
* |
73
|
|
|
* @throws Exception |
74
|
|
|
* |
75
|
|
|
* @return bool |
76
|
|
|
*/ |
77
|
3 |
|
public function canRead($pFilename) |
78
|
|
|
{ |
79
|
|
|
// Office xmlns:o="urn:schemas-microsoft-com:office:office" |
80
|
|
|
// Excel xmlns:x="urn:schemas-microsoft-com:office:excel" |
81
|
|
|
// XML Spreadsheet xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" |
82
|
|
|
// Spreadsheet component xmlns:c="urn:schemas-microsoft-com:office:component:spreadsheet" |
83
|
|
|
// XML schema xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" |
84
|
|
|
// XML data type xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" |
85
|
|
|
// MS-persist recordset xmlns:rs="urn:schemas-microsoft-com:rowset" |
86
|
|
|
// Rowset xmlns:z="#RowsetSchema" |
87
|
|
|
// |
88
|
|
|
|
89
|
|
|
$signature = [ |
90
|
3 |
|
'<?xml version="1.0"', |
91
|
|
|
'<?mso-application progid="Excel.Sheet"?>', |
92
|
|
|
]; |
93
|
|
|
|
94
|
|
|
// Open file |
95
|
3 |
|
$this->openFile($pFilename); |
96
|
3 |
|
$fileHandle = $this->fileHandle; |
97
|
|
|
|
98
|
|
|
// Read sample data (first 2 KB will do) |
99
|
3 |
|
$data = fread($fileHandle, 2048); |
100
|
3 |
|
fclose($fileHandle); |
101
|
3 |
|
$data = strtr($data, "'", '"'); // fix headers with single quote |
102
|
|
|
|
103
|
3 |
|
$valid = true; |
104
|
3 |
|
foreach ($signature as $match) { |
105
|
|
|
// every part of the signature must be present |
106
|
3 |
|
if (strpos($data, $match) === false) { |
107
|
|
|
$valid = false; |
108
|
3 |
|
break; |
109
|
|
|
} |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
// Retrieve charset encoding |
113
|
3 |
|
if (preg_match('/<?xml.*encoding=[\'"](.*?)[\'"].*?>/um', $data, $matches)) { |
114
|
3 |
|
$this->charSet = strtoupper($matches[1]); |
115
|
|
|
} |
116
|
|
|
|
117
|
3 |
|
return $valid; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Check if the file is a valid SimpleXML. |
122
|
|
|
* |
123
|
|
|
* @param string $pFilename |
124
|
|
|
* |
125
|
|
|
* @throws Exception |
126
|
|
|
* |
127
|
|
|
* @return false|\SimpleXMLElement |
128
|
|
|
*/ |
129
|
3 |
|
public function trySimpleXMLLoadString($pFilename) |
130
|
|
|
{ |
131
|
|
|
try { |
132
|
3 |
|
$xml = simplexml_load_string( |
133
|
3 |
|
$this->securityScan(file_get_contents($pFilename)), |
134
|
3 |
|
'SimpleXMLElement', |
135
|
3 |
|
Settings::getLibXmlLoaderOptions() |
136
|
|
|
); |
137
|
1 |
|
} catch (\Exception $e) { |
138
|
1 |
|
throw new Exception('Cannot load invalid XML file: ' . $pFilename, 0, $e); |
139
|
|
|
} |
140
|
|
|
|
141
|
2 |
|
return $xml; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. |
146
|
|
|
* |
147
|
|
|
* @param string $pFilename |
148
|
|
|
* |
149
|
|
|
* @throws Exception |
150
|
|
|
*/ |
151
|
|
|
public function listWorksheetNames($pFilename) |
152
|
|
|
{ |
153
|
|
|
File::assertFile($pFilename); |
154
|
|
|
if (!$this->canRead($pFilename)) { |
155
|
|
|
throw new Exception($pFilename . ' is an Invalid Spreadsheet file.'); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
$worksheetNames = []; |
159
|
|
|
|
160
|
|
|
$xml = $this->trySimpleXMLLoadString($pFilename); |
161
|
|
|
|
162
|
|
|
$namespaces = $xml->getNamespaces(true); |
163
|
|
|
|
164
|
|
|
$xml_ss = $xml->children($namespaces['ss']); |
165
|
|
|
foreach ($xml_ss->Worksheet as $worksheet) { |
166
|
|
|
$worksheet_ss = $worksheet->attributes($namespaces['ss']); |
167
|
|
|
$worksheetNames[] = self::convertStringEncoding((string) $worksheet_ss['Name'], $this->charSet); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
return $worksheetNames; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). |
175
|
|
|
* |
176
|
|
|
* @param string $pFilename |
177
|
|
|
* |
178
|
|
|
* @throws Exception |
179
|
|
|
*/ |
180
|
|
|
public function listWorksheetInfo($pFilename) |
181
|
|
|
{ |
182
|
|
|
File::assertFile($pFilename); |
183
|
|
|
|
184
|
|
|
$worksheetInfo = []; |
185
|
|
|
|
186
|
|
|
$xml = $this->trySimpleXMLLoadString($pFilename); |
187
|
|
|
|
188
|
|
|
$namespaces = $xml->getNamespaces(true); |
189
|
|
|
|
190
|
|
|
$worksheetID = 1; |
191
|
|
|
$xml_ss = $xml->children($namespaces['ss']); |
192
|
|
|
foreach ($xml_ss->Worksheet as $worksheet) { |
193
|
|
|
$worksheet_ss = $worksheet->attributes($namespaces['ss']); |
194
|
|
|
|
195
|
|
|
$tmpInfo = []; |
196
|
|
|
$tmpInfo['worksheetName'] = ''; |
197
|
|
|
$tmpInfo['lastColumnLetter'] = 'A'; |
198
|
|
|
$tmpInfo['lastColumnIndex'] = 0; |
199
|
|
|
$tmpInfo['totalRows'] = 0; |
200
|
|
|
$tmpInfo['totalColumns'] = 0; |
201
|
|
|
|
202
|
|
|
if (isset($worksheet_ss['Name'])) { |
203
|
|
|
$tmpInfo['worksheetName'] = (string) $worksheet_ss['Name']; |
204
|
|
|
} else { |
205
|
|
|
$tmpInfo['worksheetName'] = "Worksheet_{$worksheetID}"; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
if (isset($worksheet->Table->Row)) { |
209
|
|
|
$rowIndex = 0; |
210
|
|
|
|
211
|
|
|
foreach ($worksheet->Table->Row as $rowData) { |
212
|
|
|
$columnIndex = 0; |
213
|
|
|
$rowHasData = false; |
214
|
|
|
|
215
|
|
|
foreach ($rowData->Cell as $cell) { |
216
|
|
|
if (isset($cell->Data)) { |
217
|
|
|
$tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex); |
218
|
|
|
$rowHasData = true; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
++$columnIndex; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
++$rowIndex; |
225
|
|
|
|
226
|
|
|
if ($rowHasData) { |
227
|
|
|
$tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex); |
228
|
|
|
} |
229
|
|
|
} |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
$tmpInfo['lastColumnLetter'] = Cell::stringFromColumnIndex($tmpInfo['lastColumnIndex']); |
233
|
|
|
$tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1; |
234
|
|
|
|
235
|
|
|
$worksheetInfo[] = $tmpInfo; |
236
|
|
|
++$worksheetID; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
return $worksheetInfo; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Loads Spreadsheet from file. |
244
|
|
|
* |
245
|
|
|
* @param string $pFilename |
246
|
|
|
* |
247
|
|
|
* @throws Exception |
248
|
|
|
* |
249
|
|
|
* @return Spreadsheet |
250
|
|
|
*/ |
251
|
2 |
|
public function load($pFilename) |
252
|
|
|
{ |
253
|
|
|
// Create new Spreadsheet |
254
|
2 |
|
$spreadsheet = new Spreadsheet(); |
255
|
2 |
|
$spreadsheet->removeSheetByIndex(0); |
256
|
|
|
|
257
|
|
|
// Load into this instance |
258
|
2 |
|
return $this->loadIntoExisting($pFilename, $spreadsheet); |
259
|
|
|
} |
260
|
|
|
|
261
|
2 |
View Code Duplication |
protected static function identifyFixedStyleValue($styleList, &$styleAttributeValue) |
|
|
|
|
262
|
|
|
{ |
263
|
2 |
|
$styleAttributeValue = strtolower($styleAttributeValue); |
264
|
2 |
|
foreach ($styleList as $style) { |
265
|
2 |
|
if ($styleAttributeValue == strtolower($style)) { |
266
|
2 |
|
$styleAttributeValue = $style; |
267
|
|
|
|
268
|
2 |
|
return true; |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
return false; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* pixel units to excel width units(units of 1/256th of a character width). |
277
|
|
|
* |
278
|
|
|
* @param pxs |
279
|
|
|
* @param mixed $pxs |
280
|
|
|
* |
281
|
|
|
* @return |
282
|
|
|
*/ |
283
|
|
|
protected static function pixel2WidthUnits($pxs) |
284
|
|
|
{ |
285
|
|
|
$UNIT_OFFSET_MAP = [0, 36, 73, 109, 146, 182, 219]; |
286
|
|
|
|
287
|
|
|
$widthUnits = 256 * ($pxs / 7); |
288
|
|
|
$widthUnits += $UNIT_OFFSET_MAP[($pxs % 7)]; |
289
|
|
|
|
290
|
|
|
return $widthUnits; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* excel width units(units of 1/256th of a character width) to pixel units. |
295
|
|
|
* |
296
|
|
|
* @param widthUnits |
297
|
|
|
* @param mixed $widthUnits |
298
|
|
|
* |
299
|
|
|
* @return |
300
|
|
|
*/ |
301
|
|
|
protected static function widthUnits2Pixel($widthUnits) |
302
|
|
|
{ |
303
|
|
|
$pixels = ($widthUnits / 256) * 7; |
304
|
|
|
$offsetWidthUnits = $widthUnits % 256; |
305
|
|
|
$pixels += round($offsetWidthUnits / (256 / 7)); |
306
|
|
|
|
307
|
|
|
return $pixels; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
protected static function hex2str($hex) |
311
|
|
|
{ |
312
|
|
|
return chr(hexdec($hex[1])); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Loads from file into Spreadsheet instance. |
317
|
|
|
* |
318
|
|
|
* @param string $pFilename |
319
|
|
|
* @param Spreadsheet $spreadsheet |
320
|
|
|
* |
321
|
|
|
* @throws Exception |
322
|
|
|
* |
323
|
|
|
* @return Spreadsheet |
324
|
|
|
*/ |
325
|
2 |
|
public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet) |
326
|
|
|
{ |
327
|
2 |
|
$fromFormats = ['\-', '\ ']; |
328
|
2 |
|
$toFormats = ['-', ' ']; |
329
|
|
|
|
330
|
|
|
$underlineStyles = [ |
331
|
2 |
|
Font::UNDERLINE_NONE, |
332
|
1 |
|
Font::UNDERLINE_DOUBLE, |
333
|
1 |
|
Font::UNDERLINE_DOUBLEACCOUNTING, |
334
|
1 |
|
Font::UNDERLINE_SINGLE, |
335
|
1 |
|
Font::UNDERLINE_SINGLEACCOUNTING, |
336
|
|
|
]; |
337
|
|
|
$verticalAlignmentStyles = [ |
338
|
2 |
|
Alignment::VERTICAL_BOTTOM, |
339
|
1 |
|
Alignment::VERTICAL_TOP, |
340
|
1 |
|
Alignment::VERTICAL_CENTER, |
341
|
1 |
|
Alignment::VERTICAL_JUSTIFY, |
342
|
|
|
]; |
343
|
|
|
$horizontalAlignmentStyles = [ |
344
|
2 |
|
Alignment::HORIZONTAL_GENERAL, |
345
|
1 |
|
Alignment::HORIZONTAL_LEFT, |
346
|
1 |
|
Alignment::HORIZONTAL_RIGHT, |
347
|
1 |
|
Alignment::HORIZONTAL_CENTER, |
348
|
1 |
|
Alignment::HORIZONTAL_CENTER_CONTINUOUS, |
349
|
1 |
|
Alignment::HORIZONTAL_JUSTIFY, |
350
|
|
|
]; |
351
|
|
|
|
352
|
2 |
|
$timezoneObj = new DateTimeZone('Europe/London'); |
|
|
|
|
353
|
2 |
|
$GMT = new DateTimeZone('UTC'); |
|
|
|
|
354
|
|
|
|
355
|
2 |
|
File::assertFile($pFilename); |
356
|
2 |
|
if (!$this->canRead($pFilename)) { |
357
|
|
|
throw new Exception($pFilename . ' is an Invalid Spreadsheet file.'); |
358
|
|
|
} |
359
|
|
|
|
360
|
2 |
|
$xml = $this->trySimpleXMLLoadString($pFilename); |
361
|
|
|
|
362
|
2 |
|
$namespaces = $xml->getNamespaces(true); |
363
|
|
|
|
364
|
2 |
|
$docProps = $spreadsheet->getProperties(); |
365
|
2 |
|
if (isset($xml->DocumentProperties[0])) { |
366
|
|
|
foreach ($xml->DocumentProperties[0] as $propertyName => $propertyValue) { |
367
|
|
|
switch ($propertyName) { |
368
|
|
|
case 'Title': |
369
|
|
|
$docProps->setTitle(self::convertStringEncoding($propertyValue, $this->charSet)); |
370
|
|
|
break; |
371
|
|
|
case 'Subject': |
372
|
|
|
$docProps->setSubject(self::convertStringEncoding($propertyValue, $this->charSet)); |
373
|
|
|
break; |
374
|
|
|
case 'Author': |
375
|
|
|
$docProps->setCreator(self::convertStringEncoding($propertyValue, $this->charSet)); |
376
|
|
|
break; |
377
|
|
|
case 'Created': |
378
|
|
|
$creationDate = strtotime($propertyValue); |
379
|
|
|
$docProps->setCreated($creationDate); |
|
|
|
|
380
|
|
|
break; |
381
|
|
|
case 'LastAuthor': |
382
|
|
|
$docProps->setLastModifiedBy(self::convertStringEncoding($propertyValue, $this->charSet)); |
383
|
|
|
break; |
384
|
|
|
case 'LastSaved': |
385
|
|
|
$lastSaveDate = strtotime($propertyValue); |
386
|
|
|
$docProps->setModified($lastSaveDate); |
|
|
|
|
387
|
|
|
break; |
388
|
|
|
case 'Company': |
389
|
|
|
$docProps->setCompany(self::convertStringEncoding($propertyValue, $this->charSet)); |
390
|
|
|
break; |
391
|
|
|
case 'Category': |
392
|
|
|
$docProps->setCategory(self::convertStringEncoding($propertyValue, $this->charSet)); |
393
|
|
|
break; |
394
|
|
|
case 'Manager': |
395
|
|
|
$docProps->setManager(self::convertStringEncoding($propertyValue, $this->charSet)); |
396
|
|
|
break; |
397
|
|
|
case 'Keywords': |
398
|
|
|
$docProps->setKeywords(self::convertStringEncoding($propertyValue, $this->charSet)); |
399
|
|
|
break; |
400
|
|
|
case 'Description': |
401
|
|
|
$docProps->setDescription(self::convertStringEncoding($propertyValue, $this->charSet)); |
402
|
|
|
break; |
403
|
|
|
} |
404
|
|
|
} |
405
|
|
|
} |
406
|
2 |
|
if (isset($xml->CustomDocumentProperties)) { |
407
|
|
|
foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) { |
408
|
|
|
$propertyAttributes = $propertyValue->attributes($namespaces['dt']); |
409
|
|
|
$propertyName = preg_replace_callback('/_x([0-9a-z]{4})_/', ['self', 'hex2str'], $propertyName); |
410
|
|
|
$propertyType = Properties::PROPERTY_TYPE_UNKNOWN; |
411
|
|
|
switch ((string) $propertyAttributes) { |
412
|
|
|
case 'string': |
413
|
|
|
$propertyType = Properties::PROPERTY_TYPE_STRING; |
414
|
|
|
$propertyValue = trim($propertyValue); |
415
|
|
|
break; |
416
|
|
|
case 'boolean': |
417
|
|
|
$propertyType = Properties::PROPERTY_TYPE_BOOLEAN; |
418
|
|
|
$propertyValue = (bool) $propertyValue; |
419
|
|
|
break; |
420
|
|
|
case 'integer': |
421
|
|
|
$propertyType = Properties::PROPERTY_TYPE_INTEGER; |
422
|
|
|
$propertyValue = (int) $propertyValue; |
423
|
|
|
break; |
424
|
|
|
case 'float': |
425
|
|
|
$propertyType = Properties::PROPERTY_TYPE_FLOAT; |
426
|
|
|
$propertyValue = (float) $propertyValue; |
427
|
|
|
break; |
428
|
|
|
case 'dateTime.tz': |
429
|
|
|
$propertyType = Properties::PROPERTY_TYPE_DATE; |
430
|
|
|
$propertyValue = strtotime(trim($propertyValue)); |
431
|
|
|
break; |
432
|
|
|
} |
433
|
|
|
$docProps->setCustomProperty($propertyName, $propertyValue, $propertyType); |
434
|
|
|
} |
435
|
|
|
} |
436
|
|
|
|
437
|
2 |
|
foreach ($xml->Styles[0] as $style) { |
438
|
2 |
|
$style_ss = $style->attributes($namespaces['ss']); |
439
|
2 |
|
$styleID = (string) $style_ss['ID']; |
440
|
2 |
|
$this->styles[$styleID] = (isset($this->styles['Default'])) ? $this->styles['Default'] : []; |
441
|
2 |
|
foreach ($style as $styleType => $styleData) { |
442
|
2 |
|
$styleAttributes = $styleData->attributes($namespaces['ss']); |
443
|
|
|
switch ($styleType) { |
444
|
2 |
|
case 'Alignment': |
445
|
2 |
|
foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) { |
446
|
2 |
|
$styleAttributeValue = (string) $styleAttributeValue; |
447
|
|
|
switch ($styleAttributeKey) { |
448
|
2 |
|
case 'Vertical': |
449
|
2 |
|
if (self::identifyFixedStyleValue($verticalAlignmentStyles, $styleAttributeValue)) { |
450
|
2 |
|
$this->styles[$styleID]['alignment']['vertical'] = $styleAttributeValue; |
451
|
|
|
} |
452
|
2 |
|
break; |
453
|
2 |
|
case 'Horizontal': |
454
|
2 |
|
if (self::identifyFixedStyleValue($horizontalAlignmentStyles, $styleAttributeValue)) { |
455
|
2 |
|
$this->styles[$styleID]['alignment']['horizontal'] = $styleAttributeValue; |
456
|
|
|
} |
457
|
2 |
|
break; |
458
|
2 |
|
case 'WrapText': |
459
|
2 |
|
$this->styles[$styleID]['alignment']['wrapText'] = true; |
460
|
2 |
|
break; |
461
|
|
|
} |
462
|
|
|
} |
463
|
2 |
|
break; |
464
|
2 |
|
case 'Borders': |
465
|
2 |
|
foreach ($styleData->Border as $borderStyle) { |
466
|
2 |
|
$borderAttributes = $borderStyle->attributes($namespaces['ss']); |
467
|
2 |
|
$thisBorder = []; |
468
|
2 |
|
foreach ($borderAttributes as $borderStyleKey => $borderStyleValue) { |
469
|
|
|
switch ($borderStyleKey) { |
470
|
2 |
|
case 'LineStyle': |
471
|
2 |
|
$thisBorder['borderStyle'] = Border::BORDER_MEDIUM; |
472
|
2 |
|
break; |
473
|
2 |
|
case 'Weight': |
474
|
2 |
|
break; |
475
|
2 |
|
case 'Position': |
476
|
2 |
|
$borderPosition = strtolower($borderStyleValue); |
477
|
2 |
|
break; |
478
|
2 |
|
case 'Color': |
479
|
2 |
|
$borderColour = substr($borderStyleValue, 1); |
480
|
2 |
|
$thisBorder['color']['rgb'] = $borderColour; |
481
|
2 |
|
break; |
482
|
|
|
} |
483
|
|
|
} |
484
|
2 |
|
if (!empty($thisBorder)) { |
485
|
2 |
|
if (($borderPosition == 'left') || ($borderPosition == 'right') || ($borderPosition == 'top') || ($borderPosition == 'bottom')) { |
486
|
2 |
|
$this->styles[$styleID]['borders'][$borderPosition] = $thisBorder; |
|
|
|
|
487
|
|
|
} |
488
|
|
|
} |
489
|
|
|
} |
490
|
2 |
|
break; |
491
|
2 |
|
case 'Font': |
492
|
2 |
|
foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) { |
493
|
2 |
|
$styleAttributeValue = (string) $styleAttributeValue; |
494
|
|
|
switch ($styleAttributeKey) { |
495
|
2 |
|
case 'FontName': |
496
|
2 |
|
$this->styles[$styleID]['font']['name'] = $styleAttributeValue; |
497
|
2 |
|
break; |
498
|
2 |
|
case 'Size': |
499
|
2 |
|
$this->styles[$styleID]['font']['size'] = $styleAttributeValue; |
500
|
2 |
|
break; |
501
|
2 |
View Code Duplication |
case 'Color': |
|
|
|
|
502
|
2 |
|
$this->styles[$styleID]['font']['color']['rgb'] = substr($styleAttributeValue, 1); |
503
|
2 |
|
break; |
504
|
2 |
|
case 'Bold': |
505
|
2 |
|
$this->styles[$styleID]['font']['bold'] = true; |
506
|
2 |
|
break; |
507
|
2 |
|
case 'Italic': |
508
|
2 |
|
$this->styles[$styleID]['font']['italic'] = true; |
509
|
2 |
|
break; |
510
|
2 |
|
case 'Underline': |
511
|
2 |
|
if (self::identifyFixedStyleValue($underlineStyles, $styleAttributeValue)) { |
512
|
2 |
|
$this->styles[$styleID]['font']['underline'] = $styleAttributeValue; |
513
|
|
|
} |
514
|
2 |
|
break; |
515
|
|
|
} |
516
|
|
|
} |
517
|
2 |
|
break; |
518
|
2 |
|
case 'Interior': |
519
|
2 |
|
foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) { |
520
|
|
|
switch ($styleAttributeKey) { |
521
|
2 |
View Code Duplication |
case 'Color': |
|
|
|
|
522
|
2 |
|
$this->styles[$styleID]['fill']['color']['rgb'] = substr($styleAttributeValue, 1); |
523
|
2 |
|
break; |
524
|
2 |
|
case 'Pattern': |
525
|
2 |
|
$this->styles[$styleID]['fill']['fillType'] = strtolower($styleAttributeValue); |
526
|
2 |
|
break; |
527
|
|
|
} |
528
|
|
|
} |
529
|
2 |
|
break; |
530
|
2 |
|
case 'NumberFormat': |
531
|
2 |
|
foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) { |
532
|
2 |
|
$styleAttributeValue = str_replace($fromFormats, $toFormats, $styleAttributeValue); |
533
|
|
|
switch ($styleAttributeValue) { |
534
|
2 |
|
case 'Short Date': |
535
|
2 |
|
$styleAttributeValue = 'dd/mm/yyyy'; |
536
|
2 |
|
break; |
537
|
|
|
} |
538
|
2 |
|
if ($styleAttributeValue > '') { |
539
|
2 |
|
$this->styles[$styleID]['numberFormat']['formatCode'] = $styleAttributeValue; |
540
|
|
|
} |
541
|
|
|
} |
542
|
2 |
|
break; |
543
|
|
|
case 'Protection': |
544
|
|
|
foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) { |
|
|
|
|
545
|
|
|
} |
546
|
2 |
|
break; |
547
|
|
|
} |
548
|
|
|
} |
549
|
|
|
} |
550
|
|
|
|
551
|
2 |
|
$worksheetID = 0; |
552
|
2 |
|
$xml_ss = $xml->children($namespaces['ss']); |
553
|
|
|
|
554
|
2 |
|
foreach ($xml_ss->Worksheet as $worksheet) { |
555
|
2 |
|
$worksheet_ss = $worksheet->attributes($namespaces['ss']); |
556
|
|
|
|
557
|
2 |
|
if ((isset($this->loadSheetsOnly)) && (isset($worksheet_ss['Name'])) && |
558
|
2 |
|
(!in_array($worksheet_ss['Name'], $this->loadSheetsOnly))) { |
559
|
|
|
continue; |
560
|
|
|
} |
561
|
|
|
|
562
|
|
|
// Create new Worksheet |
563
|
2 |
|
$spreadsheet->createSheet(); |
564
|
2 |
|
$spreadsheet->setActiveSheetIndex($worksheetID); |
565
|
2 |
|
if (isset($worksheet_ss['Name'])) { |
566
|
2 |
|
$worksheetName = self::convertStringEncoding((string) $worksheet_ss['Name'], $this->charSet); |
567
|
|
|
// Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in |
568
|
|
|
// formula cells... during the load, all formulae should be correct, and we're simply bringing |
569
|
|
|
// the worksheet name in line with the formula, not the reverse |
570
|
2 |
|
$spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); |
571
|
|
|
} |
572
|
|
|
|
573
|
2 |
|
$columnID = 'A'; |
574
|
2 |
|
if (isset($worksheet->Table->Column)) { |
575
|
2 |
|
foreach ($worksheet->Table->Column as $columnData) { |
576
|
2 |
|
$columnData_ss = $columnData->attributes($namespaces['ss']); |
577
|
2 |
|
if (isset($columnData_ss['Index'])) { |
578
|
2 |
|
$columnID = Cell::stringFromColumnIndex($columnData_ss['Index'] - 1); |
579
|
|
|
} |
580
|
2 |
|
if (isset($columnData_ss['Width'])) { |
581
|
2 |
|
$columnWidth = $columnData_ss['Width']; |
582
|
2 |
|
$spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4); |
583
|
|
|
} |
584
|
2 |
|
++$columnID; |
585
|
|
|
} |
586
|
|
|
} |
587
|
|
|
|
588
|
2 |
|
$rowID = 1; |
589
|
2 |
|
if (isset($worksheet->Table->Row)) { |
590
|
2 |
|
$additionalMergedCells = 0; |
591
|
2 |
|
foreach ($worksheet->Table->Row as $rowData) { |
592
|
2 |
|
$rowHasData = false; |
593
|
2 |
|
$row_ss = $rowData->attributes($namespaces['ss']); |
594
|
2 |
|
if (isset($row_ss['Index'])) { |
595
|
2 |
|
$rowID = (int) $row_ss['Index']; |
596
|
|
|
} |
597
|
|
|
|
598
|
2 |
|
$columnID = 'A'; |
599
|
2 |
|
foreach ($rowData->Cell as $cell) { |
600
|
2 |
|
$cell_ss = $cell->attributes($namespaces['ss']); |
601
|
2 |
|
if (isset($cell_ss['Index'])) { |
602
|
2 |
|
$columnID = Cell::stringFromColumnIndex($cell_ss['Index'] - 1); |
603
|
|
|
} |
604
|
2 |
|
$cellRange = $columnID . $rowID; |
605
|
|
|
|
606
|
2 |
View Code Duplication |
if ($this->getReadFilter() !== null) { |
|
|
|
|
607
|
2 |
|
if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { |
|
|
|
|
608
|
|
|
++$columnID; |
609
|
|
|
continue; |
610
|
|
|
} |
611
|
|
|
} |
612
|
|
|
|
613
|
2 |
|
if (isset($cell_ss['HRef'])) { |
614
|
2 |
|
$spreadsheet->getActiveSheet()->getCell($cellRange)->getHyperlink()->setUrl($cell_ss['HRef']); |
615
|
|
|
} |
616
|
|
|
|
617
|
2 |
|
if ((isset($cell_ss['MergeAcross'])) || (isset($cell_ss['MergeDown']))) { |
618
|
2 |
|
$columnTo = $columnID; |
619
|
2 |
|
if (isset($cell_ss['MergeAcross'])) { |
620
|
2 |
|
$additionalMergedCells += (int) $cell_ss['MergeAcross']; |
621
|
2 |
|
$columnTo = Cell::stringFromColumnIndex(Cell::columnIndexFromString($columnID) + $cell_ss['MergeAcross'] - 1); |
622
|
|
|
} |
623
|
2 |
|
$rowTo = $rowID; |
624
|
2 |
|
if (isset($cell_ss['MergeDown'])) { |
625
|
2 |
|
$rowTo = $rowTo + $cell_ss['MergeDown']; |
626
|
|
|
} |
627
|
2 |
|
$cellRange .= ':' . $columnTo . $rowTo; |
628
|
2 |
|
$spreadsheet->getActiveSheet()->mergeCells($cellRange); |
629
|
|
|
} |
630
|
|
|
|
631
|
2 |
|
$cellIsSet = $hasCalculatedValue = false; |
632
|
2 |
|
$cellDataFormula = ''; |
633
|
2 |
|
if (isset($cell_ss['Formula'])) { |
634
|
2 |
|
$cellDataFormula = $cell_ss['Formula']; |
635
|
|
|
// added this as a check for array formulas |
636
|
2 |
|
if (isset($cell_ss['ArrayRange'])) { |
637
|
|
|
$cellDataCSEFormula = $cell_ss['ArrayRange']; |
|
|
|
|
638
|
|
|
} |
639
|
2 |
|
$hasCalculatedValue = true; |
640
|
|
|
} |
641
|
2 |
|
if (isset($cell->Data)) { |
642
|
2 |
|
$cellValue = $cellData = $cell->Data; |
643
|
2 |
|
$type = Cell\DataType::TYPE_NULL; |
644
|
2 |
|
$cellData_ss = $cellData->attributes($namespaces['ss']); |
645
|
2 |
|
if (isset($cellData_ss['Type'])) { |
646
|
2 |
|
$cellDataType = $cellData_ss['Type']; |
647
|
|
|
switch ($cellDataType) { |
648
|
|
|
/* |
649
|
|
|
const TYPE_STRING = 's'; |
650
|
|
|
const TYPE_FORMULA = 'f'; |
651
|
|
|
const TYPE_NUMERIC = 'n'; |
652
|
|
|
const TYPE_BOOL = 'b'; |
653
|
|
|
const TYPE_NULL = 'null'; |
654
|
|
|
const TYPE_INLINE = 'inlineStr'; |
655
|
|
|
const TYPE_ERROR = 'e'; |
656
|
|
|
*/ |
657
|
2 |
|
case 'String': |
658
|
2 |
|
$cellValue = self::convertStringEncoding($cellValue, $this->charSet); |
659
|
2 |
|
$type = Cell\DataType::TYPE_STRING; |
660
|
2 |
|
break; |
661
|
2 |
|
case 'Number': |
662
|
2 |
|
$type = Cell\DataType::TYPE_NUMERIC; |
663
|
2 |
|
$cellValue = (float) $cellValue; |
664
|
2 |
|
if (floor($cellValue) == $cellValue) { |
665
|
2 |
|
$cellValue = (int) $cellValue; |
666
|
|
|
} |
667
|
2 |
|
break; |
668
|
2 |
|
case 'Boolean': |
669
|
2 |
|
$type = Cell\DataType::TYPE_BOOL; |
670
|
2 |
|
$cellValue = ($cellValue != 0); |
671
|
2 |
|
break; |
672
|
2 |
|
case 'DateTime': |
673
|
2 |
|
$type = Cell\DataType::TYPE_NUMERIC; |
674
|
2 |
|
$cellValue = Date::PHPToExcel(strtotime($cellValue)); |
675
|
2 |
|
break; |
676
|
|
|
case 'Error': |
677
|
|
|
$type = Cell\DataType::TYPE_ERROR; |
678
|
|
|
break; |
679
|
|
|
} |
680
|
|
|
} |
681
|
|
|
|
682
|
2 |
|
if ($hasCalculatedValue) { |
683
|
2 |
|
$type = Cell\DataType::TYPE_FORMULA; |
684
|
2 |
|
$columnNumber = Cell::columnIndexFromString($columnID); |
685
|
2 |
|
if (substr($cellDataFormula, 0, 3) == 'of:') { |
686
|
2 |
|
$cellDataFormula = substr($cellDataFormula, 3); |
687
|
2 |
|
$temp = explode('"', $cellDataFormula); |
688
|
2 |
|
$key = false; |
689
|
2 |
|
foreach ($temp as &$value) { |
690
|
|
|
// Only replace in alternate array entries (i.e. non-quoted blocks) |
691
|
2 |
|
if ($key = !$key) { |
692
|
2 |
|
$value = str_replace(['[.', '.', ']'], '', $value); |
693
|
|
|
} |
694
|
|
|
} |
695
|
|
|
} else { |
696
|
|
|
// Convert R1C1 style references to A1 style references (but only when not quoted) |
697
|
|
|
$temp = explode('"', $cellDataFormula); |
698
|
|
|
$key = false; |
699
|
|
View Code Duplication |
foreach ($temp as &$value) { |
|
|
|
|
700
|
|
|
// Only replace in alternate array entries (i.e. non-quoted blocks) |
701
|
|
|
if ($key = !$key) { |
702
|
|
|
preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE); |
703
|
|
|
// Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way |
704
|
|
|
// through the formula from left to right. Reversing means that we work right to left.through |
705
|
|
|
// the formula |
706
|
|
|
$cellReferences = array_reverse($cellReferences); |
707
|
|
|
// Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent, |
708
|
|
|
// then modify the formula to use that new reference |
709
|
|
|
foreach ($cellReferences as $cellReference) { |
710
|
|
|
$rowReference = $cellReference[2][0]; |
711
|
|
|
// Empty R reference is the current row |
712
|
|
|
if ($rowReference == '') { |
713
|
|
|
$rowReference = $rowID; |
714
|
|
|
} |
715
|
|
|
// Bracketed R references are relative to the current row |
716
|
|
|
if ($rowReference[0] == '[') { |
717
|
|
|
$rowReference = $rowID + trim($rowReference, '[]'); |
718
|
|
|
} |
719
|
|
|
$columnReference = $cellReference[4][0]; |
720
|
|
|
// Empty C reference is the current column |
721
|
|
|
if ($columnReference == '') { |
722
|
|
|
$columnReference = $columnNumber; |
723
|
|
|
} |
724
|
|
|
// Bracketed C references are relative to the current column |
725
|
|
|
if ($columnReference[0] == '[') { |
726
|
|
|
$columnReference = $columnNumber + trim($columnReference, '[]'); |
727
|
|
|
} |
728
|
|
|
$A1CellReference = Cell::stringFromColumnIndex($columnReference - 1) . $rowReference; |
729
|
|
|
$value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0])); |
730
|
|
|
} |
731
|
|
|
} |
732
|
|
|
} |
733
|
|
|
} |
734
|
2 |
|
unset($value); |
735
|
|
|
// Then rebuild the formula string |
736
|
2 |
|
$cellDataFormula = implode('"', $temp); |
737
|
|
|
} |
738
|
|
|
|
739
|
2 |
|
$spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValueExplicit((($hasCalculatedValue) ? $cellDataFormula : $cellValue), $type); |
740
|
2 |
|
if ($hasCalculatedValue) { |
741
|
2 |
|
$spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setCalculatedValue($cellValue); |
742
|
|
|
} |
743
|
2 |
|
$cellIsSet = $rowHasData = true; |
744
|
|
|
} |
745
|
|
|
|
746
|
2 |
|
if (isset($cell->Comment)) { |
747
|
2 |
|
$commentAttributes = $cell->Comment->attributes($namespaces['ss']); |
748
|
2 |
|
$author = 'unknown'; |
749
|
2 |
|
if (isset($commentAttributes->Author)) { |
750
|
|
|
$author = (string) $commentAttributes->Author; |
751
|
|
|
} |
752
|
2 |
|
$node = $cell->Comment->Data->asXML(); |
753
|
2 |
|
$annotation = strip_tags($node); |
754
|
2 |
|
$spreadsheet->getActiveSheet()->getComment($columnID . $rowID)->setAuthor(self::convertStringEncoding($author, $this->charSet))->setText($this->parseRichText($annotation)); |
755
|
|
|
} |
756
|
|
|
|
757
|
2 |
|
if (($cellIsSet) && (isset($cell_ss['StyleID']))) { |
758
|
2 |
|
$style = (string) $cell_ss['StyleID']; |
759
|
2 |
|
if ((isset($this->styles[$style])) && (!empty($this->styles[$style]))) { |
760
|
2 |
|
if (!$spreadsheet->getActiveSheet()->cellExists($columnID . $rowID)) { |
761
|
|
|
$spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValue(null); |
762
|
|
|
} |
763
|
2 |
|
$spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($this->styles[$style]); |
764
|
|
|
} |
765
|
|
|
} |
766
|
2 |
|
++$columnID; |
767
|
2 |
|
while ($additionalMergedCells > 0) { |
768
|
2 |
|
++$columnID; |
769
|
2 |
|
--$additionalMergedCells; |
770
|
|
|
} |
771
|
|
|
} |
772
|
|
|
|
773
|
2 |
|
if ($rowHasData) { |
774
|
2 |
|
if (isset($row_ss['StyleID'])) { |
775
|
|
|
$rowStyle = $row_ss['StyleID']; |
|
|
|
|
776
|
|
|
} |
777
|
2 |
|
if (isset($row_ss['Height'])) { |
778
|
2 |
|
$rowHeight = $row_ss['Height']; |
779
|
2 |
|
$spreadsheet->getActiveSheet()->getRowDimension($rowID)->setRowHeight($rowHeight); |
780
|
|
|
} |
781
|
|
|
} |
782
|
|
|
|
783
|
2 |
|
++$rowID; |
784
|
|
|
} |
785
|
|
|
} |
786
|
2 |
|
++$worksheetID; |
787
|
|
|
} |
788
|
|
|
|
789
|
|
|
// Return |
790
|
2 |
|
return $spreadsheet; |
791
|
|
|
} |
792
|
|
|
|
793
|
2 |
|
protected static function convertStringEncoding($string, $charset) |
794
|
|
|
{ |
795
|
2 |
|
if ($charset != 'UTF-8') { |
796
|
|
|
return StringHelper::convertEncoding($string, 'UTF-8', $charset); |
797
|
|
|
} |
798
|
|
|
|
799
|
2 |
|
return $string; |
800
|
|
|
} |
801
|
|
|
|
802
|
2 |
|
protected function parseRichText($is) |
803
|
|
|
{ |
804
|
2 |
|
$value = new RichText(); |
805
|
|
|
|
806
|
2 |
|
$value->createText(self::convertStringEncoding($is, $this->charSet)); |
807
|
|
|
|
808
|
2 |
|
return $value; |
809
|
|
|
} |
810
|
|
|
} |
811
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.