1
|
|
|
<?php |
|
|
|
|
2
|
|
|
|
3
|
|
|
namespace PhpOffice\PhpSpreadsheet\Shared; |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* PhpSpreadsheet |
7
|
|
|
* |
8
|
|
|
* Copyright (c) 2006 - 2016 PhpSpreadsheet |
9
|
|
|
* |
10
|
|
|
* This library is free software; you can redistribute it and/or |
11
|
|
|
* modify it under the terms of the GNU Lesser General Public |
12
|
|
|
* License as published by the Free Software Foundation; either |
13
|
|
|
* version 2.1 of the License, or (at your option) any later version. |
14
|
|
|
* |
15
|
|
|
* This library is distributed in the hope that it will be useful, |
16
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
17
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
18
|
|
|
* Lesser General Public License for more details. |
19
|
|
|
* |
20
|
|
|
* You should have received a copy of the GNU Lesser General Public |
21
|
|
|
* License along with this library; if not, write to the Free Software |
22
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
23
|
|
|
* |
24
|
|
|
* @category PhpSpreadsheet |
25
|
|
|
* @copyright Copyright (c) 2006 - 2016 PhpSpreadsheet (https://github.com/PHPOffice/PhpSpreadsheet) |
26
|
|
|
* @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt LGPL |
27
|
|
|
*/ |
28
|
|
|
|
29
|
5 |
|
defined('IDENTIFIER_OLE') || |
30
|
5 |
|
define('IDENTIFIER_OLE', pack('CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1)); |
31
|
|
|
|
32
|
|
|
class OLERead |
33
|
|
|
{ |
34
|
|
|
private $data = ''; |
35
|
|
|
|
36
|
|
|
// OLE identifier |
37
|
|
|
const IDENTIFIER_OLE = IDENTIFIER_OLE; |
38
|
|
|
|
39
|
|
|
// Size of a sector = 512 bytes |
40
|
|
|
const BIG_BLOCK_SIZE = 0x200; |
41
|
|
|
|
42
|
|
|
// Size of a short sector = 64 bytes |
43
|
|
|
const SMALL_BLOCK_SIZE = 0x40; |
44
|
|
|
|
45
|
|
|
// Size of a directory entry always = 128 bytes |
46
|
|
|
const PROPERTY_STORAGE_BLOCK_SIZE = 0x80; |
47
|
|
|
|
48
|
|
|
// Minimum size of a standard stream = 4096 bytes, streams smaller than this are stored as short streams |
49
|
|
|
const SMALL_BLOCK_THRESHOLD = 0x1000; |
50
|
|
|
|
51
|
|
|
// header offsets |
52
|
|
|
const NUM_BIG_BLOCK_DEPOT_BLOCKS_POS = 0x2c; |
53
|
|
|
const ROOT_START_BLOCK_POS = 0x30; |
54
|
|
|
const SMALL_BLOCK_DEPOT_BLOCK_POS = 0x3c; |
55
|
|
|
const EXTENSION_BLOCK_POS = 0x44; |
56
|
|
|
const NUM_EXTENSION_BLOCK_POS = 0x48; |
57
|
|
|
const BIG_BLOCK_DEPOT_BLOCKS_POS = 0x4c; |
58
|
|
|
|
59
|
|
|
// property storage offsets (directory offsets) |
60
|
|
|
const SIZE_OF_NAME_POS = 0x40; |
61
|
|
|
const TYPE_POS = 0x42; |
62
|
|
|
const START_BLOCK_POS = 0x74; |
63
|
|
|
const SIZE_POS = 0x78; |
64
|
|
|
|
65
|
|
|
public $wrkbook = null; |
66
|
|
|
public $summaryInformation = null; |
67
|
|
|
public $documentSummaryInformation = null; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Read the file |
71
|
|
|
* |
72
|
|
|
* @param $pFilename string Filename |
73
|
|
|
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception |
74
|
|
|
*/ |
75
|
5 |
|
public function read($pFilename) |
76
|
|
|
{ |
77
|
5 |
|
File::assertFile($pFilename); |
78
|
|
|
|
79
|
|
|
// Get the file identifier |
80
|
|
|
// Don't bother reading the whole file until we know it's a valid OLE file |
81
|
5 |
|
$this->data = file_get_contents($pFilename, false, null, 0, 8); |
82
|
|
|
|
83
|
|
|
// Check OLE identifier |
84
|
5 |
|
if ($this->data != self::IDENTIFIER_OLE) { |
85
|
|
|
throw new \PhpOffice\PhpSpreadsheet\Reader\Exception('The filename ' . $pFilename . ' is not recognised as an OLE file'); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
// Get the file data |
89
|
5 |
|
$this->data = file_get_contents($pFilename); |
90
|
|
|
|
91
|
|
|
// Total number of sectors used for the SAT |
92
|
5 |
|
$this->numBigBlockDepotBlocks = self::getInt4d($this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS); |
|
|
|
|
93
|
|
|
|
94
|
|
|
// SecID of the first sector of the directory stream |
95
|
5 |
|
$this->rootStartBlock = self::getInt4d($this->data, self::ROOT_START_BLOCK_POS); |
|
|
|
|
96
|
|
|
|
97
|
|
|
// SecID of the first sector of the SSAT (or -2 if not extant) |
98
|
5 |
|
$this->sbdStartBlock = self::getInt4d($this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS); |
|
|
|
|
99
|
|
|
|
100
|
|
|
// SecID of the first sector of the MSAT (or -2 if no additional sectors are used) |
101
|
5 |
|
$this->extensionBlock = self::getInt4d($this->data, self::EXTENSION_BLOCK_POS); |
|
|
|
|
102
|
|
|
|
103
|
|
|
// Total number of sectors used by MSAT |
104
|
5 |
|
$this->numExtensionBlocks = self::getInt4d($this->data, self::NUM_EXTENSION_BLOCK_POS); |
|
|
|
|
105
|
|
|
|
106
|
5 |
|
$bigBlockDepotBlocks = []; |
107
|
5 |
|
$pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS; |
108
|
|
|
|
109
|
5 |
|
$bbdBlocks = $this->numBigBlockDepotBlocks; |
110
|
|
|
|
111
|
5 |
|
if ($this->numExtensionBlocks != 0) { |
|
|
|
|
112
|
|
|
$bbdBlocks = (self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS) / 4; |
113
|
|
|
} |
114
|
|
|
|
115
|
5 |
View Code Duplication |
for ($i = 0; $i < $bbdBlocks; ++$i) { |
|
|
|
|
116
|
5 |
|
$bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos); |
117
|
5 |
|
$pos += 4; |
118
|
|
|
} |
119
|
|
|
|
120
|
5 |
|
for ($j = 0; $j < $this->numExtensionBlocks; ++$j) { |
|
|
|
|
121
|
|
|
$pos = ($this->extensionBlock + 1) * self::BIG_BLOCK_SIZE; |
122
|
|
|
$blocksToRead = min($this->numBigBlockDepotBlocks - $bbdBlocks, self::BIG_BLOCK_SIZE / 4 - 1); |
123
|
|
|
|
124
|
|
View Code Duplication |
for ($i = $bbdBlocks; $i < $bbdBlocks + $blocksToRead; ++$i) { |
|
|
|
|
125
|
|
|
$bigBlockDepotBlocks[$i] = self::getInt4d($this->data, $pos); |
126
|
|
|
$pos += 4; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
$bbdBlocks += $blocksToRead; |
130
|
|
|
if ($bbdBlocks < $this->numBigBlockDepotBlocks) { |
131
|
|
|
$this->extensionBlock = self::getInt4d($this->data, $pos); |
132
|
|
|
} |
133
|
|
|
} |
134
|
|
|
|
135
|
5 |
|
$pos = 0; |
|
|
|
|
136
|
5 |
|
$this->bigBlockChain = ''; |
|
|
|
|
137
|
5 |
|
$bbs = self::BIG_BLOCK_SIZE / 4; |
138
|
5 |
|
for ($i = 0; $i < $this->numBigBlockDepotBlocks; ++$i) { |
139
|
5 |
|
$pos = ($bigBlockDepotBlocks[$i] + 1) * self::BIG_BLOCK_SIZE; |
140
|
|
|
|
141
|
5 |
|
$this->bigBlockChain .= substr($this->data, $pos, 4 * $bbs); |
142
|
5 |
|
$pos += 4 * $bbs; |
143
|
|
|
} |
144
|
|
|
|
145
|
5 |
|
$pos = 0; |
|
|
|
|
146
|
5 |
|
$sbdBlock = $this->sbdStartBlock; |
147
|
5 |
|
$this->smallBlockChain = ''; |
|
|
|
|
148
|
5 |
|
while ($sbdBlock != -2) { |
149
|
5 |
|
$pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE; |
150
|
|
|
|
151
|
5 |
|
$this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs); |
152
|
5 |
|
$pos += 4 * $bbs; |
153
|
|
|
|
154
|
5 |
|
$sbdBlock = self::getInt4d($this->bigBlockChain, $sbdBlock * 4); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
// read the directory stream |
158
|
5 |
|
$block = $this->rootStartBlock; |
159
|
5 |
|
$this->entry = $this->_readData($block); |
|
|
|
|
160
|
|
|
|
161
|
5 |
|
$this->readPropertySets(); |
162
|
5 |
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Extract binary stream data |
166
|
|
|
* |
167
|
|
|
* @param int $stream |
168
|
|
|
* @return string |
169
|
|
|
*/ |
170
|
4 |
|
public function getStream($stream) |
171
|
|
|
{ |
172
|
4 |
|
if ($stream === null) { |
173
|
|
|
return null; |
174
|
|
|
} |
175
|
|
|
|
176
|
4 |
|
$streamData = ''; |
177
|
|
|
|
178
|
4 |
|
if ($this->props[$stream]['size'] < self::SMALL_BLOCK_THRESHOLD) { |
179
|
4 |
|
$rootdata = $this->_readData($this->props[$this->rootentry]['startBlock']); |
|
|
|
|
180
|
|
|
|
181
|
4 |
|
$block = $this->props[$stream]['startBlock']; |
182
|
|
|
|
183
|
4 |
|
while ($block != -2) { |
184
|
4 |
|
$pos = $block * self::SMALL_BLOCK_SIZE; |
185
|
4 |
|
$streamData .= substr($rootdata, $pos, self::SMALL_BLOCK_SIZE); |
186
|
|
|
|
187
|
4 |
|
$block = self::getInt4d($this->smallBlockChain, $block * 4); |
188
|
|
|
} |
189
|
|
|
|
190
|
4 |
|
return $streamData; |
191
|
|
|
} else { |
192
|
4 |
|
$numBlocks = $this->props[$stream]['size'] / self::BIG_BLOCK_SIZE; |
193
|
4 |
|
if ($this->props[$stream]['size'] % self::BIG_BLOCK_SIZE != 0) { |
194
|
4 |
|
++$numBlocks; |
195
|
|
|
} |
196
|
|
|
|
197
|
4 |
|
if ($numBlocks == 0) { |
198
|
|
|
return ''; |
199
|
|
|
} |
200
|
|
|
|
201
|
4 |
|
$block = $this->props[$stream]['startBlock']; |
202
|
|
|
|
203
|
4 |
View Code Duplication |
while ($block != -2) { |
|
|
|
|
204
|
4 |
|
$pos = ($block + 1) * self::BIG_BLOCK_SIZE; |
205
|
4 |
|
$streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); |
206
|
4 |
|
$block = self::getInt4d($this->bigBlockChain, $block * 4); |
207
|
|
|
} |
208
|
|
|
|
209
|
4 |
|
return $streamData; |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Read a standard stream (by joining sectors using information from SAT) |
215
|
|
|
* |
216
|
|
|
* @param int $bl Sector ID where the stream starts |
217
|
|
|
* @return string Data for standard stream |
218
|
|
|
*/ |
219
|
5 |
|
private function _readData($bl) |
220
|
|
|
{ |
221
|
5 |
|
$block = $bl; |
222
|
5 |
|
$data = ''; |
223
|
|
|
|
224
|
5 |
View Code Duplication |
while ($block != -2) { |
|
|
|
|
225
|
5 |
|
$pos = ($block + 1) * self::BIG_BLOCK_SIZE; |
226
|
5 |
|
$data .= substr($this->data, $pos, self::BIG_BLOCK_SIZE); |
227
|
5 |
|
$block = self::getInt4d($this->bigBlockChain, $block * 4); |
228
|
|
|
} |
229
|
|
|
|
230
|
5 |
|
return $data; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Read entries in the directory stream. |
235
|
|
|
*/ |
236
|
5 |
|
private function readPropertySets() |
237
|
|
|
{ |
238
|
5 |
|
$offset = 0; |
239
|
|
|
|
240
|
|
|
// loop through entires, each entry is 128 bytes |
241
|
5 |
|
$entryLen = strlen($this->entry); |
242
|
5 |
|
while ($offset < $entryLen) { |
243
|
|
|
// entry data (128 bytes) |
244
|
5 |
|
$d = substr($this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE); |
245
|
|
|
|
246
|
|
|
// size in bytes of name |
247
|
5 |
|
$nameSize = ord($d[self::SIZE_OF_NAME_POS]) | (ord($d[self::SIZE_OF_NAME_POS + 1]) << 8); |
248
|
|
|
|
249
|
|
|
// type of entry |
250
|
5 |
|
$type = ord($d[self::TYPE_POS]); |
251
|
|
|
|
252
|
|
|
// sectorID of first sector or short sector, if this entry refers to a stream (the case with workbook) |
253
|
|
|
// sectorID of first sector of the short-stream container stream, if this entry is root entry |
254
|
5 |
|
$startBlock = self::getInt4d($d, self::START_BLOCK_POS); |
255
|
|
|
|
256
|
5 |
|
$size = self::getInt4d($d, self::SIZE_POS); |
257
|
|
|
|
258
|
5 |
|
$name = str_replace("\x00", '', substr($d, 0, $nameSize)); |
259
|
|
|
|
260
|
5 |
|
$this->props[] = [ |
261
|
5 |
|
'name' => $name, |
262
|
5 |
|
'type' => $type, |
263
|
5 |
|
'startBlock' => $startBlock, |
264
|
5 |
|
'size' => $size, |
265
|
|
|
]; |
266
|
|
|
|
267
|
|
|
// tmp helper to simplify checks |
268
|
5 |
|
$upName = strtoupper($name); |
269
|
|
|
|
270
|
|
|
// Workbook directory entry (BIFF5 uses Book, BIFF8 uses Workbook) |
271
|
5 |
|
if (($upName === 'WORKBOOK') || ($upName === 'BOOK')) { |
272
|
5 |
|
$this->wrkbook = count($this->props) - 1; |
273
|
5 |
|
} elseif ($upName === 'ROOT ENTRY' || $upName === 'R') { |
274
|
|
|
// Root entry |
275
|
5 |
|
$this->rootentry = count($this->props) - 1; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
// Summary information |
279
|
5 |
|
if ($name == chr(5) . 'SummaryInformation') { |
280
|
5 |
|
$this->summaryInformation = count($this->props) - 1; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
// Additional Document Summary information |
284
|
5 |
|
if ($name == chr(5) . 'DocumentSummaryInformation') { |
285
|
5 |
|
$this->documentSummaryInformation = count($this->props) - 1; |
286
|
|
|
} |
287
|
|
|
|
288
|
5 |
|
$offset += self::PROPERTY_STORAGE_BLOCK_SIZE; |
289
|
|
|
} |
290
|
5 |
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Read 4 bytes of data at specified position |
294
|
|
|
* |
295
|
|
|
* @param string $data |
296
|
|
|
* @param int $pos |
297
|
|
|
* @return int |
298
|
|
|
*/ |
299
|
5 |
|
private static function getInt4d($data, $pos) |
300
|
|
|
{ |
301
|
5 |
|
if (trim($data) == '') { |
302
|
|
|
// No data provided |
303
|
|
|
throw new \PhpOffice\PhpSpreadsheet\Reader\Exception('Parameter data is empty.'); |
304
|
5 |
|
} elseif ($pos < 0) { |
305
|
|
|
// Invalid position |
306
|
|
|
throw new \PhpOffice\PhpSpreadsheet\Reader\Exception('Parameter pos=' . $pos . ' is invalid.'); |
307
|
|
|
} |
308
|
|
|
|
309
|
5 |
|
$len = strlen($data); |
310
|
5 |
|
if ($len < $pos + 4) { |
311
|
|
|
$data .= str_repeat("\0", $pos + 4 - $len); |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
// FIX: represent numbers correctly on 64-bit system |
315
|
|
|
// http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334 |
316
|
|
|
// Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems |
317
|
5 |
|
$_or_24 = ord($data[$pos + 3]); |
318
|
5 |
|
if ($_or_24 >= 128) { |
319
|
|
|
// negative number |
320
|
5 |
|
$_ord_24 = -abs((256 - $_or_24) << 24); |
321
|
|
|
} else { |
322
|
5 |
|
$_ord_24 = ($_or_24 & 127) << 24; |
323
|
|
|
} |
324
|
|
|
|
325
|
5 |
|
return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24; |
326
|
|
|
} |
327
|
|
|
} |
328
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.