Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Implementation of zip file archive.
19
 *
20
 * @package   core_files
21
 * @copyright 2008 Petr Skoda (http://skodak.org)
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
defined('MOODLE_INTERNAL') || die();
26
 
27
require_once("$CFG->libdir/filestorage/file_archive.php");
28
 
29
/**
30
 * Zip file archive class.
31
 *
32
 * @package   core_files
33
 * @category  files
34
 * @copyright 2008 Petr Skoda (http://skodak.org)
35
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36
 */
37
class zip_archive extends file_archive {
38
 
39
    /** @var string Pathname of archive */
40
    protected $archivepathname = null;
41
 
42
    /** @var int archive open mode */
43
    protected $mode = null;
44
 
45
    /** @var int Used memory tracking */
46
    protected $usedmem = 0;
47
 
48
    /** @var int Iteration position */
49
    protected $pos = 0;
50
 
51
    /** @var ZipArchive instance */
52
    protected $za;
53
 
54
    /** @var bool was this archive modified? */
55
    protected $modified = false;
56
 
57
    /** @var array unicode decoding array, created by decoding zip file */
58
    protected $namelookup = null;
59
 
60
    /** @var string base64 encoded contents of empty zip file */
61
    protected static $emptyzipcontent = 'UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA==';
62
 
63
    /** @var bool ugly hack for broken empty zip handling in < PHP 5.3.10 */
64
    protected $emptyziphack = false;
65
 
66
    /**
67
     * Create new zip_archive instance.
68
     */
69
    public function __construct() {
70
        $this->encoding = null; // Autodetects encoding by default.
71
    }
72
 
73
    /**
74
     * Open or create archive (depending on $mode).
75
     *
76
     * @todo MDL-31048 return error message
77
     * @param string $archivepathname
78
     * @param int $mode OPEN, CREATE or OVERWRITE constant
79
     * @param string $encoding archive local paths encoding, empty means autodetect
80
     * @return bool success
81
     */
82
    public function open($archivepathname, $mode=file_archive::CREATE, $encoding=null) {
83
        $this->close();
84
 
85
        $this->usedmem  = 0;
86
        $this->pos      = 0;
87
        $this->encoding = $encoding;
88
        $this->mode     = $mode;
89
 
90
        $this->za = new ZipArchive();
91
 
92
        switch($mode) {
93
            case file_archive::OPEN:      $flags = 0; break;
94
            case file_archive::OVERWRITE: $flags = ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE; break; //changed in PHP 5.2.8
95
            case file_archive::CREATE:
96
            default :                     $flags = ZIPARCHIVE::CREATE; break;
97
        }
98
 
99
        $result = $this->za->open($archivepathname, $flags);
100
 
101
        if ($flags == 0 and $result === ZIPARCHIVE::ER_NOZIP and filesize($archivepathname) === 22) {
102
            // Legacy PHP versions < 5.3.10 can not deal with empty zip archives.
103
            if (file_get_contents($archivepathname) === base64_decode(self::$emptyzipcontent)) {
104
                if ($temp = make_temp_directory('zip')) {
105
                    $this->emptyziphack = tempnam($temp, 'zip');
106
                    $this->za = new ZipArchive();
107
                    $result = $this->za->open($this->emptyziphack, ZIPARCHIVE::CREATE);
108
                }
109
            }
110
        }
111
 
112
        if ($result === true) {
113
            if (file_exists($archivepathname)) {
114
                $this->archivepathname = realpath($archivepathname);
115
            } else {
116
                $this->archivepathname = $archivepathname;
117
            }
118
            return true;
119
 
120
        } else {
121
            $message = 'Unknown error.';
122
            switch ($result) {
123
                case ZIPARCHIVE::ER_EXISTS: $message = 'File already exists.'; break;
124
                case ZIPARCHIVE::ER_INCONS: $message = 'Zip archive inconsistent.'; break;
125
                case ZIPARCHIVE::ER_INVAL: $message = 'Invalid argument.'; break;
126
                case ZIPARCHIVE::ER_MEMORY: $message = 'Malloc failure.'; break;
127
                case ZIPARCHIVE::ER_NOENT: $message = 'No such file.'; break;
128
                case ZIPARCHIVE::ER_NOZIP: $message = 'Not a zip archive.'; break;
129
                case ZIPARCHIVE::ER_OPEN: $message = 'Can\'t open file.'; break;
130
                case ZIPARCHIVE::ER_READ: $message = 'Read error.'; break;
131
                case ZIPARCHIVE::ER_SEEK: $message = 'Seek error.'; break;
132
            }
133
            debugging($message.': '.$archivepathname, DEBUG_DEVELOPER);
134
            $this->za = null;
135
            $this->archivepathname = null;
136
            return false;
137
        }
138
    }
139
 
140
    /**
141
     * Normalize $localname, always keep in utf-8 encoding.
142
     *
143
     * @param string $localname name of file in utf-8 encoding
144
     * @return string normalised compressed file or directory name
145
     */
146
    protected function mangle_pathname($localname) {
147
        $result = str_replace('\\', '/', $localname);   // no MS \ separators
148
        $result = preg_replace('/\.\.+\//', '', $result); // Cleanup any potential ../ transversal (any number of dots).
149
        $result = preg_replace('/\.\.+/', '.', $result); // Join together any number of consecutive dots.
150
        $result = ltrim($result, '/');                  // no leading slash
151
 
152
        if ($result === '.') {
153
            $result = '';
154
        }
155
 
156
        return $result;
157
    }
158
 
159
    /**
160
     * Tries to convert $localname into utf-8
161
     * please note that it may fail really badly.
162
     * The resulting file name is cleaned.
163
     *
164
     * @param string $localname name (encoding is read from zip file or guessed)
165
     * @return string in utf-8
166
     */
167
    protected function unmangle_pathname($localname) {
168
        $this->init_namelookup();
169
 
170
        if (!isset($this->namelookup[$localname])) {
171
            $name = $localname;
172
            // This should not happen.
173
            if (!empty($this->encoding) and $this->encoding !== 'utf-8') {
174
                $name = @core_text::convert($name, $this->encoding, 'utf-8');
175
            }
176
            $name = str_replace('\\', '/', $name);   // no MS \ separators
177
            $name = clean_param($name, PARAM_PATH);  // only safe chars
178
            return ltrim($name, '/');                // no leading slash
179
        }
180
 
181
        return $this->namelookup[$localname];
182
    }
183
 
184
    /**
185
     * Close archive, write changes to disk.
186
     *
187
     * @return bool success
188
     */
189
    public function close() {
190
        if (!isset($this->za)) {
191
            return false;
192
        }
193
 
194
        if ($this->emptyziphack) {
195
            @$this->za->close();
196
            $this->za = null;
197
            $this->mode = null;
198
            $this->namelookup = null;
199
            $this->modified = false;
200
            @unlink($this->emptyziphack);
201
            $this->emptyziphack = false;
202
            return true;
203
 
204
        } else if ($this->za->numFiles == 0) {
205
            // PHP can not create empty archives, so let's fake it.
206
            @$this->za->close();
207
            $this->za = null;
208
            $this->mode = null;
209
            $this->namelookup = null;
210
            $this->modified = false;
211
            // If the existing archive is already empty, we didn't change it.  Don't bother completing a save.
212
            // This is important when we are inspecting archives that we might not have write permission to.
213
            if (@filesize($this->archivepathname) == 22 &&
214
                    @file_get_contents($this->archivepathname) === base64_decode(self::$emptyzipcontent)) {
215
                return true;
216
            }
217
            @unlink($this->archivepathname);
218
            $data = base64_decode(self::$emptyzipcontent);
219
            if (!file_put_contents($this->archivepathname, $data)) {
220
                return false;
221
            }
222
            return true;
223
        }
224
 
225
        $res = $this->za->close();
226
        $this->za = null;
227
        $this->mode = null;
228
        $this->namelookup = null;
229
 
230
        if ($this->modified) {
231
            $this->fix_utf8_flags();
232
            $this->modified = false;
233
        }
234
 
235
        return $res;
236
    }
237
 
238
    /**
239
     * Returns file stream for reading of content.
240
     *
241
     * @param int $index index of file
242
     * @return resource|bool file handle or false if error
243
     */
244
    public function get_stream($index) {
245
        if (!isset($this->za)) {
246
            return false;
247
        }
248
 
249
        $name = $this->za->getNameIndex($index);
250
        if ($name === false) {
251
            return false;
252
        }
253
 
254
        return $this->za->getStream($name);
255
    }
256
 
257
    /**
258
     * Extract the archive contents to the given location.
259
     *
260
     * @param string $destination Path to the location where to extract the files.
261
     * @param int $index Index of the archive entry.
262
     * @return bool true on success or false on failure
263
     */
264
    public function extract_to($destination, $index) {
265
 
266
        if (!isset($this->za)) {
267
            return false;
268
        }
269
 
270
        $name = $this->za->getNameIndex($index);
271
 
272
        if ($name === false) {
273
            return false;
274
        }
275
 
276
        return $this->za->extractTo($destination, $name);
277
    }
278
 
279
    /**
280
     * Returns file information.
281
     *
282
     * @param int $index index of file
283
     * @return stdClass|bool info object or false if error
284
     */
285
    public function get_info($index) {
286
        if (!isset($this->za)) {
287
            return false;
288
        }
289
 
290
        // Need to use the ZipArchive's numfiles, as $this->count() relies on this function to count actual files (skipping OSX junk).
291
        if ($index < 0 or $index >=$this->za->numFiles) {
292
            return false;
293
        }
294
 
295
        // PHP 5.6 introduced encoding guessing logic for file names. To keep consistent behaviour with older versions,
296
        // we fall back to obtaining file names as raw unmodified strings.
297
        $result = $this->za->statIndex($index, ZipArchive::FL_ENC_RAW);
298
 
299
        if ($result === false) {
300
            return false;
301
        }
302
 
303
        $info = new stdClass();
304
        $info->index             = $index;
305
        $info->original_pathname = $result['name'];
306
        $info->pathname          = $this->unmangle_pathname($result['name']);
307
        $info->mtime             = (int)$result['mtime'];
308
 
309
        if ($info->pathname[strlen($info->pathname)-1] === '/') {
310
            $info->is_directory = true;
311
            $info->size         = 0;
312
        } else {
313
            $info->is_directory = false;
314
            $info->size         = (int)$result['size'];
315
        }
316
 
317
        if ($this->is_system_file($info)) {
318
            // Don't return system files.
319
            return false;
320
        }
321
 
322
        return $info;
323
    }
324
 
325
    /**
326
     * Returns array of info about all files in archive.
327
     *
328
     * @return array of file infos
329
     */
330
    public function list_files() {
331
        if (!isset($this->za)) {
332
            return false;
333
        }
334
 
335
        $infos = array();
336
 
337
        foreach ($this as $info) {
338
            // Simply iterating over $this will give us info only for files we're interested in.
339
            array_push($infos, $info);
340
        }
341
 
342
        return $infos;
343
    }
344
 
345
    public function is_system_file($fileinfo) {
346
        if (substr($fileinfo->pathname, 0, 8) === '__MACOSX' or substr($fileinfo->pathname, -9) === '.DS_Store') {
347
            // Mac OSX system files.
348
            return true;
349
        }
350
        if (substr($fileinfo->pathname, -9) === 'Thumbs.db') {
351
            $stream = $this->za->getStream($fileinfo->pathname);
352
            $info = base64_encode(fread($stream, 8));
353
            fclose($stream);
354
            if ($info === '0M8R4KGxGuE=') {
355
                // It's an OLE Compound File - so it's almost certainly a Windows thumbnail cache.
356
                return true;
357
            }
358
        }
359
        return false;
360
    }
361
 
362
    /**
363
     * Returns number of files in archive.
364
     *
365
     * @return int number of files
366
     */
367
    public function count(): int {
368
        if (!isset($this->za)) {
369
            return false;
370
        }
371
 
372
        return count($this->list_files());
373
    }
374
 
375
    /**
376
     * Returns approximate number of files in archive. This may be a slight
377
     * overestimate.
378
     *
379
     * @return int|bool Estimated number of files, or false if not opened
380
     */
381
    public function estimated_count() {
382
        if (!isset($this->za)) {
383
            return false;
384
        }
385
 
386
        return $this->za->numFiles;
387
    }
388
 
389
    /**
390
     * Add file into archive.
391
     *
392
     * @param string $localname name of file in archive
393
     * @param string $pathname location of file
394
     * @return bool success
395
     */
396
    public function add_file_from_pathname($localname, $pathname) {
397
        if ($this->emptyziphack) {
398
            $this->close();
399
            $this->open($this->archivepathname, file_archive::OVERWRITE, $this->encoding);
400
        }
401
 
402
        if (!isset($this->za)) {
403
            return false;
404
        }
405
 
406
        if ($this->archivepathname === realpath($pathname)) {
407
            // Do not add self into archive.
408
            return false;
409
        }
410
 
411
        if (!is_readable($pathname) or is_dir($pathname)) {
412
            return false;
413
        }
414
 
415
        if (is_null($localname)) {
416
            $localname = clean_param($pathname, PARAM_PATH);
417
        }
418
        $localname = trim($localname, '/'); // No leading slashes in archives!
419
        $localname = $this->mangle_pathname($localname);
420
 
421
        if ($localname === '') {
422
            // Sorry - conversion failed badly.
423
            return false;
424
        }
425
 
426
        if (!$this->za->addFile($pathname, $localname)) {
427
            return false;
428
        }
429
        $this->modified = true;
430
        return true;
431
    }
432
 
433
    /**
434
     * Add content of string into archive.
435
     *
436
     * @param string $localname name of file in archive
437
     * @param string $contents contents
438
     * @return bool success
439
     */
440
    public function add_file_from_string($localname, $contents) {
441
        if ($this->emptyziphack) {
442
            $this->close();
443
            $this->open($this->archivepathname, file_archive::OVERWRITE, $this->encoding);
444
        }
445
 
446
        if (!isset($this->za)) {
447
            return false;
448
        }
449
 
450
        $localname = trim($localname, '/'); // No leading slashes in archives!
451
        $localname = $this->mangle_pathname($localname);
452
 
453
        if ($localname === '') {
454
            // Sorry - conversion failed badly.
455
            return false;
456
        }
457
 
458
        if ($this->usedmem > 2097151) {
459
            // This prevents running out of memory when adding many large files using strings.
460
            $this->close();
461
            $res = $this->open($this->archivepathname, file_archive::OPEN, $this->encoding);
462
            if ($res !== true) {
463
                throw new \moodle_exception('cannotopenzip');
464
            }
465
        }
466
        $this->usedmem += strlen($contents);
467
 
468
        if (!$this->za->addFromString($localname, $contents)) {
469
            return false;
470
        }
471
        $this->modified = true;
472
        return true;
473
    }
474
 
475
    /**
476
     * Add empty directory into archive.
477
     *
478
     * @param string $localname name of file in archive
479
     * @return bool success
480
     */
481
    public function add_directory($localname) {
482
        if ($this->emptyziphack) {
483
            $this->close();
484
            $this->open($this->archivepathname, file_archive::OVERWRITE, $this->encoding);
485
        }
486
 
487
        if (!isset($this->za)) {
488
            return false;
489
        }
490
        $localname = trim($localname, '/'). '/';
491
        $localname = $this->mangle_pathname($localname);
492
 
493
        if ($localname === '/') {
494
            // Sorry - conversion failed badly.
495
            return false;
496
        }
497
 
498
        if ($localname !== '') {
499
            if (!$this->za->addEmptyDir($localname)) {
500
                return false;
501
            }
502
            $this->modified = true;
503
        }
504
        return true;
505
    }
506
 
507
    /**
508
     * Returns current file info.
509
     *
510
     * @return stdClass
511
     */
512
    #[\ReturnTypeWillChange]
513
    public function current() {
514
        if (!isset($this->za)) {
515
            return false;
516
        }
517
 
518
        return $this->get_info($this->pos);
519
    }
520
 
521
    /**
522
     * Returns the index of current file.
523
     *
524
     * @return int current file index
525
     */
526
    #[\ReturnTypeWillChange]
527
    public function key() {
528
        return $this->pos;
529
    }
530
 
531
    /**
532
     * Moves forward to next file.
533
     */
534
    public function next(): void {
535
        $this->pos++;
536
    }
537
 
538
    /**
539
     * Rewinds back to the first file.
540
     */
541
    public function rewind(): void {
542
        $this->pos = 0;
543
    }
544
 
545
    /**
546
     * Did we reach the end?
547
     *
548
     * @return bool
549
     */
550
    public function valid(): bool {
551
        if (!isset($this->za)) {
552
            return false;
553
        }
554
 
555
        // Skip over unwanted system files (get_info will return false).
556
        while (!$this->get_info($this->pos) && $this->pos < $this->za->numFiles) {
557
            $this->next();
558
        }
559
 
560
        // No files left - we're at the end.
561
        if ($this->pos >= $this->za->numFiles) {
562
            return false;
563
        }
564
 
565
        return true;
566
    }
567
 
568
    /**
569
     * Create a map of file names used in zip archive.
570
     * @return void
571
     */
572
    protected function init_namelookup() {
573
        if ($this->emptyziphack) {
574
            $this->namelookup = array();
575
            return;
576
        }
577
 
578
        if (!isset($this->za)) {
579
            return;
580
        }
581
        if (isset($this->namelookup)) {
582
            return;
583
        }
584
 
585
        $this->namelookup = array();
586
 
587
        if ($this->mode != file_archive::OPEN) {
588
            // No need to tweak existing names when creating zip file because there are none yet!
589
            return;
590
        }
591
 
592
        if (!file_exists($this->archivepathname)) {
593
            return;
594
        }
595
 
596
        if (!$fp = fopen($this->archivepathname, 'rb')) {
597
            return;
598
        }
599
        if (!$filesize = filesize($this->archivepathname)) {
600
            return;
601
        }
602
 
603
        $centralend = self::zip_get_central_end($fp, $filesize);
604
 
605
        if ($centralend === false or $centralend['disk'] !== 0 or $centralend['disk_start'] !== 0 or $centralend['offset'] === 0xFFFFFFFF) {
606
            // Single disk archives only and o support for ZIP64, sorry.
607
            fclose($fp);
608
            return;
609
        }
610
 
611
        fseek($fp, $centralend['offset']);
612
        $data = fread($fp, $centralend['size']);
613
        $pos = 0;
614
        $files = array();
615
        for($i=0; $i<$centralend['entries']; $i++) {
616
            $file = self::zip_parse_file_header($data, $centralend, $pos);
617
            if ($file === false) {
618
                // Wrong header, sorry.
619
                fclose($fp);
620
                return;
621
            }
622
            $files[] = $file;
623
        }
624
        fclose($fp);
625
 
626
        foreach ($files as $file) {
627
            $name = $file['name'];
628
            if (preg_match('/^[a-zA-Z0-9_\-\.]*$/', $file['name'])) {
629
                // No need to fix ASCII.
630
                $name = fix_utf8($name);
631
 
632
            } else if (!($file['general'] & pow(2, 11))) {
633
                // First look for unicode name alternatives.
634
                $found = false;
635
                foreach($file['extra'] as $extra) {
636
                    if ($extra['id'] === 0x7075) {
637
                        $data = unpack('cversion/Vcrc', substr($extra['data'], 0, 5));
638
                        if ($data['crc'] === crc32($name)) {
639
                            $found = true;
640
                            $name = substr($extra['data'], 5);
641
                        }
642
                    }
643
                }
644
                if (!$found and !empty($this->encoding) and $this->encoding !== 'utf-8') {
645
                    // Try the encoding from open().
646
                    $newname = @core_text::convert($name, $this->encoding, 'utf-8');
647
                    $original  = core_text::convert($newname, 'utf-8', $this->encoding);
648
                    if ($original === $name) {
649
                        $found = true;
650
                        $name = $newname;
651
                    }
652
                }
653
                if (!$found and $file['version'] === 0x315) {
654
                    // This looks like OS X build in zipper.
655
                    $newname = fix_utf8($name);
656
                    if ($newname === $name) {
657
                        $found = true;
658
                        $name = $newname;
659
                    }
660
                }
661
                if (!$found and $file['version'] === 0) {
662
                    // This looks like our old borked Moodle 2.2 file.
663
                    $newname = fix_utf8($name);
664
                    if ($newname === $name) {
665
                        $found = true;
666
                        $name = $newname;
667
                    }
668
                }
669
                if (!$found and $encoding = get_string('oldcharset', 'langconfig')) {
670
                    // Last attempt - try the dos/unix encoding from current language.
671
                    $windows = true;
672
                    foreach($file['extra'] as $extra) {
673
                        // In Windows archivers do not usually set any extras with the exception of NTFS flag in WinZip/WinRar.
674
                        $windows = false;
675
                        if ($extra['id'] === 0x000a) {
676
                            $windows = true;
677
                            break;
678
                        }
679
                    }
680
 
681
                    if ($windows === true) {
682
                        switch(strtoupper($encoding)) {
683
                            case 'ISO-8859-1': $encoding = 'CP850'; break;
684
                            case 'ISO-8859-2': $encoding = 'CP852'; break;
685
                            case 'ISO-8859-4': $encoding = 'CP775'; break;
686
                            case 'ISO-8859-5': $encoding = 'CP866'; break;
687
                            case 'ISO-8859-6': $encoding = 'CP720'; break;
688
                            case 'ISO-8859-7': $encoding = 'CP737'; break;
689
                            case 'ISO-8859-8': $encoding = 'CP862'; break;
690
                            case 'WINDOWS-1251': $encoding = 'CP866'; break;
691
                            case 'EUC-JP':
692
                            case 'UTF-8':
693
                                if ($winchar = get_string('localewincharset', 'langconfig')) {
694
                                    // Most probably works only for zh_cn,
695
                                    // if there are more problems we could add zipcharset to langconfig files.
696
                                    $encoding = $winchar;
697
                                }
698
                                break;
699
                        }
700
                    }
701
                    $newname = @core_text::convert($name, $encoding, 'utf-8');
702
                    $original  = core_text::convert($newname, 'utf-8', $encoding);
703
 
704
                    if ($original === $name) {
705
                        $name = $newname;
706
                    }
707
                }
708
            }
709
            $name = str_replace('\\', '/', $name);  // no MS \ separators
710
            $name = clean_param($name, PARAM_PATH); // only safe chars
711
            $name = ltrim($name, '/');              // no leading slash
712
 
713
            if (function_exists('normalizer_normalize')) {
714
                $name = normalizer_normalize($name, Normalizer::FORM_C);
715
            }
716
 
717
            $this->namelookup[$file['name']] = $name;
718
        }
719
    }
720
 
721
    /**
722
     * Add unicode flag to all files in archive.
723
     *
724
     * NOTE: single disk archives only, no ZIP64 support.
725
     *
726
     * @return bool success, modifies the file contents
727
     */
728
    protected function fix_utf8_flags() {
729
        if ($this->emptyziphack) {
730
            return true;
731
        }
732
 
733
        if (!file_exists($this->archivepathname)) {
734
            return true;
735
        }
736
 
737
        // Note: the ZIP structure is described at http://www.pkware.com/documents/casestudies/APPNOTE.TXT
738
        if (!$fp = fopen($this->archivepathname, 'rb+')) {
739
            return false;
740
        }
741
        if (!$filesize = filesize($this->archivepathname)) {
742
            return false;
743
        }
744
 
745
        $centralend = self::zip_get_central_end($fp, $filesize);
746
 
747
        if ($centralend === false or $centralend['disk'] !== 0 or $centralend['disk_start'] !== 0 or $centralend['offset'] === 0xFFFFFFFF) {
748
            // Single disk archives only and o support for ZIP64, sorry.
749
            fclose($fp);
750
            return false;
751
        }
752
 
753
        fseek($fp, $centralend['offset']);
754
        $data = fread($fp, $centralend['size']);
755
        $pos = 0;
756
        $files = array();
757
        for($i=0; $i<$centralend['entries']; $i++) {
758
            $file = self::zip_parse_file_header($data, $centralend, $pos);
759
            if ($file === false) {
760
                // Wrong header, sorry.
761
                fclose($fp);
762
                return false;
763
            }
764
 
765
            $newgeneral = $file['general'] | pow(2, 11);
766
            if ($newgeneral === $file['general']) {
767
                // Nothing to do with this file.
768
                continue;
769
            }
770
 
771
            if (preg_match('/^[a-zA-Z0-9_\-\.]*$/', $file['name'])) {
772
                // ASCII file names are always ok.
773
                continue;
774
            }
775
            if ($file['extra']) {
776
                // Most probably not created by php zip ext, better to skip it.
777
                continue;
778
            }
779
            if (fix_utf8($file['name']) !== $file['name']) {
780
                // Does not look like a valid utf-8 encoded file name, skip it.
781
                continue;
782
            }
783
 
784
            // Read local file header.
785
            fseek($fp, $file['local_offset']);
786
            $localfile = unpack('Vsig/vversion_req/vgeneral/vmethod/vmtime/vmdate/Vcrc/Vsize_compressed/Vsize/vname_length/vextra_length', fread($fp, 30));
787
            if ($localfile['sig'] !== 0x04034b50) {
788
                // Borked file!
789
                fclose($fp);
790
                return false;
791
            }
792
 
793
            $file['local'] = $localfile;
794
            $files[] = $file;
795
        }
796
 
797
        foreach ($files as $file) {
798
            $localfile = $file['local'];
799
            // Add the unicode flag in central file header.
800
            fseek($fp, $file['central_offset'] + 8);
801
            if (ftell($fp) === $file['central_offset'] + 8) {
802
                $newgeneral = $file['general'] | pow(2, 11);
803
                fwrite($fp, pack('v', $newgeneral));
804
            }
805
            // Modify local file header too.
806
            fseek($fp, $file['local_offset'] + 6);
807
            if (ftell($fp) === $file['local_offset'] + 6) {
808
                $newgeneral = $localfile['general'] | pow(2, 11);
809
                fwrite($fp, pack('v', $newgeneral));
810
            }
811
        }
812
 
813
        fclose($fp);
814
        return true;
815
    }
816
 
817
    /**
818
     * Read end of central signature of ZIP file.
819
     * @internal
820
     * @static
821
     * @param resource $fp
822
     * @param int $filesize
823
     * @return array|bool
824
     */
825
    public static function zip_get_central_end($fp, $filesize) {
826
        // Find end of central directory record.
827
        fseek($fp, $filesize - 22);
828
        $info = unpack('Vsig', fread($fp, 4));
829
        if ($info['sig'] === 0x06054b50) {
830
            // There is no comment.
831
            fseek($fp, $filesize - 22);
832
            $data = fread($fp, 22);
833
        } else {
834
            // There is some comment with 0xFF max size - that is 65557.
835
            fseek($fp, $filesize - 65557);
836
            $data = fread($fp, 65557);
837
        }
838
 
839
        $pos = strpos($data, pack('V', 0x06054b50));
840
        if ($pos === false) {
841
            // Borked ZIP structure!
842
            return false;
843
        }
844
        $centralend = unpack('Vsig/vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_length', substr($data, $pos, 22));
845
        if ($centralend['comment_length']) {
846
            $centralend['comment'] = substr($data, 22, $centralend['comment_length']);
847
        } else {
848
            $centralend['comment'] = '';
849
        }
850
 
851
        return $centralend;
852
    }
853
 
854
    /**
855
     * Parse file header.
856
     * @internal
857
     * @param string $data
858
     * @param array $centralend
859
     * @param int $pos (modified)
860
     * @return array|bool file info
861
     */
862
    public static function zip_parse_file_header($data, $centralend, &$pos) {
863
        $file = unpack('Vsig/vversion/vversion_req/vgeneral/vmethod/Vmodified/Vcrc/Vsize_compressed/Vsize/vname_length/vextra_length/vcomment_length/vdisk/vattr/Vattrext/Vlocal_offset', substr($data, $pos, 46));
864
        $file['central_offset'] = $centralend['offset'] + $pos;
865
        $pos = $pos + 46;
866
        if ($file['sig'] !== 0x02014b50) {
867
            // Borked ZIP structure!
868
            return false;
869
        }
870
        $file['name'] = substr($data, $pos, $file['name_length']);
871
        $pos = $pos + $file['name_length'];
872
        $file['extra'] = array();
873
        $file['extra_data'] = '';
874
        if ($file['extra_length']) {
875
            $extradata = substr($data, $pos, $file['extra_length']);
876
            $file['extra_data'] = $extradata;
877
            while (strlen($extradata) > 4) {
878
                $extra = unpack('vid/vsize', substr($extradata, 0, 4));
879
                $extra['data'] = substr($extradata, 4, $extra['size']);
880
                $extradata = substr($extradata, 4+$extra['size']);
881
                $file['extra'][] = $extra;
882
            }
883
            $pos = $pos + $file['extra_length'];
884
        }
885
        if ($file['comment_length']) {
886
            $pos = $pos + $file['comment_length'];
887
            $file['comment'] = substr($data, $pos, $file['comment_length']);
888
        } else {
889
            $file['comment'] = '';
890
        }
891
        return $file;
892
    }
893
}