fix image text
[pear] / OLE.php
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4: */
3 // +----------------------------------------------------------------------+
4 // | PHP Version 4                                                        |
5 // +----------------------------------------------------------------------+
6 // | Copyright (c) 1997-2002 The PHP Group                                |
7 // +----------------------------------------------------------------------+
8 // | This source file is subject to version 2.02 of the PHP license,      |
9 // | that is bundled with this package in the file LICENSE, and is        |
10 // | available at through the world-wide-web at                           |
11 // | http://www.php.net/license/2_02.txt.                                 |
12 // | If you did not receive a copy of the PHP license and are unable to   |
13 // | obtain it through the world-wide-web, please send a note to          |
14 // | license@php.net so we can mail you a copy immediately.               |
15 // +----------------------------------------------------------------------+
16 // | Author: Xavier Noguer <xnoguer@php.net>                              |
17 // | Based on OLE::Storage_Lite by Kawai, Takanori                        |
18 // +----------------------------------------------------------------------+
19 //
20 // $Id: OLE.php 260165 2008-05-23 16:33:58Z schmidt $
21
22
23 /**
24 * Constants for OLE package
25 */
26 define('OLE_PPS_TYPE_ROOT',        5);
27 define('OLE_PPS_TYPE_DIR',         1);
28 define('OLE_PPS_TYPE_FILE',        2);
29 define('OLE_DATA_SIZE_SMALL', 0x1000);
30 define('OLE_LONG_INT_SIZE',        4);
31 define('OLE_PPS_SIZE',          0x80);
32
33 require_once 'PEAR.php';
34
35 /**
36 * Array for storing OLE instances that are accessed from
37 * OLE_ChainedBlockStream::stream_open().
38 * @var  array
39 */
40 $GLOBALS['_OLE_INSTANCES'] = array();
41
42 /**
43 * OLE package base class.
44 *
45 * @category Structures
46 * @package  OLE
47 * @author   Xavier Noguer <xnoguer@php.net>
48 * @author   Christian Schmidt <schmidt@php.net>
49 */
50 class OLE extends PEAR
51 {
52
53     /**
54     * The file handle for reading an OLE container
55     * @var resource
56     */
57     var $_file_handle;
58
59     /**
60     * Array of PPS's found on the OLE container
61     * @var array
62     */
63     var $_list;
64
65     /**
66     * Root directory of OLE container
67     * @var OLE_PPS_Root
68     */
69     var $root;
70
71     /**
72     * Big Block Allocation Table
73     * @var array  (blockId => nextBlockId)
74     */
75     var $bbat;
76
77     /**
78     * Short Block Allocation Table
79     * @var array  (blockId => nextBlockId)
80     */
81     var $sbat;
82
83     /**
84     * Size of big blocks. This is usually 512.
85     * @var  int  number of octets per block.
86     */
87     var $bigBlockSize;
88
89     /**
90     * Size of small blocks. This is usually 64.
91     * @var  int  number of octets per block
92     */
93     var $smallBlockSize;
94
95     /**
96     * Creates a new OLE object
97     * @access public
98     */
99     function __construct()
100     {
101         $this->_list = array();
102     }
103
104     /**
105     * Destructor (using PEAR)
106     * Just closes the file handle on the OLE file.
107     *
108     * @access private
109     */
110     function _OLE()
111     {
112         fclose($this->_file_handle);
113     }
114
115     /**
116     * Reads an OLE container from the contents of the file given.
117     *
118     * @access public
119     * @param string $file
120     * @return mixed true on success, PEAR_Error on failure
121     */
122     function read($file)
123     {
124         $fh = @fopen($file, "r");
125         if (!$fh) {
126             return $this->raiseError("Can't open file $file");
127         }
128         $this->_file_handle = $fh;
129
130         $signature = fread($fh, 8);
131         if ("\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1" != $signature) {
132             return $this->raiseError("File doesn't seem to be an OLE container.");
133         }
134         fseek($fh, 28);
135         if (fread($fh, 2) != "\xFE\xFF") {
136             // This shouldn't be a problem in practice
137             return $this->raiseError("Only Little-Endian encoding is supported.");
138         }
139         // Size of blocks and short blocks in bytes
140         $this->bigBlockSize   = pow(2, $this->_readInt2($fh));
141         $this->smallBlockSize = pow(2, $this->_readInt2($fh));
142
143         // Skip UID, revision number and version number
144         fseek($fh, 44);
145         // Number of blocks in Big Block Allocation Table
146         $bbatBlockCount = $this->_readInt4($fh);
147
148         // Root chain 1st block
149         $directoryFirstBlockId = $this->_readInt4($fh);
150
151         // Skip unused bytes
152         fseek($fh, 56);
153         // Streams shorter than this are stored using small blocks
154         $this->bigBlockThreshold = $this->_readInt4($fh);
155         // Block id of first sector in Short Block Allocation Table
156         $sbatFirstBlockId = $this->_readInt4($fh);
157         // Number of blocks in Short Block Allocation Table
158         $sbbatBlockCount = $this->_readInt4($fh);
159         // Block id of first sector in Master Block Allocation Table
160         $mbatFirstBlockId = $this->_readInt4($fh);
161         // Number of blocks in Master Block Allocation Table
162         $mbbatBlockCount = $this->_readInt4($fh);
163         $this->bbat = array();
164
165         // Remaining 4 * 109 bytes of current block is beginning of Master
166         // Block Allocation Table
167         $mbatBlocks = array();
168         for ($i = 0; $i < 109; $i++) {
169             $mbatBlocks[] = $this->_readInt4($fh);
170         }
171
172         // Read rest of Master Block Allocation Table (if any is left)
173         $pos = $this->_getBlockOffset($mbatFirstBlockId);
174         for ($i = 0; $i < $mbbatBlockCount; $i++) {
175             fseek($fh, $pos);
176             for ($j = 0; $j < $this->bigBlockSize / 4 - 1; $j++) {
177                 $mbatBlocks[] = $this->_readInt4($fh);
178             }
179             // Last block id in each block points to next block
180             $pos = $this->_getBlockOffset($this->_readInt4($fh));
181         }
182
183         // Read Big Block Allocation Table according to chain specified by
184         // $mbatBlocks
185         for ($i = 0; $i < $bbatBlockCount; $i++) {
186             $pos = $this->_getBlockOffset($mbatBlocks[$i]);
187             fseek($fh, $pos);
188             for ($j = 0 ; $j < $this->bigBlockSize / 4; $j++) {
189                 $this->bbat[] = $this->_readInt4($fh);
190             }
191         }
192
193         // Read short block allocation table (SBAT)
194         $this->sbat = array();
195         $shortBlockCount = $sbbatBlockCount * $this->bigBlockSize / 4;
196         $sbatFh = $this->getStream($sbatFirstBlockId);
197         if (!$sbatFh) {
198             // Avoid an infinite loop if ChainedBlockStream.php somehow is
199             // missing
200             return false;
201         }
202         for ($blockId = 0; $blockId < $shortBlockCount; $blockId++) {
203             $this->sbat[$blockId] = $this->_readInt4($sbatFh);
204         }
205         fclose($sbatFh);
206
207         $this->_readPpsWks($directoryFirstBlockId);
208
209         return true;
210     }
211
212     /**
213      * @param int $blockId block id
214      * @return int byte offset from beginning of file
215      * @access private
216      */
217     function _getBlockOffset($blockId)
218     {
219         return 512 + $blockId * $this->bigBlockSize;
220     }
221
222     /**
223      * Returns a stream for use with fread() etc. External callers should
224      * use OLE_PPS_File::getStream().
225      * @param int|PPS $blockIdOrPps block id or PPS
226      * @return resource read-only stream
227      */
228     function getStream($blockIdOrPps)
229     {
230         include_once 'OLE/ChainedBlockStream.php';
231         static $isRegistered = false;
232         if (!$isRegistered) {
233             stream_wrapper_register('ole-chainedblockstream',
234                                     'OLE_ChainedBlockStream');
235             $isRegistered = true;
236         }
237
238         // Store current instance in global array, so that it can be accessed
239         // in OLE_ChainedBlockStream::stream_open().
240         // Object is removed from self::$instances in OLE_Stream::close().
241         $GLOBALS['_OLE_INSTANCES'][] = $this;
242         $instanceId = end(array_keys($GLOBALS['_OLE_INSTANCES']));
243
244         $path = 'ole-chainedblockstream://oleInstanceId=' . $instanceId;
245         if (is_a($blockIdOrPps, 'OLE_PPS')) {
246             $path .= '&blockId=' . $blockIdOrPps->_StartBlock;
247             $path .= '&size=' . $blockIdOrPps->Size;
248         } else {
249             $path .= '&blockId=' . $blockIdOrPps;
250         }
251         return fopen($path, 'r');
252     }
253
254     /**
255      * Reads a signed char.
256      * @param resource $fh file handle
257      * @return int
258      * @access private
259      */
260     function _readInt1($fh)
261     {
262         list(, $tmp) = unpack("c", fread($fh, 1));
263         return $tmp;
264     }
265
266     /**
267      * Reads an unsigned short (2 octets).
268      * @param resource $fh file handle
269      * @return int
270      * @access private
271      */
272     function _readInt2($fh)
273     {
274         list(, $tmp) = unpack("v", fread($fh, 2));
275         return $tmp;
276     }
277
278     /**
279      * Reads an unsigned long (4 octets).
280      * @param   resource  file handle
281      * @return  int
282      * @access private
283      */
284     function _readInt4($fh)
285     {
286         list(, $tmp) = unpack("V", fread($fh, 4));
287         return $tmp;
288     }
289
290     /**
291     * Gets information about all PPS's on the OLE container from the PPS WK's
292     * creates an OLE_PPS object for each one.
293     *
294     * @access private
295     * @param integer $blockId the block id of the first block
296     * @return mixed true on success, PEAR_Error on failure
297     */
298     function _readPpsWks($blockId)
299     {
300         $fh = $this->getStream($blockId);
301         for ($pos = 0; ; $pos += 128) {
302             fseek($fh, $pos, SEEK_SET);
303             $nameUtf16 = fread($fh, 64);
304             $nameLength = $this->_readInt2($fh);
305             $nameUtf16 = substr($nameUtf16, 0, $nameLength - 2);
306             // Simple conversion from UTF-16LE to ISO-8859-1
307             $name = str_replace("\x00", "", $nameUtf16);
308             $type = $this->_readInt1($fh);
309             switch ($type) {
310             case OLE_PPS_TYPE_ROOT:
311                 require_once 'OLE/PPS/Root.php';
312                 $pps = new OLE_PPS_Root(null, null, array());
313                 $this->root = $pps;
314                 break;
315             case OLE_PPS_TYPE_DIR:
316                 $pps = new OLE_PPS(null, null, null, null, null,
317                                    null, null, null, null, array());
318                 break;
319             case OLE_PPS_TYPE_FILE:
320                 require_once 'OLE/PPS/File.php';
321                 $pps = new OLE_PPS_File($name);
322                 break;
323             default:
324                 continue 2;
325             }
326             fseek($fh, 1, SEEK_CUR);
327             $pps->Type    = $type;
328             $pps->Name    = $name;
329             $pps->PrevPps = $this->_readInt4($fh);
330             $pps->NextPps = $this->_readInt4($fh);
331             $pps->DirPps  = $this->_readInt4($fh);
332             fseek($fh, 20, SEEK_CUR);
333             $pps->Time1st = OLE::OLE2LocalDate(fread($fh, 8));
334             $pps->Time2nd = OLE::OLE2LocalDate(fread($fh, 8));
335             $pps->_StartBlock = $this->_readInt4($fh);
336             $pps->Size = $this->_readInt4($fh);
337             $pps->No = count($this->_list);
338             $this->_list[] = $pps;
339
340             // check if the PPS tree (starting from root) is complete
341             if (isset($this->root) &&
342                 $this->_ppsTreeComplete($this->root->No)) {
343
344                 break;
345             }
346         }
347         fclose($fh);
348
349         // Initialize $pps->children on directories
350         foreach ($this->_list as $pps) {
351             if ($pps->Type == OLE_PPS_TYPE_DIR || $pps->Type == OLE_PPS_TYPE_ROOT) {
352                 $nos = array($pps->DirPps);
353                 $pps->children = array();
354                 while ($nos) {
355                     $no = array_pop($nos);
356                     if ($no != -1) {
357                         $childPps = $this->_list[$no];
358                         $nos[] = $childPps->PrevPps;
359                         $nos[] = $childPps->NextPps;
360                         $pps->children[] = $childPps;
361                     }
362                 }
363             }
364         }
365
366         return true;
367     }
368
369     /**
370     * It checks whether the PPS tree is complete (all PPS's read)
371     * starting with the given PPS (not necessarily root)
372     *
373     * @access private
374     * @param integer $index The index of the PPS from which we are checking
375     * @return boolean Whether the PPS tree for the given PPS is complete
376     */
377     function _ppsTreeComplete($index)
378     {
379         return isset($this->_list[$index]) &&
380                ($pps = $this->_list[$index]) &&
381                ($pps->PrevPps == -1 ||
382                 $this->_ppsTreeComplete($pps->PrevPps)) &&
383                ($pps->NextPps == -1 ||
384                 $this->_ppsTreeComplete($pps->NextPps)) &&
385                ($pps->DirPps == -1 ||
386                 $this->_ppsTreeComplete($pps->DirPps));
387     }
388
389     /** 
390     * Checks whether a PPS is a File PPS or not.
391     * If there is no PPS for the index given, it will return false.
392     * @param integer $index The index for the PPS
393     * @return bool true if it's a File PPS, false otherwise
394     * @access public
395     */
396     function isFile($index)
397     {
398         if (isset($this->_list[$index])) {
399             return ($this->_list[$index]->Type == OLE_PPS_TYPE_FILE);
400         }
401         return false;
402     }
403
404     /** 
405     * Checks whether a PPS is a Root PPS or not.
406     * If there is no PPS for the index given, it will return false.
407     * @param integer $index The index for the PPS.
408     * @return bool true if it's a Root PPS, false otherwise
409     * @access public
410     */
411     function isRoot($index)
412     {
413         if (isset($this->_list[$index])) {
414             return ($this->_list[$index]->Type == OLE_PPS_TYPE_ROOT);
415         }
416         return false;
417     }
418
419     /** 
420     * Gives the total number of PPS's found in the OLE container.
421     * @return integer The total number of PPS's found in the OLE container
422     * @access public
423     */
424     function ppsTotal()
425     {
426         return count($this->_list);
427     }
428
429     /**
430     * Gets data from a PPS
431     * If there is no PPS for the index given, it will return an empty string.
432     * @param integer $index    The index for the PPS
433     * @param integer $position The position from which to start reading
434     *                          (relative to the PPS)
435     * @param integer $length   The amount of bytes to read (at most)
436     * @return string The binary string containing the data requested
437     * @access public
438     * @see OLE_PPS_File::getStream()
439     */
440     function getData($index, $position, $length)
441     {
442         // if position is not valid return empty string
443         if (!isset($this->_list[$index]) ||
444             $position >= $this->_list[$index]->Size ||
445             $position < 0) {
446
447             return '';
448         }
449         $fh = $this->getStream($this->_list[$index]);
450         $data = stream_get_contents($fh, $length, $position);
451         fclose($fh);
452         return $data;
453     }
454
455     /**
456     * Gets the data length from a PPS
457     * If there is no PPS for the index given, it will return 0.
458     * @param integer $index The index for the PPS
459     * @return integer The amount of bytes in data the PPS has
460     * @access public
461     */
462     function getDataLength($index)
463     {
464         if (isset($this->_list[$index])) {
465             return $this->_list[$index]->Size;
466         }
467         return 0;
468     }
469
470     /**
471     * Utility function to transform ASCII text to Unicode
472     *
473     * @access public
474     * @static
475     * @param string $ascii The ASCII string to transform
476     * @return string The string in Unicode
477     */
478     static function Asc2Ucs($ascii)
479     {
480         $rawname = '';
481         for ($i = 0; $i < strlen($ascii); $i++) {
482             $rawname .= $ascii[$i] . "\x00";
483         }
484         return $rawname;
485     }
486
487     /**
488     * Utility function
489     * Returns a string for the OLE container with the date given
490     *
491     * @access public
492     * @static
493     * @param integer $date A timestamp 
494     * @return string The string for the OLE container
495     */
496    static  function LocalDate2OLE($date = null)
497     {
498         if (!isset($date)) {
499             return "\x00\x00\x00\x00\x00\x00\x00\x00";
500         }
501
502         // factor used for separating numbers into 4 bytes parts
503         $factor = pow(2, 32);
504
505         // days from 1-1-1601 until the beggining of UNIX era
506         $days = 134774;
507         // calculate seconds
508         $big_date = $days * 24 * 3600 +
509             gmmktime(date("H",$date),date("i",$date),date("s",$date),
510                      date("m",$date),date("d",$date),date("Y",$date));
511         // multiply just to make MS happy
512         $big_date *= 10000000;
513
514         $high_part = floor($big_date / $factor);
515         // lower 4 bytes
516         $low_part = floor((($big_date / $factor) - $high_part) * $factor);
517
518         // Make HEX string
519         $res = '';
520
521         for ($i = 0; $i < 4; $i++) {
522             $hex = ((int)$low_part) % 0x100;
523             $res .= pack('c', $hex);
524             $low_part /= 0x100;
525         }
526         for ($i = 0; $i < 4; $i++) {
527             $hex = ((int)$high_part) % 0x100;
528             $res .= pack('c', $hex);
529             $high_part /= 0x100;
530         }
531         return $res;
532     }
533
534     /**
535     * Returns a timestamp from an OLE container's date
536     * @param integer $string A binary string with the encoded date
537     * @return string The timestamp corresponding to the string
538     * @access public
539     * @static
540     */
541     static function OLE2LocalDate($string)
542     {
543         if (strlen($string) != 8) {
544             return new PEAR_Error("Expecting 8 byte string");
545         }
546
547         // factor used for separating numbers into 4 bytes parts
548         $factor = pow(2,32);
549         $high_part = 0;
550         for ($i = 0; $i < 4; $i++) {
551             list(, $high_part) = unpack('C', $string[(7 - $i)]);
552             if ($i < 3) {
553                 $high_part *= 0x100;
554             }
555         }
556         $low_part = 0;
557         for ($i = 4; $i < 8; $i++) {
558             list(, $low_part) = unpack('C', $string[(7 - $i)]);
559             if ($i < 7) {
560                 $low_part *= 0x100;
561             }
562         }
563         $big_date = ($high_part * $factor) + $low_part;
564         // translate to seconds
565         $big_date /= 10000000;
566         
567         // days from 1-1-1601 until the beggining of UNIX era
568         $days = 134774;
569         
570         // translate to seconds from beggining of UNIX era
571         $big_date -= $days * 24 * 3600;
572         return floor($big_date);
573     }
574 }
575