Completed
Pull Request — master (#313)
by Thomas
18:52 queued 11:36
created

clsTbsZip::CentralDirRead_File()   D

Complexity

Conditions 2
Paths 2

Size

Total Lines 32
Code Lines 26

Duplication

Lines 1
Ratio 3.13 %

Importance

Changes 0
Metric Value
cc 2
eloc 26
nc 2
nop 1
dl 1
loc 32
rs 4.5365
c 0
b 0
f 0
1
<?php
2
3
/*
4
TbsZip version 2.16
5
Date    : 2014-04-08
6
Author  : Skrol29 (email: http://www.tinybutstrong.com/onlyyou.html)
7
Licence : LGPL
8
This class is independent from any other classes and has been originally created for the OpenTbs plug-in
9
for TinyButStrong Template Engine (TBS). OpenTbs makes TBS able to merge OpenOffice and Ms Office documents.
10
Visit http://www.tinybutstrong.com
11
*/
12
13
14
class clsTbsZip {
15
    const TBSZIP_DOWNLOAD = 1;   // download (default)
16
    const TBSZIP_NOHEADER = 4;   // option to use with DOWNLOAD: no header is sent
17
    const TBSZIP_FILE     = 8;   // output to file  , or add from file
18
    const TBSZIP_STRING   = 32;  // output to string, or add from string
19
20
    function __construct() {
21
        $this->Meth8Ok = extension_loaded('zlib'); // check if Zlib extension is available. This is need for compress and uncompress with method 8.
22
        $this->DisplayError = true;
23
        $this->ArchFile = '';
24
        $this->Error = false;
25
    }
26
27
    function CreateNew($ArchName='new.zip') {
28
    // Create a new virtual empty archive, the name will be the default name when the archive is flushed.
29
        if (!isset($this->Meth8Ok)) $this->__construct();  // for PHP 4 compatibility
30
        $this->Close(); // note that $this->ArchHnd is set to false here
31
        $this->Error = false;
32
        $this->ArchFile = $ArchName;
33
        $this->ArchIsNew = true;
34
        $bin = 'PK'.chr(05).chr(06).str_repeat(chr(0), 18);
35
        $this->CdEndPos = strlen($bin) - 4;
36
        $this->CdInfo = array('disk_num_curr'=>0, 'disk_num_cd'=>0, 'file_nbr_curr'=>0, 'file_nbr_tot'=>0, 'l_cd'=>0, 'p_cd'=>0, 'l_comm'=>0, 'v_comm'=>'', 'bin'=>$bin);
37
        $this->CdPos = $this->CdInfo['p_cd'];
38
    }
39
40
    function Open($ArchFile, $UseIncludePath=false) {
41
    // Open the zip archive
42
        if (!isset($this->Meth8Ok)) $this->__construct();  // for PHP 4 compatibility
43
        $this->Close(); // close handle and init info
44
        $this->Error = false;
45
        $this->ArchIsNew = false;
46
        $this->ArchIsStream = (is_resource($ArchFile) && (get_resource_type($ArchFile)=='stream'));
47
        if ($this->ArchIsStream) {
48
            $this->ArchFile = 'from_stream.zip';
49
            $this->ArchHnd = $ArchFile;
50
        } else {
51
            // open the file
52
            $this->ArchFile = $ArchFile;
53
            $this->ArchHnd = fopen($ArchFile, 'rb', $UseIncludePath);
54
        }
55
        $ok = !($this->ArchHnd===false);
56
        if ($ok) $ok = $this->CentralDirRead();
57
        return $ok;
58
    }
59
60
    function Close() {
61
        if (isset($this->ArchHnd) and ($this->ArchHnd!==false)) fclose($this->ArchHnd);
62
        $this->ArchFile = '';
63
        $this->ArchHnd = false;
64
        $this->CdInfo = array();
65
        $this->CdFileLst = array();
66
        $this->CdFileNbr = 0;
67
        $this->CdFileByName = array();
68
        $this->VisFileLst = array();
69
        $this->ArchCancelModif();
70
    }
71
72
    function ArchCancelModif() {
73
        $this->LastReadComp = false; // compression of the last read file (1=compressed, 0=stored not compressed, -1= stored compressed but read uncompressed)
74
        $this->LastReadIdx = false;  // index of the last file read
75
        $this->ReplInfo = array();
76
        $this->ReplByPos = array();
77
        $this->AddInfo = array();
78
    }
79
80
    function FileAdd($Name, $Data, $DataType=self::TBSZIP_STRING, $Compress=true) {
81
82
        if ($Data===false) return $this->FileCancelModif($Name, false); // Cancel a previously added file
83
84
        // Save information for adding a new file into the archive
85
        $Diff = 30 + 46 + 2*strlen($Name); // size of the header + cd info
86
        $Ref = $this->_DataCreateNewRef($Data, $DataType, $Compress, $Diff, $Name);
87
        if ($Ref===false) return false;
88
        $Ref['name'] = $Name;
89
        $this->AddInfo[] = $Ref;
90
        return $Ref['res'];
91
92
    }
93
94
    function CentralDirRead() {
95
        $cd_info = 'PK'.chr(05).chr(06); // signature of the Central Directory
96
        $cd_pos = -22;
97
        $this->_MoveTo($cd_pos, SEEK_END);
98
        $b = $this->_ReadData(4);
99
        if ($b===$cd_info) {
100
            $this->CdEndPos = ftell($this->ArchHnd) - 4;
101
        } else {
102
            $p = $this->_FindCDEnd($cd_info);
103
            //echo 'p='.var_export($p,true); exit;
104
            if ($p===false) {
105
                return $this->RaiseError('The End of Central Directory Record is not found.');
106
            } else {
107
                $this->CdEndPos = $p;
108
                $this->_MoveTo($p+4);
109
            }
110
        }
111
        $this->CdInfo = $this->CentralDirRead_End($cd_info);
112
        $this->CdFileLst = array();
113
        $this->CdFileNbr = $this->CdInfo['file_nbr_curr'];
114
        $this->CdPos = $this->CdInfo['p_cd'];
115
116
        if ($this->CdFileNbr<=0) return $this->RaiseError('No header found in the Central Directory.');
117
        if ($this->CdPos<=0) return $this->RaiseError('No position found for the Central Directory.');
118
119
        $this->_MoveTo($this->CdPos);
120
        for ($i=0;$i<$this->CdFileNbr;$i++) {
121
            $x = $this->CentralDirRead_File($i);
122
            if ($x!==false) {
123
                $this->CdFileLst[$i] = $x;
124
                $this->CdFileByName[$x['v_name']] = $i;
125
            }
126
        }
127
        return true;
128
    }
129
130
    function CentralDirRead_End($cd_info) {
131
        $b = $cd_info.$this->_ReadData(18);
132
        $x = array();
133
        $x['disk_num_curr'] = $this->_GetDec($b,4,2);  // number of this disk
134
        $x['disk_num_cd'] = $this->_GetDec($b,6,2);    // number of the disk with the start of the central directory
135
        $x['file_nbr_curr'] = $this->_GetDec($b,8,2);  // total number of entries in the central directory on this disk
136
        $x['file_nbr_tot'] = $this->_GetDec($b,10,2);  // total number of entries in the central directory
137
        $x['l_cd'] = $this->_GetDec($b,12,4);          // size of the central directory
138
        $x['p_cd'] = $this->_GetDec($b,16,4);          // position of start of central directory with respect to the starting disk number
139
        $x['l_comm'] = $this->_GetDec($b,20,2);        // .ZIP file comment length
140
        $x['v_comm'] = $this->_ReadData($x['l_comm']); // .ZIP file comment
141
        $x['bin'] = $b.$x['v_comm'];
142
        return $x;
143
    }
144
145
    function CentralDirRead_File($idx) {
146
147
        $b = $this->_ReadData(46);
148
149
        $x = $this->_GetHex($b,0,4);
150
        if ($x!=='h:02014b50') return $this->RaiseError("Signature of Central Directory Header #".$idx." (file information) expected but not found at position ".$this->_TxtPos(ftell($this->ArchHnd) - 46).".");
151
152
        $x = array();
153
        $x['vers_used'] = $this->_GetDec($b,4,2);
154
        $x['vers_necess'] = $this->_GetDec($b,6,2);
155
        $x['purp'] = $this->_GetBin($b,8,2);
156
        $x['meth'] = $this->_GetDec($b,10,2);
157
        $x['time'] = $this->_GetDec($b,12,2);
158
        $x['date'] = $this->_GetDec($b,14,2);
159
        $x['crc32'] = $this->_GetDec($b,16,4);
160
        $x['l_data_c'] = $this->_GetDec($b,20,4);
161
        $x['l_data_u'] = $this->_GetDec($b,24,4);
162
        $x['l_name'] = $this->_GetDec($b,28,2);
163
        $x['l_fields'] = $this->_GetDec($b,30,2);
164
        $x['l_comm'] = $this->_GetDec($b,32,2);
165
        $x['disk_num'] = $this->_GetDec($b,34,2);
166
        $x['int_file_att'] = $this->_GetDec($b,36,2);
167
        $x['ext_file_att'] = $this->_GetDec($b,38,4);
168
        $x['p_loc'] = $this->_GetDec($b,42,4);
169
        $x['v_name'] = $this->_ReadData($x['l_name']);
170
        $x['v_fields'] = $this->_ReadData($x['l_fields']);
171
        $x['v_comm'] = $this->_ReadData($x['l_comm']);
172
173
        $x['bin'] = $b.$x['v_name'].$x['v_fields'].$x['v_comm'];
174
175
        return $x;
176
    }
177
178
    function RaiseError($Msg) {
179
        if ($this->DisplayError) {
180
            if (PHP_SAPI==='cli') {
181
                echo get_class($this).' ERROR with the zip archive: '.$Msg."\r\n";
182
            } else {
183
                echo '<strong>'.get_class($this).' ERROR with the zip archive:</strong> '.$Msg.'<br>'."\r\n";
184
            }
185
        }
186
        $this->Error = $Msg;
187
        return false;
188
    }
189
190
    function Debug($FileHeaders=false) {
191
192
        $this->DisplayError = true;
193
194
        if ($FileHeaders) {
195
            // Calculations first in order to have error messages before other information
196
            $idx = 0;
197
            $pos = 0;
198
            $pos_stop = $this->CdInfo['p_cd'];
199
            $this->_MoveTo($pos);
200
            while ( ($pos<$pos_stop) && ($ok = $this->_ReadFile($idx,false)) ) {
201
                $this->VisFileLst[$idx]['p_this_header (debug_mode only)'] = $pos;
202
                $pos = ftell($this->ArchHnd);
203
                $idx++;
204
            }
205
        }
206
207
        $nl = "\r\n";
208
        echo "<pre>";
209
210
        echo "-------------------------------".$nl;
211
        echo "End of Central Directory record".$nl;
212
        echo "-------------------------------".$nl;
213
        print_r($this->DebugArray($this->CdInfo));
214
215
        echo $nl;
216
        echo "-------------------------".$nl;
217
        echo "Central Directory headers".$nl;
218
        echo "-------------------------".$nl;
219
        print_r($this->DebugArray($this->CdFileLst));
220
221
        if ($FileHeaders) {
222
            echo $nl;
223
            echo "------------------".$nl;
224
            echo "Local File headers".$nl;
225
            echo "------------------".$nl;
226
            print_r($this->DebugArray($this->VisFileLst));
227
        }
228
229
        echo "</pre>";
230
231
    }
232
233
    function DebugArray($arr) {
234
        foreach ($arr as $k=>$v) {
235
            if (is_array($v)) {
236
                $arr[$k] = $this->DebugArray($v);
237
            } elseif (substr($k,0,2)=='p_') {
238
                $arr[$k] = $this->_TxtPos($v);
239
            }
240
        }
241
        return $arr;
242
    }
243
244
    function FileExists($NameOrIdx) {
245
        return ($this->FileGetIdx($NameOrIdx)!==false);
246
    }
247
248
    function FileGetIdx($NameOrIdx) {
249
    // Check if a file name, or a file index exists in the Central Directory, and return its index
250
        if (is_string($NameOrIdx)) {
251
            if (isset($this->CdFileByName[$NameOrIdx])) {
252
                return $this->CdFileByName[$NameOrIdx];
253
            } else {
254
                return false;
255
            }
256
        } else {
257
            if (isset($this->CdFileLst[$NameOrIdx])) {
258
                return $NameOrIdx;
259
            } else {
260
                return false;
261
            }
262
        }
263
    }
264
265
    function FileGetIdxAdd($Name) {
266
    // Check if a file name exists in the list of file to add, and return its index
267
        if (!is_string($Name)) return false;
268
        $idx_lst = array_keys($this->AddInfo);
269
        foreach ($idx_lst as $idx) {
270
            if ($this->AddInfo[$idx]['name']===$Name) return $idx;
271
        }
272
        return false;
273
    }
274
275
    function FileRead($NameOrIdx, $Uncompress=true) {
276
277
        $this->LastReadComp = false; // means the file is not found
278
        $this->LastReadIdx = false;
279
280
        $idx = $this->FileGetIdx($NameOrIdx);
281
        if ($idx===false) return $this->RaiseError('File "'.$NameOrIdx.'" is not found in the Central Directory.');
282
283
        $pos = $this->CdFileLst[$idx]['p_loc'];
284
        $this->_MoveTo($pos);
285
286
        $this->LastReadIdx = $idx; // Can be usefull to get the idx
287
288
        $Data = $this->_ReadFile($idx, true);
289
290
        // Manage uncompression
291
        $Comp = 1; // means the contents stays compressed
292
        $meth = $this->CdFileLst[$idx]['meth'];
293
        if ($meth==8) {
294
            if ($Uncompress) {
295
                if ($this->Meth8Ok) {
296
                    $Data = gzinflate($Data);
297
                    $Comp = -1; // means uncompressed
298
                } else {
299
                    $this->RaiseError('Unable to uncompress file "'.$NameOrIdx.'" because extension Zlib is not installed.');
300
                }
301
            }
302
        } elseif($meth==0) {
303
            $Comp = 0; // means stored without compression
304
        } else {
305
            if ($Uncompress) $this->RaiseError('Unable to uncompress file "'.$NameOrIdx.'" because it is compressed with method '.$meth.'.');
306
        }
307
        $this->LastReadComp = $Comp;
308
309
        return $Data;
310
311
    }
312
313
    function _ReadFile($idx, $ReadData) {
314
    // read the file header (and maybe the data ) in the archive, assuming the cursor in at a new file position
315
316
        $b = $this->_ReadData(30);
317
318
        $x = $this->_GetHex($b,0,4);
319
        if ($x!=='h:04034b50') return $this->RaiseError("Signature of Local File Header #".$idx." (data section) expected but not found at position ".$this->_TxtPos(ftell($this->ArchHnd)-30).".");
320
321
        $x = array();
322
        $x['vers'] = $this->_GetDec($b,4,2);
323
        $x['purp'] = $this->_GetBin($b,6,2);
324
        $x['meth'] = $this->_GetDec($b,8,2);
325
        $x['time'] = $this->_GetDec($b,10,2);
326
        $x['date'] = $this->_GetDec($b,12,2);
327
        $x['crc32'] = $this->_GetDec($b,14,4);
328
        $x['l_data_c'] = $this->_GetDec($b,18,4);
329
        $x['l_data_u'] = $this->_GetDec($b,22,4);
330
        $x['l_name'] = $this->_GetDec($b,26,2);
331
        $x['l_fields'] = $this->_GetDec($b,28,2);
332
        $x['v_name'] = $this->_ReadData($x['l_name']);
333
        $x['v_fields'] = $this->_ReadData($x['l_fields']);
334
335
        $x['bin'] = $b.$x['v_name'].$x['v_fields'];
336
337
        // Read Data
338
        if (isset($this->CdFileLst[$idx])) {
339
            $len_cd = $this->CdFileLst[$idx]['l_data_c'];
340
            if ($x['l_data_c']==0) {
341
                // Sometimes, the size is not specified in the local information.
342
                $len = $len_cd;
343
            } else {
344
                $len = $x['l_data_c'];
345
                if ($len!=$len_cd) {
346
                    //echo "TbsZip Warning: Local information for file #".$idx." says len=".$len.", while Central Directory says len=".$len_cd.".";
347
                }
348
            }
349
        } else {
350
            $len = $x['l_data_c'];
351
            if ($len==0) $this->RaiseError("File Data #".$idx." cannt be read because no length is specified in the Local File Header and its Central Directory information has not been found.");
352
        }
353
354
        if ($ReadData) {
355
            $Data = $this->_ReadData($len);
356
        } else {
357
            $this->_MoveTo($len, SEEK_CUR);
358
        }
359
360
        // Description information
361
        $desc_ok = ($x['purp'][2+3]=='1');
362
        if ($desc_ok) {
363
            $b = $this->_ReadData(12);
364
            $s = $this->_GetHex($b,0,4);
365
            $d = 0;
366
            // the specification says the signature may or may not be present
367
            if ($s=='h:08074b50') {
368
                $b .= $this->_ReadData(4);
369
                $d = 4;
370
                $x['desc_bin'] = $b;
371
                $x['desc_sign'] = $s;
372
            } else {
373
                $x['desc_bin'] = $b;
374
            }
375
            $x['desc_crc32']    = $this->_GetDec($b,0+$d,4);
376
            $x['desc_l_data_c'] = $this->_GetDec($b,4+$d,4);
377
            $x['desc_l_data_u'] = $this->_GetDec($b,8+$d,4);
378
        }
379
380
        // Save file info without the data
381
        $this->VisFileLst[$idx] = $x;
382
383
        // Return the info
384
        if ($ReadData) {
385
            return $Data;
386
        } else {
387
            return true;
388
        }
389
390
    }
391
392
    function FileReplace($NameOrIdx, $Data, $DataType=self::TBSZIP_STRING, $Compress=true) {
393
    // Store replacement information.
394
395
        $idx = $this->FileGetIdx($NameOrIdx);
396
        if ($idx===false) return $this->RaiseError('File "'.$NameOrIdx.'" is not found in the Central Directory.');
397
398
        $pos = $this->CdFileLst[$idx]['p_loc'];
399
400
        if ($Data===false) {
401
            // file to delete
402
            $this->ReplInfo[$idx] = false;
403
            $Result = true;
404
        } else {
405
            // file to replace
406
            $Diff = - $this->CdFileLst[$idx]['l_data_c'];
407
            $Ref = $this->_DataCreateNewRef($Data, $DataType, $Compress, $Diff, $NameOrIdx);
408
            if ($Ref===false) return false;
409
            $this->ReplInfo[$idx] = $Ref;
410
            $Result = $Ref['res'];
411
        }
412
413
        $this->ReplByPos[$pos] = $idx;
414
415
        return $Result;
416
417
    }
418
419
    /**
420
     * Return the state of the file.
421
     * @return {string} 'u'=unchanged, 'm'=modified, 'd'=deleted, 'a'=added, false=unknown
422
     */
423
    function FileGetState($NameOrIdx) {
424
425
        $idx = $this->FileGetIdx($NameOrIdx);
426
        if ($idx===false) {
427
            $idx = $this->FileGetIdxAdd($NameOrIdx);
428
            if ($idx===false) {
429
                return false;
430
            } else {
431
                return 'a';
432
            }
433
        } elseif (isset($this->ReplInfo[$idx])) {
434
            if ($this->ReplInfo[$idx]===false) {
435
                return 'd';
436
            } else {
437
                return 'm';
438
            }
439
        } else {
440
            return 'u';
441
        }
442
443
    }
444
445
    function FileCancelModif($NameOrIdx, $ReplacedAndDeleted=true) {
446
    // cancel added, modified or deleted modifications on a file in the archive
447
    // return the number of cancels
448
449
        $nbr = 0;
450
451
        if ($ReplacedAndDeleted) {
452
            // replaced or deleted files
453
            $idx = $this->FileGetIdx($NameOrIdx);
454
            if ($idx!==false) {
455
                if (isset($this->ReplInfo[$idx])) {
456
                    $pos = $this->CdFileLst[$idx]['p_loc'];
457
                    unset($this->ReplByPos[$pos]);
458
                    unset($this->ReplInfo[$idx]);
459
                    $nbr++;
460
                }
461
            }
462
        }
463
464
        // added files
465
        $idx = $this->FileGetIdxAdd($NameOrIdx);
466
        if ($idx!==false) {
467
            unset($this->AddInfo[$idx]);
468
            $nbr++;
469
        }
470
471
        return $nbr;
472
473
    }
474
475
    function Flush($Render=self::TBSZIP_DOWNLOAD, $File='', $ContentType='') {
476
477
        if ( ($File!=='') && ($this->ArchFile===$File) && ($Render==self::TBSZIP_FILE) ) {
478
            $this->RaiseError('Method Flush() cannot overwrite the current opened archive: \''.$File.'\''); // this makes corrupted zip archives without PHP error.
479
            return false;
480
        }
481
482
        $ArchPos = 0;
483
        $Delta = 0;
484
        $FicNewPos = array();
485
        $DelLst = array(); // idx of deleted files
486
        $DeltaCdLen = 0; // delta of the CD's size
487
488
        $now = time();
489
        $date  = $this->_MsDos_Date($now);
490
        $time  = $this->_MsDos_Time($now);
491
492
        if (!$this->OutputOpen($Render, $File, $ContentType)) return false;
493
494
        // output modified zipped files and unmodified zipped files that are beetween them
495
        ksort($this->ReplByPos);
496
        foreach ($this->ReplByPos as $ReplPos => $ReplIdx) {
497
            // output data from the zip archive which is before the data to replace
498
            $this->OutputFromArch($ArchPos, $ReplPos);
499
            // get current file information
500
            if (!isset($this->VisFileLst[$ReplIdx])) $this->_ReadFile($ReplIdx, false);
501
            $FileInfo =& $this->VisFileLst[$ReplIdx];
502
            $b1 = $FileInfo['bin'];
503
            if (isset($FileInfo['desc_bin'])) {
504
                $b2 = $FileInfo['desc_bin'];
505
            } else {
506
                $b2 = '';
507
            }
508
            $info_old_len = strlen($b1) + $this->CdFileLst[$ReplIdx]['l_data_c'] + strlen($b2); // $FileInfo['l_data_c'] may have a 0 value in some archives
509
            // get replacement information
510
            $ReplInfo =& $this->ReplInfo[$ReplIdx];
511
            if ($ReplInfo===false) {
512
                // The file is to be deleted
513
                $Delta = $Delta - $info_old_len; // headers and footers are also deleted
514
                $DelLst[$ReplIdx] = true;
515
            } else {
516
                // prepare the header of the current file
517
                $this->_DataPrepare($ReplInfo); // get data from external file if necessary
518
                $this->_PutDec($b1, $time, 10, 2); // time
519
                $this->_PutDec($b1, $date, 12, 2); // date
520
                $this->_PutDec($b1, $ReplInfo['crc32'], 14, 4); // crc32
521
                $this->_PutDec($b1, $ReplInfo['len_c'], 18, 4); // l_data_c
522
                $this->_PutDec($b1, $ReplInfo['len_u'], 22, 4); // l_data_u
523
                if ($ReplInfo['meth']!==false) $this->_PutDec($b1, $ReplInfo['meth'], 8, 2); // meth
524
                // prepare the bottom description if the zipped file, if any
525
                if ($b2!=='') {
526
                    $d = (strlen($b2)==16) ? 4 : 0; // offset because of the signature if any
527
                    $this->_PutDec($b2, $ReplInfo['crc32'], $d+0, 4); // crc32
528
                    $this->_PutDec($b2, $ReplInfo['len_c'], $d+4, 4); // l_data_c
529
                    $this->_PutDec($b2, $ReplInfo['len_u'], $d+8, 4); // l_data_u
530
                }
531
                // output data
532
                $this->OutputFromString($b1.$ReplInfo['data'].$b2);
533
                unset($ReplInfo['data']); // save PHP memory
534
                $Delta = $Delta + $ReplInfo['diff'] + $ReplInfo['len_c'];
535
            }
536
            // Update the delta of positions for zipped files which are physically after the currently replaced one
537
            for ($i=0;$i<$this->CdFileNbr;$i++) {
538
                if ($this->CdFileLst[$i]['p_loc']>$ReplPos) {
539
                    $FicNewPos[$i] = $this->CdFileLst[$i]['p_loc'] + $Delta;
540
                }
541
            }
542
            // Update the current pos in the archive
543
            $ArchPos = $ReplPos + $info_old_len;
544
        }
545
546
        // Ouput all the zipped files that remain before the Central Directory listing
547
        if ($this->ArchHnd!==false) $this->OutputFromArch($ArchPos, $this->CdPos); // ArchHnd is false if CreateNew() has been called
548
        $ArchPos = $this->CdPos;
549
550
        // Output file to add
551
        $AddNbr = count($this->AddInfo);
552
        $AddDataLen = 0; // total len of added data (inlcuding file headers)
553
        if ($AddNbr>0) {
554
            $AddPos = $ArchPos + $Delta; // position of the start
555
            $AddLst = array_keys($this->AddInfo);
556
            foreach ($AddLst as $idx) {
557
                $n = $this->_DataOuputAddedFile($idx, $AddPos);
558
                $AddPos += $n;
559
                $AddDataLen += $n;
560
            }
561
        }
562
563
        // Modifiy file information in the Central Directory for replaced files
564
        $b2 = '';
565
        $old_cd_len = 0;
566
        for ($i=0;$i<$this->CdFileNbr;$i++) {
567
            $b1 = $this->CdFileLst[$i]['bin'];
568
            $old_cd_len += strlen($b1);
569
            if (!isset($DelLst[$i])) {
570
                if (isset($FicNewPos[$i])) $this->_PutDec($b1, $FicNewPos[$i], 42, 4);   // p_loc
571
                if (isset($this->ReplInfo[$i])) {
572
                    $ReplInfo =& $this->ReplInfo[$i];
573
                    $this->_PutDec($b1, $time, 12, 2); // time
574
                    $this->_PutDec($b1, $date, 14, 2); // date
575
                    $this->_PutDec($b1, $ReplInfo['crc32'], 16, 4); // crc32
576
                    $this->_PutDec($b1, $ReplInfo['len_c'], 20, 4); // l_data_c
577
                    $this->_PutDec($b1, $ReplInfo['len_u'], 24, 4); // l_data_u
578
                    if ($ReplInfo['meth']!==false) $this->_PutDec($b1, $ReplInfo['meth'], 10, 2); // meth
579
                }
580
                $b2 .= $b1;
581
            }
582
        }
583
        $this->OutputFromString($b2);
584
        $ArchPos += $old_cd_len;
585
        $DeltaCdLen =  $DeltaCdLen + strlen($b2) - $old_cd_len;
586
587
        // Output until "end of central directory record"
588
        if ($this->ArchHnd!==false) $this->OutputFromArch($ArchPos, $this->CdEndPos); // ArchHnd is false if CreateNew() has been called
589
590
        // Output file information of the Central Directory for added files
591
        if ($AddNbr>0) {
592
            $b2 = '';
593
            foreach ($AddLst as $idx) {
594
                $b2 .= $this->AddInfo[$idx]['bin'];
595
            }
596
            $this->OutputFromString($b2);
597
            $DeltaCdLen += strlen($b2);
598
        }
599
600
        // Output "end of central directory record"
601
        $b2 = $this->CdInfo['bin'];
602
        $DelNbr = count($DelLst);
603
        if ( ($AddNbr>0) or ($DelNbr>0) ) {
604
            // total number of entries in the central directory on this disk
605
            $n = $this->_GetDec($b2, 8, 2);
606
            $this->_PutDec($b2, $n + $AddNbr - $DelNbr,  8, 2);
607
            // total number of entries in the central directory
608
            $n = $this->_GetDec($b2, 10, 2);
609
            $this->_PutDec($b2, $n + $AddNbr - $DelNbr, 10, 2);
610
            // size of the central directory
611
            $n = $this->_GetDec($b2, 12, 4);
612
            $this->_PutDec($b2, $n + $DeltaCdLen, 12, 4);
613
            $Delta = $Delta + $AddDataLen;
614
        }
615
        $this->_PutDec($b2, $this->CdPos+$Delta , 16, 4); // p_cd  (offset of start of central directory with respect to the starting disk number)
616
        $this->OutputFromString($b2);
617
618
        $this->OutputClose();
619
620
        return true;
621
622
    }
623
624
    // ----------------
625
    // output functions
626
    // ----------------
627
628
    function OutputOpen($Render, $File, $ContentType) {
629
630
        if (($Render & self::TBSZIP_FILE)==self::TBSZIP_FILE) {
631
            $this->OutputMode = self::TBSZIP_FILE;
632
            if (''.$File=='') $File = basename($this->ArchFile).'.zip';
633
            $this->OutputHandle = @fopen($File, 'w');
634
            if ($this->OutputHandle===false) {
635
                return $this->RaiseError('Method Flush() cannot overwrite the target file \''.$File.'\'. This may not be a valid file path or the file may be locked by another process or because of a denied permission.');
636
            }
637
        } elseif (($Render & self::TBSZIP_STRING)==self::TBSZIP_STRING) {
638
            $this->OutputMode = self::TBSZIP_STRING;
639
            $this->OutputSrc = '';
640
        } elseif (($Render & self::TBSZIP_DOWNLOAD)==self::TBSZIP_DOWNLOAD) {
641
            $this->OutputMode = self::TBSZIP_DOWNLOAD;
642
            // Output the file
643
            if (''.$File=='') $File = basename($this->ArchFile);
644
            if (($Render & self::TBSZIP_NOHEADER)==self::TBSZIP_NOHEADER) {
645
            } else {
646
                header ('Pragma: no-cache');
647
                if ($ContentType!='') header ('Content-Type: '.$ContentType);
648
                header('Content-Disposition: attachment; filename="'.$File.'"');
649
                header('Expires: 0');
650
                header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
651
                header('Cache-Control: public');
652
                header('Content-Description: File Transfer');
653
                header('Content-Transfer-Encoding: binary');
654
                $Len = $this->_EstimateNewArchSize();
655
                if ($Len!==false) header('Content-Length: '.$Len);
656
            }
657
        } else {
658
            return $this->RaiseError('Method Flush is called with a unsupported render option.');
659
        }
660
661
        return true;
662
663
    }
664
665
    function OutputFromArch($pos, $pos_stop) {
666
        $len = $pos_stop - $pos;
667
        if ($len<0) return;
668
        $this->_MoveTo($pos);
669
        $block = 1024;
670
        while ($len>0) {
671
            $l = min($len, $block);
672
            $x = $this->_ReadData($l);
673
            $this->OutputFromString($x);
674
            $len = $len - $l;
675
        }
676
        unset($x);
677
    }
678
679
    function OutputFromString($data) {
680
        if ($this->OutputMode===self::TBSZIP_DOWNLOAD) {
681
            echo $data; // donwload
682
        } elseif ($this->OutputMode===self::TBSZIP_STRING) {
683
            $this->OutputSrc .= $data; // to string
684
        } elseif (self::TBSZIP_FILE) {
685
            fwrite($this->OutputHandle, $data); // to file
686
        }
687
    }
688
689
    function OutputClose() {
690
        if ( ($this->OutputMode===self::TBSZIP_FILE) && ($this->OutputHandle!==false) ) {
691
            fclose($this->OutputHandle);
692
            $this->OutputHandle = false;
693
        }
694
    }
695
696
    // ----------------
697
    // Reading functions
698
    // ----------------
699
700
    function _MoveTo($pos, $relative = SEEK_SET) {
701
        fseek($this->ArchHnd, $pos, $relative);
702
    }
703
704
    function _ReadData($len) {
705
        if ($len>0) {
706
            $x = fread($this->ArchHnd, $len);
707
            return $x;
708
        } else {
709
            return '';
710
        }
711
    }
712
713
    // ----------------
714
    // Take info from binary data
715
    // ----------------
716
717
    function _GetDec($txt, $pos, $len) {
718
        $x = substr($txt, $pos, $len);
719
        $z = 0;
720
        for ($i=0;$i<$len;$i++) {
721
            $asc = ord($x[$i]);
722
            if ($asc>0) $z = $z + $asc*pow(256,$i);
723
        }
724
        return $z;
725
    }
726
727
    function _GetHex($txt, $pos, $len) {
728
        $x = substr($txt, $pos, $len);
729
        return 'h:'.bin2hex(strrev($x));
730
    }
731
732
    function _GetBin($txt, $pos, $len) {
733
        $x = substr($txt, $pos, $len);
734
        $z = '';
735
        for ($i=0;$i<$len;$i++) {
736
            $asc = ord($x[$i]);
737
            if (isset($x[$i])) {
738
                for ($j=0;$j<8;$j++) {
739
                    $z .= ($asc & pow(2,$j)) ? '1' : '0';
740
                }
741
            } else {
742
                $z .= '00000000';
743
            }
744
        }
745
        return 'b:'.$z;
746
    }
747
748
    // ----------------
749
    // Put info into binary data
750
    // ----------------
751
752
    function _PutDec(&$txt, $val, $pos, $len) {
753
        $x = '';
754
        for ($i=0;$i<$len;$i++) {
755
            if ($val==0) {
756
                $z = 0;
757
            } else {
758
                $z = intval($val % 256);
759
                if (($val<0) && ($z!=0)) { // ($z!=0) is very important, example: val=-420085702
760
                    // special opration for negative value. If the number id too big, PHP stores it into a signed integer. For example: crc32('coucou') => -256185401 instead of  4038781895. NegVal = BigVal - (MaxVal+1) = BigVal - 256^4
761
                    $val = ($val - $z)/256 -1;
762
                    $z = 256 + $z;
763
                } else {
764
                    $val = ($val - $z)/256;
765
                }
766
            }
767
            $x .= chr($z);
768
        }
769
        $txt = substr_replace($txt, $x, $pos, $len);
770
    }
771
772
    function _MsDos_Date($Timestamp = false) {
773
        // convert a date-time timstamp into the MS-Dos format
774
        $d = ($Timestamp===false) ? getdate() : getdate($Timestamp);
775
        return (($d['year']-1980)*512) + ($d['mon']*32) + $d['mday'];
776
    }
777
    function _MsDos_Time($Timestamp = false) {
778
        // convert a date-time timstamp into the MS-Dos format
779
        $d = ($Timestamp===false) ? getdate() : getdate($Timestamp);
780
        return ($d['hours']*2048) + ($d['minutes']*32) + intval($d['seconds']/2); // seconds are rounded to an even number in order to save 1 bit
781
    }
782
783
    function _MsDos_Debug($date, $time) {
784
        // Display the formated date and time. Just for debug purpose.
785
        // date end time are encoded on 16 bits (2 bytes) : date = yyyyyyymmmmddddd , time = hhhhhnnnnnssssss
786
        $y = ($date & 65024)/512 + 1980;
787
        $m = ($date & 480)/32;
788
        $d = ($date & 31);
789
        $h = ($time & 63488)/2048;
790
        $i = ($time & 1984)/32;
791
        $s = ($time & 31) * 2; // seconds have been rounded to an even number in order to save 1 bit
792
        return $y.'-'.str_pad($m,2,'0',STR_PAD_LEFT).'-'.str_pad($d,2,'0',STR_PAD_LEFT).' '.str_pad($h,2,'0',STR_PAD_LEFT).':'.str_pad($i,2,'0',STR_PAD_LEFT).':'.str_pad($s,2,'0',STR_PAD_LEFT);
793
    }
794
795
    function _TxtPos($pos) {
796
        // Return the human readable position in both decimal and hexa
797
        return $pos." (h:".dechex($pos).")";
798
    }
799
800
    /**
801
     * Search the record of end of the Central Directory.
802
     * Return the position of the record in the file.
803
     * Return false if the record is not found. The comment cannot exceed 65335 bytes (=FFFF).
804
     * The method is read backwards a block of 256 bytes and search the key in this block.
805
     */
806
    function _FindCDEnd($cd_info) {
807
        $nbr = 1;
808
        $p = false;
809
        $pos = ftell($this->ArchHnd) - 4 - 256;
810
        while ( ($p===false) && ($nbr<256) ) {
811
            if ($pos<=0) {
812
                $pos = 0;
813
                $nbr = 256; // in order to make this a last check
814
            }
815
            $this->_MoveTo($pos);
816
            $x = $this->_ReadData(256);
817
            $p = strpos($x, $cd_info);
818
            if ($p===false) {
819
                $nbr++;
820
                $pos = $pos - 256 - 256;
821
            } else {
822
                return $pos + $p;
823
            }
824
        }
825
        return false;
826
    }
827
828
    function _DataOuputAddedFile($Idx, $PosLoc) {
829
830
        $Ref =& $this->AddInfo[$Idx];
831
        $this->_DataPrepare($Ref); // get data from external file if necessary
832
833
        // Other info
834
        $file_time = $Ref['file_time'] !== false ? $Ref['file_time'] : time();
835
        $date  = $this->_MsDos_Date($file_time);
836
        $time  = $this->_MsDos_Time($file_time);
837
        $len_n = strlen($Ref['name']);
838
        $purp  = 2048 ; // purpose // +8 to indicates that there is an extended local header
839
840
        // Header for file in the data section
841
        $b = 'PK'.chr(03).chr(04).str_repeat(' ',26); // signature
842
        $this->_PutDec($b,20,4,2); //vers = 20
843
        $this->_PutDec($b,$purp,6,2); // purp
844
        $this->_PutDec($b,$Ref['meth'],8,2);  // meth
845
        $this->_PutDec($b,$time,10,2); // time
846
        $this->_PutDec($b,$date,12,2); // date
847
        $this->_PutDec($b,$Ref['crc32'],14,4); // crc32
848
        $this->_PutDec($b,$Ref['len_c'],18,4); // l_data_c
849
        $this->_PutDec($b,$Ref['len_u'],22,4); // l_data_u
850
        $this->_PutDec($b,$len_n,26,2); // l_name
851
        $this->_PutDec($b,0,28,2); // l_fields
852
        $b .= $Ref['name']; // name
853
        $b .= ''; // fields
854
855
        // Output the data
856
        $this->OutputFromString($b.$Ref['data']);
857
        $OutputLen = strlen($b) + $Ref['len_c']; // new position of the cursor
858
        unset($Ref['data']); // save PHP memory
859
860
        // Information for file in the Central Directory
861
        $b = 'PK'.chr(01).chr(02).str_repeat(' ',42); // signature
862
        $this->_PutDec($b,20,4,2);  // vers_used = 20
863
        $this->_PutDec($b,20,6,2);  // vers_necess = 20
864
        $this->_PutDec($b,$purp,8,2);  // purp
865
        $this->_PutDec($b,$Ref['meth'],10,2); // meth
866
        $this->_PutDec($b,$time,12,2); // time
867
        $this->_PutDec($b,$date,14,2); // date
868
        $this->_PutDec($b,$Ref['crc32'],16,4); // crc32
869
        $this->_PutDec($b,$Ref['len_c'],20,4); // l_data_c
870
        $this->_PutDec($b,$Ref['len_u'],24,4); // l_data_u
871
        $this->_PutDec($b,$len_n,28,2); // l_name
872
        $this->_PutDec($b,0,30,2); // l_fields
873
        $this->_PutDec($b,0,32,2); // l_comm
874
        $this->_PutDec($b,0,34,2); // disk_num
875
        $this->_PutDec($b,0,36,2); // int_file_att
876
        $this->_PutDec($b,0,38,4); // ext_file_att
877
        $this->_PutDec($b,$PosLoc,42,4); // p_loc
878
        $b .= $Ref['name']; // v_name
879
        $b .= ''; // v_fields
880
        $b .= ''; // v_comm
881
882
        $Ref['bin'] = $b;
883
884
        return $OutputLen;
885
886
    }
887
888
    function _DataCreateNewRef($Data, $DataType, $Compress, $Diff, $NameOrIdx) {
889
890
        $file_time = false;
891
        if (is_array($Compress)) {
892
            $result = 2;
893
            $meth = $Compress['meth'];
894
            $len_u = $Compress['len_u'];
895
            $crc32 = $Compress['crc32'];
896
            $Compress = false;
897
        } elseif ($Compress and ($this->Meth8Ok)) {
898
            $result = 1;
899
            $meth = 8;
900
            $len_u = false; // means unknown
901
            $crc32 = false;
902
        } else {
903
            $result = ($Compress) ? -1 : 0;
904
            $meth = 0;
905
            $len_u = false;
906
            $crc32 = false;
907
            $Compress = false;
908
        }
909
910
        // TODO support for data taken from okapi cache
911
        if ($DataType==self::TBSZIP_STRING) {
912
            $path = false;
913
            if ($Compress) {
914
                // we compress now in order to save PHP memory
915
                $len_u = strlen($Data);
916
                $crc32 = crc32($Data);
917
                $Data = gzdeflate($Data);
918
                $len_c = strlen($Data);
919
            } else {
920
                $len_c = strlen($Data);
921
                if ($len_u===false) {
922
                    $len_u = $len_c;
923
                    $crc32 = crc32($Data);
924
                }
925
            }
926
        } else {
927
            $path = $Data;
928
            $Data = false;
929
            $fi = stat($path);
930
            if ($fi !== false) {
931
                $fz = $fi['size'];
932
                if ($len_u===false) $len_u = $fz;
933
                $len_c = ($Compress) ? false : $fz;
934
                $file_time = $fi['mtime'];
935
            } else {
936
                return $this->RaiseError("Cannot add the file '".$path."' because it is not found.");
937
            }
938
        }
939
940
        // at this step $Data and $crc32 can be false only in case of external file, and $len_c is false only in case of external file to compress
941
        return array('data'=>$Data, 'path'=>$path, 'meth'=>$meth, 'len_u'=>$len_u, 'len_c'=>$len_c,
942
                'crc32'=>$crc32, 'diff'=>$Diff, 'res'=>$result, 'file_time'=>$file_time);
943
944
    }
945
946
    function _DataPrepare(&$Ref) {
947
        // returns the real size of data
948
        // TODO: support for data returned from okapi cache
949
        if ($Ref['path']!==false) {
950
            $Ref['data'] = file_get_contents($Ref['path']);
951
            if ($Ref['crc32']===false) $Ref['crc32'] = crc32($Ref['data']);
952
            if ($Ref['len_c']===false) {
953
                // means the data must be compressed
954
                $Ref['data'] = gzdeflate($Ref['data']);
955
                $Ref['len_c'] = strlen($Ref['data']);
956
            }
957
        }
958
    }
959
    /**
960
     * Return the size of the new archive, or false if it cannot be calculated (because of external file that must be compressed before to be insered)
961
     */
962
    function _EstimateNewArchSize($Optim=true) {
963
964
        if ($this->ArchIsNew) {
965
            $Len = strlen($this->CdInfo['bin']);
966
        } elseif ($this->ArchIsStream) {
967
            $x = fstat($this->ArchHnd);
968
            $Len = $x['size'];
969
        } else {
970
            $Len = filesize($this->ArchFile);
971
        }
972
973
        // files to replace or delete
974
        foreach ($this->ReplByPos as $i) {
975
            $Ref =& $this->ReplInfo[$i];
976
            if ($Ref===false) {
977
                // file to delete
978
                $Info =& $this->CdFileLst[$i];
979
                if (!isset($this->VisFileLst[$i])) {
980
                    if ($Optim) return false; // if $Optimization is set to true, then we d'ont rewind to read information
981
                    $this->_MoveTo($Info['p_loc']);
982
                    $this->_ReadFile($i, false);
983
                }
984
                $Vis =& $this->VisFileLst[$i];
985
                $Len += -strlen($Vis['bin']) -strlen($Info['bin']) - $Info['l_data_c'];
986
                if (isset($Vis['desc_bin'])) $Len += -strlen($Vis['desc_bin']);
987
            } elseif ($Ref['len_c']===false) {
988
                return false; // information not yet known
989
            } else {
990
                // file to replace
991
                $Len += $Ref['len_c'] + $Ref['diff'];
992
            }
993
        }
994
995
        // files to add
996
        $i_lst = array_keys($this->AddInfo);
997
        foreach ($i_lst as $i) {
998
            $Ref =& $this->AddInfo[$i];
999
            if ($Ref['len_c']===false) {
1000
                return false; // information not yet known
1001
            } else {
1002
                $Len += $Ref['len_c'] + $Ref['diff'];
1003
            }
1004
        }
1005
1006
        return $Len;
1007
1008
    }
1009
1010
}