1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
4/**
5 * Factory to access the most common File_Archive features
6 * It uses lazy include, so you dont have to include the files from
7 * File/Archive/* directories
8 *
9 * PHP versions 4 and 5
10 *
11 * This library is free software; you can redistribute it and/or
12 * modify it under the terms of the GNU Lesser General Public
13 * License as published by the Free Software Foundation; either
14 * version 2.1 of the License, or (at your option) any later version.
15 *
16 * This library is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19 * Lesser General Public License for more details.
20 *
21 * You should have received a copy of the GNU Lesser General Public
22 * License along with this library; if not, write to the Free Software
23 * Foundation, Inc., 59 Temple Place, Suite 330,Boston,MA 02111-1307 USA
24 *
25 * @category   File Formats
26 * @package    File_Archive
27 * @author     Vincent Lascaux <vincentlascaux@php.net>
28 * @copyright  1997-2005 The PHP Group
29 * @license    http://www.gnu.org/copyleft/lesser.html  LGPL
30 * @version    CVS: $Id$
31 * @link       http://pear.php.net/package/File_Archive
32 */
33
34/**
35 * To have access to PEAR::isError and PEAR::raiseError
36 * We should probably use lazy include and remove this inclusion...
37 */
38require_once "PEAR.php";
39
40function File_Archive_cleanCache($file, $group)
41{
42    $file = explode('_', $file);
43    if (count($file) != 3) {
44        return false; //not a File_Archive file, keep it
45    }
46
47    $name = $file[2];
48    $name = urldecode($name);
49
50    $group = $file[1];
51
52    //clean the cache only for files in File_Archive groups
53    return substr($group, 0, 11) == 'FileArchive' &&
54           !file_exists($name); //and only if the related file no longer exists
55}
56
57/**
58 * Factory to access the most common File_Archive features
59 * It uses lazy include, so you dont have to include the files from
60 * File/Archive/* directories
61 */
62class File_Archive
63{
64    function& _option($name)
65    {
66        static $container = array(
67            'zipCompressionLevel' => 9,
68             'gzCompressionLevel' => 9,
69            'tmpDirectory' => '.',
70            'cache' => null,
71            'appendRemoveDuplicates' => false,
72            'blockSize' => 65536,
73            'cacheCondition' => false
74        );
75        return $container[$name];
76    }
77    /**
78     * Sets an option that will be used by default by all readers or writers
79     * Option names are case sensitive
80     * Currently, the following options are used:
81     *
82     * "cache"
83     *      Instance of a Cache_Lite object used to cache some compressed
84     *      data to speed up future compressions of files
85     *      Default: null (no cache used)
86     *
87     * "zipCompressionLevel"
88     *      Value between 0 and 9 specifying the default compression level used
89     *      by Zip writers (0 no compression, 9 highest compression)
90     *      Default: 9
91     *
92     * "gzCompressionLevel"
93     *      Value between 0 and 9 specifying the default compression level used
94     *      by Gz writers (0 no compression, 9 highest compression)
95     *      Default: 9
96     *
97     * "tmpDirectory"
98     *      Directory where the temporary files generated by File_Archive will
99     *      be created
100     *      Default: '.'
101     *
102     * "appendRemoveDuplicates"
103     *      If set to true, the appender created will by default remove the
104     *      file present in the archive when adding a new one. This will slow the
105     *      appending of files to archives
106     *      Default: false
107     *
108     * "blockSize"
109     *      To transfer data from a reader to a writer, some chunks a read from the
110     *      source and written to the writer. This parameter controls the size of the
111     *      chunks
112     *      Default: 64kB
113     *
114     * "cacheCondition"
115     *      This parameter specifies when a cache should be used. When the cache is
116     *      used, the data of the reader is saved in a temporary file for future access.
117     *      The cached reader will be read only once, even if you read it several times.
118     *      This can be usefull to read compressed files or downloaded files (from http or ftp)
119     *      The possible values for this option are
120     *       - false: never use cache
121     *       - a regexp: A cache will be used if the specified URL matches the regexp
122     *         preg_match is used
123     *      Default: false
124     *      Example: '/^(http|ftp):\/\//' will cache all files downloaded via http or ftp
125     *
126     */
127    function setOption($name, $value)
128    {
129        $option =& File_Archive::_option($name);
130        $option = $value;
131        if ($name == 'cache' && $value !== null) {
132            //TODO: ask to Cache_Lite to allow that
133            $value->_fileNameProtection = false;
134        }
135    }
136
137    /**
138     * Retrieve the value of an option
139     */
140    function getOption($name)
141    {
142        return File_Archive::_option($name);
143    }
144
145    /**
146     * Create a reader to read the URL $URL.
147     * If the URL is a directory, it will recursively read that directory.
148     * If $uncompressionLevel is not null, the archives (files with extension
149     * tar, zip, gz or tgz) will be considered as directories (up to a depth of
150     * $uncompressionLevel if $uncompressionLevel > 0). The reader will only
151     * read files with a directory depth of $directoryDepth. It reader will
152     * replace the given URL ($URL) with $symbolic in the public filenames
153     * The default symbolic name is the last filename in the URL (or '' for
154     * directories)
155     *
156     * Examples:
157     * Considere the following file system
158     * <pre>
159     * a.txt
160     * b.tar (archive that contains the following files)
161     *     c.txt
162     *     d.tgz (archive that contains the following files)
163     *         e.txt
164     *         dir1/
165     *             f.txt
166     * dir2/
167     *     g.txt
168     *     dir3/
169     *         h.tar (archive that contains the following files)
170     *             i.txt
171     * </pre>
172     *
173     * read('.') will return a reader that gives access to following
174     * files (recursively read current dir):
175     * <pre>
176     * a.txt
177     * b.tar
178     * dir2/g.txt
179     * dir2/dir3/h.tar
180     * </pre>
181     *
182     * read('.', 'myBaseDir') will return the following reader:
183     * <pre>
184     * myBaseDir/a.txt
185     * myBaseDir/b.tar
186     * myBaseDir/dir2/g.txt
187     * myBaseDir/dir2/dir3/h.tar
188     * </pre>
189     *
190     * read('.', '', -1) will return the following reader (uncompress
191     * everything)
192     * <pre>
193     * a.txt
194     * b.tar/c.txt
195     * b.tar/d.tgz/e.txt
196     * b.tar/d.tgz/dir1/f.txt
197     * dir2/g.txt
198     * dir2/dir3/h.tar/i.txt
199     * </pre>
200     *
201     * read('.', '', 1) will uncompress only one level (so d.tgz will
202     * not be uncompressed):
203     * <pre>
204     * a.txt
205     * b.tar/c.txt
206     * b.tar/d.tgz
207     * dir2/g.txt
208     * dir2/dir3/h.tar/i.txt
209     * </pre>
210     *
211     * read('.', '', 0, 0) will not recurse into subdirectories
212     * <pre>
213     * a.txt
214     * b.tar
215     * </pre>
216     *
217     * read('.', '', 0, 1) will recurse only one level in
218     * subdirectories
219     * <pre>
220     * a.txt
221     * b.tar
222     * dir2/g.txt
223     * </pre>
224     *
225     * read('.', '', -1, 2) will uncompress everything and recurse in
226     * only 2 levels in subdirectories or archives
227     * <pre>
228     * a.txt
229     * b.tar/c.txt
230     * b.tar/d.tgz/e.txt
231     * dir2/g.txt
232     * </pre>
233     *
234     * The recursion level is determined by the real path, not the symbolic one.
235     * So read('.', 'myBaseDir', -1, 2) will result to the same files:
236     * <pre>
237     * myBaseDir/a.txt
238     * myBaseDir/b.tar/c.txt
239     * myBaseDir/b.tar/d.tgz/e.txt (accepted because the real depth is 2)
240     * myBaseDir/dir2/g.txt
241     * </pre>
242     *
243     * Use readSource to do the same thing, reading from a specified reader instead of
244     * reading from the system files
245     *
246     * To read a single file, you can do read('a.txt', 'public_name.txt')
247     * If no public name is provided, the default one is the name of the file
248     * read('dir2/g.txt') contains the single file named 'g.txt'
249     * read('b.tar/c.txt') contains the single file named 'c.txt'
250     *
251     * Note: This function uncompress files reading their extension
252     *       The compressed files must have a tar, zip, gz or tgz extension
253     *       Since it is impossible for some URLs to use is_dir or is_file, this
254     *       function may not work with
255     *       URLs containing folders which name ends with such an extension
256     */
257    function readSource(&$source, $URL, $symbolic = null,
258                  $uncompression = 0, $directoryDepth = -1)
259    {
260        return File_Archive::_readSource($source, $URL, $reachable, $baseDir,
261                  $symbolic, $uncompression, $directoryDepth);
262    }
263
264    /**
265     * This function performs exactly as readSource, but with two additional parameters
266     * ($reachable and $baseDir) that will be set so that $reachable."/".$baseDir == $URL
267     * and $reachable can be reached (in case of error)
268     *
269     * @access private
270     */
271    function _readSource(&$toConvert, $URL, &$reachable, &$baseDir, $symbolic = null,
272                  $uncompression = 0, $directoryDepth = -1)
273    {
274        $source =& File_Archive::_convertToReader($toConvert);
275        if (PEAR::isError($source)) {
276            return $source;
277        }
278        if (is_array($URL)) {
279            $converted = array();
280            foreach($URL as $key => $foo) {
281                $converted[] =& File_Archive::_convertToReader($URL[$key]);
282            }
283            return File_Archive::readMulti($converted);
284        }
285
286        //No need to uncompress more than $directoryDepth
287        //That's not perfect, and some archives will still be uncompressed just
288        //to be filtered out :(
289        if ($directoryDepth >= 0) {
290            $uncompressionLevel = min($uncompression, $directoryDepth);
291        } else {
292            $uncompressionLevel = $uncompression;
293        }
294
295        require_once 'File/Archive/Reader.php';
296        $std = File_Archive_Reader::getStandardURL($URL);
297
298        //Modify the symbolic name if necessary
299        $slashPos = strrpos($std, '/');
300        if ($symbolic === null) {
301            if ($slashPos === false) {
302                $realSymbolic = $std;
303            } else {
304                $realSymbolic = substr($std, $slashPos+1);
305            }
306        } else {
307            $realSymbolic = $symbolic;
308        }
309        if ($slashPos !== false) {
310            $baseFile = substr($std, 0, $slashPos+1);
311            $lastFile = substr($std, $slashPos+1);
312        } else {
313            $baseFile = '';
314            $lastFile = $std;
315        }
316
317        if (strpos($lastFile, '*')!==false ||
318            strpos($lastFile, '?')!==false) {
319            //We have to build a regexp here
320            $regexp = str_replace(
321                array('\*', '\?'),
322                array('[^/]*', '[^/]'),
323                preg_quote($lastFile)
324            );
325            $result = File_Archive::_readSource($source, $baseFile,
326                                                $reachable, $baseDir, null, 0, -1);
327            return File_Archive::filter(
328                    File_Archive::predPreg('/^'.$regexp.'$/'),
329                    $result
330                   );
331        }
332
333        //If the URL can be interpreted as a directory, and we are reading from the file system
334        if ((empty($URL) || is_dir($URL)) && $source === null) {
335            require_once "File/Archive/Reader/Directory.php";
336
337            if ($uncompressionLevel != 0) {
338                require_once "File/Archive/Reader/Uncompress.php";
339                $result = new File_Archive_Reader_Uncompress(
340                    new File_Archive_Reader_Directory($std, '', $directoryDepth),
341                    $uncompressionLevel
342                );
343            } else {
344                $result = new File_Archive_Reader_Directory($std, '', $directoryDepth);
345            }
346
347            if ($directoryDepth >= 0) {
348                require_once 'File/Archive/Reader/Filter.php';
349                require_once 'File/Archive/Predicate/MaxDepth.php';
350
351                $tmp =& File_Archive::filter(
352                    new File_Archive_Predicate_MaxDepth($directoryDepth),
353                    $result
354                );
355                unset($result);
356                $result =& $tmp;
357            }
358            if (!empty($realSymbolic)) {
359                if ($symbolic === null) {
360                    $realSymbolic = '';
361                }
362                require_once "File/Archive/Reader/ChangeName/AddDirectory.php";
363                $tmp = new File_Archive_Reader_ChangeName_AddDirectory(
364                    $realSymbolic,
365                    $result
366                );
367                unset($result);
368                $result =& $tmp;
369            }
370
371        //If the URL can be interpreted as a file, and we are reading from the file system
372        } else if (is_file($URL) && substr($URL, -1)!='/' && $source === null) {
373            require_once "File/Archive/Reader/File.php";
374            $result = new File_Archive_Reader_File($URL, $realSymbolic);
375
376        //Else, we will have to build a complex reader
377        } else {
378            require_once "File/Archive/Reader/File.php";
379
380            $realPath = $std;
381
382            // Try to find a file with a known extension in the path (
383            // (to manage URLs like archive.tar/directory/file)
384            $pos = 0;
385            do {
386                if ($pos+1<strlen($realPath)) {
387                    $pos = strpos($realPath, '/', $pos+1);
388                } else {
389                    $pos = false;
390                }
391                if ($pos === false) {
392                    $pos = strlen($realPath);
393                }
394
395                $file = substr($realPath, 0, $pos);
396                $baseDir = substr($realPath, $pos+1);
397                $dotPos = strrpos($file, '.');
398                $extension = '';
399                if ($dotPos !== false) {
400                    $extension = substr($file, $dotPos+1);
401                }
402            } while ($pos < strlen($realPath) &&
403                (!File_Archive::isKnownExtension($extension) ||
404                 (is_dir($file) && $source==null)));
405
406            $reachable = $file;
407
408            //If we are reading from the file system
409            if ($source === null) {
410                //Create a file reader
411                $result = new File_Archive_Reader_File($file);
412            } else {
413                //Select in the source the file $file
414
415                require_once "File/Archive/Reader/Select.php";
416                $result = new File_Archive_Reader_Select($file, $source);
417            }
418
419            require_once "File/Archive/Reader/Uncompress.php";
420            $tmp = new File_Archive_Reader_Uncompress($result, $uncompressionLevel);
421            unset($result);
422            $result = $tmp;
423
424            //Select the requested folder in the uncompress reader
425            $isDir = $result->setBaseDir($std);
426            if (PEAR::isError($isDir)) {
427                return $isDir;
428            }
429            if ($isDir && $symbolic==null) {
430                //Default symbolic name for directories is empty
431                $realSymbolic = '';
432            }
433
434            if ($directoryDepth >= 0) {
435                //Limit the maximum depth if necessary
436                require_once "File/Archive/Reader/Filter.php";
437                require_once "File/Archive/Predicate/MaxDepth.php";
438
439                $tmp = new File_Archive_Reader_Filter(
440                    new File_Archive_Predicate(
441                        $directoryDepth +
442                        substr_count(substr($std, $pos+1), '/')
443                    ),
444                    $result
445                );
446                unset($result);
447                $result =& $tmp;
448            }
449
450            if ($std != $realSymbolic) {
451                require_once "File/Archive/Reader/ChangeName/Directory.php";
452
453                //Change the base name to the symbolic one if necessary
454                $tmp = new File_Archive_Reader_ChangeName_Directory(
455                    $std,
456                    $realSymbolic,
457                    $result
458                );
459                unset($result);
460                $result =& $tmp;
461            }
462        }
463
464        $cacheCondition = File_Archive::getOption('cacheCondition');
465        if ($cacheCondition !== false &&
466            preg_match($cacheCondition, $URL)) {
467            $tmp =& File_Archive::cache($result);
468            unset($result);
469            $result =& $tmp;
470        }
471
472        return $result;
473    }
474    function read($URL, $symbolic = null,
475                  $uncompression = 0, $directoryDepth = -1)
476    {
477        $source = null;
478        return File_Archive::readSource($source, $URL, $symbolic, $uncompression, $directoryDepth);
479    }
480
481    /**
482     * Create a file reader on an uploaded file. The reader will read
483     * $_FILES[$name]['tmp_name'] and will have $_FILES[$name]['name']
484     * as a symbolic filename.
485     *
486     * A PEAR error is returned if one of the following happen
487     *  - $_FILES[$name] is not set
488     *  - $_FILES[$name]['error'] is not 0
489     *  - is_uploaded_file returns false
490     *
491     * @param string $name Index of the file in the $_FILES array
492     * @return File_Archive_Reader File reader on the uploaded file
493     */
494    function readUploadedFile($name)
495    {
496        if (!isset($_FILES[$name])) {
497            return PEAR::raiseError("File $name has not been uploaded");
498        }
499        switch ($_FILES[$name]['error']) {
500        case 0:
501            //No error
502            break;
503        case 1:
504            return PEAR::raiseError(
505                        "The upload size limit didn't allow to upload file ".
506                        $_FILES[$name]['name']
507                    );
508        case 2:
509            return PEAR::raiseError(
510                        "The form size limit didn't allow to upload file ".
511                        $_FILES[$name]['name']
512                   );
513        case 3:
514            return PEAR::raiseError(
515                        "The file was not entirely uploaded"
516                   );
517        case 4:
518            return PEAR::raiseError(
519                        "The uploaded file is empty"
520                   );
521        default:
522            return PEAR::raiseError(
523                        "Unknown error ".$_FILES[$name]['error']." in file upload. ".
524                        "Please, report a bug"
525                   );
526        }
527        if (!is_uploaded_file($_FILES[$name]['tmp_name'])) {
528            return PEAR::raiseError("The file is not an uploaded file");
529        }
530
531        require_once "File/Archive/Reader/File.php";
532        return new File_Archive_Reader_File(
533                    $_FILES[$name]['tmp_name'],
534                    $_FILES[$name]['name'],
535                    $_FILES[$name]['type']
536               );
537    }
538
539    /**
540     * Adds a cache layer above the specified reader
541     * The data of the reader is saved in a temporary file for future access.
542     * The cached reader will be read only once, even if you read it several times.
543     * This can be usefull to read compressed files or downloaded files (from http or ftp)
544     *
545     * @param mixed $toConvert The reader to cache
546     *        It can be a File_Archive_Reader or a string, which will be converted using the
547     *        read function
548     */
549    function cache(&$toConvert)
550    {
551        $source =& File_Archive::_convertToReader($toConvert);
552        if (PEAR::isError($source)) {
553            return $source;
554        }
555
556        require_once 'File/Archive/Reader/Cache.php';
557        return new File_Archive_Reader_Cache($source);
558    }
559
560    /**
561     * Try to interpret the object as a reader
562     * Strings are converted to readers using File_Archive::read
563     * Arrays are converted to readers using File_Archive::readMulti
564     *
565     * @access private
566     */
567    function &_convertToReader(&$source)
568    {
569        if (is_string($source)) {
570            $cacheCondition = File_Archive::getOption('cacheCondition');
571            if ($cacheCondition !== false &&
572                preg_match($cacheCondition, $source)) {
573                $obj = File_Archive::cache(File_Archive::read($source));
574                return $obj;
575            } else {
576                $obj = File_Archive::read($source);
577                return $obj;
578            }
579        } else if (is_array($source)) {
580            return File_Archive::readMulti($source);
581        } else {
582            return $source;
583        }
584     }
585
586    /**
587     * Try to interpret the object as a writer
588     * Strings are converted to writers using File_Archive::appender
589     * Arrays are converted to writers using a multi writer
590     *
591     * @access private
592     */
593    function &_convertToWriter(&$dest)
594    {
595        if (is_string($dest)) {
596            $obj =& File_Archive::appender($dest);
597            return $obj;
598        } else if (is_array($dest)) {
599            require_once 'File/Archive/Writer/Multi.php';
600            $writer = new File_Archive_Writer_Multi();
601            foreach($dest as $key => $foo) {
602                $writer->addWriter($dest[$key]);
603            }
604            return $writer;
605        } else {
606            return $dest;
607        }
608    }
609
610    /**
611     * Check if a file with a specific extension can be read as an archive
612     * with File_Archive::read*
613     * This function is case sensitive.
614     *
615     * @param string $extension the checked extension
616     * @return bool whether this file can be understood reading its extension
617     *         Currently, supported extensions are tar, zip, jar, gz, tgz,
618     *         tbz, bz2, bzip2, ar, deb
619     */
620    function isKnownExtension($extension)
621    {
622        return $extension == 'tar'   ||
623               $extension == 'zip'   ||
624               $extension == 'jar'   ||
625               $extension == 'gz'    ||
626               $extension == 'tgz'   ||
627               $extension == 'tbz'   ||
628               $extension == 'bz2'   ||
629               $extension == 'bzip2' ||
630               $extension == 'ar'    ||
631               $extension == 'deb'   /* ||
632               $extension == 'cab'   ||
633               $extension == 'rar' */;
634    }
635
636    /**
637     * Create a reader that will read the single file source $source as
638     * a specific archive
639     *
640     * @param string $extension determines the kind of archive $source contains
641     *        $extension is case sensitive
642     * @param File_Archive_Reader $source stores the archive
643     * @param bool $sourceOpened specifies if the archive is already opened
644     *        if false, next will be called on source
645     *        Closing the returned archive will close $source iif $sourceOpened
646     *        is true
647     * @return A File_Archive_Reader that uncompresses the archive contained in
648     *         $source interpreting it as a $extension archive
649     *         If $extension is not handled return false
650     */
651    function readArchive($extension, &$toConvert, $sourceOpened = false)
652    {
653        $source =& File_Archive::_convertToReader($toConvert);
654        if (PEAR::isError($source)) {
655            return $source;
656        }
657
658        switch($extension) {
659        case 'tgz':
660            return File_Archive::readArchive('tar',
661                    File_Archive::readArchive('gz', $source, $sourceOpened)
662                    );
663        case 'tbz':
664            return File_Archive::readArchive('tar',
665                    File_Archive::readArchive('bz2', $source, $sourceOpened)
666                    );
667        case 'tar':
668            require_once 'File/Archive/Reader/Tar.php';
669            return new File_Archive_Reader_Tar($source, $sourceOpened);
670
671        case 'gz':
672        case 'gzip':
673            require_once 'File/Archive/Reader/Gzip.php';
674            return new File_Archive_Reader_Gzip($source, $sourceOpened);
675
676        case 'zip':
677        case 'jar':
678            require_once 'File/Archive/Reader/Zip.php';
679            return new File_Archive_Reader_Zip($source, $sourceOpened);
680
681        case 'bz2':
682        case 'bzip2':
683            require_once 'File/Archive/Reader/Bzip2.php';
684            return new File_Archive_Reader_Bzip2($source, $sourceOpened);
685
686        case 'deb':
687        case 'ar':
688            require_once 'File/Archive/Reader/Ar.php';
689            return new File_Archive_Reader_Ar($source, $sourceOpened);
690
691/*        case 'cab':
692            require_once 'File/Archive/Reader/Cab.php';
693            return new File_Archive_Reader_Cab($source, $sourceOpened);
694
695
696        case 'rar':
697            require_once "File/Archive/Reader/Rar.php";
698            return new File_Archive_Reader_Rar($source, $sourceOpened); */
699
700        default:
701            return false;
702        }
703    }
704
705    /**
706     * Contains only one file with data read from a memory buffer
707     *
708     * @param string $memory content of the file
709     * @param string $filename public name of the file
710     * @param array $stat statistics of the file. Index 7 (size) will be
711     *        overwritten to match the size of $memory
712     * @param string $mime mime type of the file. Default will determine the
713     *        mime type thanks to the extension of $filename
714     * @see File_Archive_Reader_Memory
715     */
716    function readMemory($memory, $filename, $stat=array(), $mime=null)
717    {
718        require_once "File/Archive/Reader/Memory.php";
719        return new File_Archive_Reader_Memory($memory, $filename, $stat, $mime);
720    }
721
722    /**
723     * Contains several other sources. Take care the sources don't have several
724     * files with the same filename. The sources are given as a parameter, or
725     * can be added thanks to the reader addSource method
726     *
727     * @param array $sources Array of strings or readers that will be added to
728     *        the multi reader. If the parameter is a string, a reader will be
729     *        built thanks to the read function
730     * @see   File_Archive_Reader_Multi, File_Archive::read()
731     */
732    function readMulti($sources = array())
733    {
734        require_once "File/Archive/Reader/Multi.php";
735        $result = new File_Archive_Reader_Multi();
736        foreach ($sources as $index => $foo) {
737            $s =& File_Archive::_convertToReader($sources[$index]);
738            if (PEAR::isError($s)) {
739                return $s;
740            } else {
741                $result->addSource($s);
742            }
743        }
744        return $result;
745    }
746    /**
747     * Make the files of a source appear as one large file whose content is the
748     * concatenation of the content of all the files
749     *
750     * @param File_Archive_Reader $toConvert The source whose files must be
751     *        concatened
752     * @param string $filename name of the only file of the created reader
753     * @param array $stat statistics of the file. Index 7 (size) will be
754     *        overwritten to match the total size of the files
755     * @param string $mime mime type of the file. Default will determine the
756     *        mime type thanks to the extension of $filename
757     * @see   File_Archive_Reader_Concat
758     */
759    function readConcat(&$toConvert, $filename, $stat=array(), $mime=null)
760    {
761        $source =& File_Archive::_convertToReader($toConvert);
762        if (PEAR::isError($source)) {
763            return $source;
764        }
765
766        require_once "File/Archive/Reader/Concat.php";
767        return new File_Archive_Reader_Concat($source, $filename, $stat, $mime);
768    }
769
770    /**
771     * Changes the name of each file in a reader by applying a custom function
772     * The function must return false if the file is to be discarded, or the new
773     * name of the file else
774     *
775     * @param Callable $function Function called to modify the name of the file
776     *        $function takes the name of the file as a parameter and returns the
777     *        new name, or false if the file must be discarded
778     * @param File_Archive_Reader $toConvert The files of this source will be
779     *        modified
780     * @return File_Archive_Reader a new reader that contains the same files
781     *        as $toConvert but with a different name
782     */
783    function changeName($function, &$toConvert)
784    {
785        $source =& File_Archive::_convertToReader($toConvert);
786        if (PEAR::isError($source)) {
787            return $source;
788        }
789
790        require_once "File/Archive/Reader/ChangeName.php";
791        return new File_Archive_Reader_RemoveDirectory($source);
792    }
793
794    /**
795     * Removes from a source the files that do not follow a given predicat
796     *
797     * @param File_Archive_Predicate $predicate Only the files for which
798     *        $predicate->isTrue() will be kept
799     * @param File_Archive_Reader $source Source that will be filtered
800     * @see   File_Archive_Reader_Filter
801     */
802    function filter($predicate, &$toConvert)
803    {
804        $source =& File_Archive::_convertToReader($toConvert);
805        if (PEAR::isError($source)) {
806            return $source;
807        }
808
809        require_once "File/Archive/Reader/Filter.php";
810        return new File_Archive_Reader_Filter($predicate, $source);
811    }
812    /**
813     * Predicate that always evaluate to true
814     *
815     * @see File_Archive_Predicate_True
816     */
817    function predTrue()
818    {
819        require_once "File/Archive/Predicate/True.php";
820        return new File_Archive_Predicate_True();
821    }
822    /**
823     * Predicate that always evaluate to false
824     *
825     * @see File_Archive_Predicate_False
826     */
827    function predFalse()
828    {
829        require_once "File/Archive/Predicate/False.php";
830        return new File_Archive_Predicate_False();
831    }
832    /**
833     * Predicate that evaluates to the logical AND of the parameters
834     * You can add other predicates thanks to the
835     * File_Archive_Predicate_And::addPredicate() function
836     *
837     * @param File_Archive_Predicate (any number of them)
838     * @see File_Archive_Predicate_And
839     */
840    function predAnd()
841    {
842        require_once "File/Archive/Predicate/And.php";
843        $pred = new File_Archive_Predicate_And();
844        $args = func_get_args();
845        foreach ($args as $p) {
846            $pred->addPredicate($p);
847        }
848        return $pred;
849    }
850    /**
851     * Predicate that evaluates to the logical OR of the parameters
852     * You can add other predicates thanks to the
853     * File_Archive_Predicate_Or::addPredicate() function
854     *
855     * @param File_Archive_Predicate (any number of them)
856     * @see File_Archive_Predicate_Or
857     */
858    function predOr()
859    {
860        require_once "File/Archive/Predicate/Or.php";
861        $pred = new File_Archive_Predicate_Or();
862        $args = func_get_args();
863        foreach ($args as $p) {
864            $pred->addPredicate($p);
865        }
866        return $pred;
867    }
868    /**
869     * Negate a predicate
870     *
871     * @param File_Archive_Predicate $pred Predicate to negate
872     * @see File_Archive_Predicate_Not
873     */
874    function predNot($pred)
875    {
876        require_once "File/Archive/Predicate/Not.php";
877        return new File_Archive_Predicate_Not($pred);
878    }
879    /**
880     * Evaluates to true iif the file is larger than a given size
881     *
882     * @param int $size the minimal size of the files (in Bytes)
883     * @see File_Archive_Predicate_MinSize
884     */
885    function predMinSize($size)
886    {
887        require_once "File/Archive/Predicate/MinSize.php";
888        return new File_Archive_Predicate_MinSize($size);
889    }
890    /**
891     * Evaluates to true iif the file has been modified after a given time
892     *
893     * @param int $time Unix timestamp of the minimal modification time of the
894     *        files
895     * @see File_Archive_Predicate_MinTime
896     */
897    function predMinTime($time)
898    {
899        require_once "File/Archive/Predicate/MinTime.php";
900        return new File_Archive_Predicate_MinTime($time);
901    }
902    /**
903     * Evaluates to true iif the file has less that a given number of
904     * directories in its path
905     *
906     * @param int $depth Maximal number of directories in path of the files
907     * @see File_Archive_Predicate_MaxDepth
908     */
909    function predMaxDepth($depth)
910    {
911        require_once "File/Archive/Predicate/MaxDepth.php";
912        return new File_Archive_Predicate_MaxDepth($depth);
913    }
914    /**
915     * Evaluates to true iif the extension of the file is in a given list
916     *
917     * @param array or string $list List or comma separated string of possible
918     * extension of the files
919     * @see File_Archive_Predicate_Extension
920     */
921    function predExtension($list)
922    {
923        require_once "File/Archive/Predicate/Extension.php";
924        return new File_Archive_Predicate_Extension($list);
925    }
926    /**
927     * Evaluates to true iif the MIME type of the file is in a given list
928     *
929     * @param array or string $list List or comma separated string of possible
930     *        MIME types of the files. You may enter wildcards like "image/*" to
931     *        select all the MIME in class image
932     * @see   File_Archive_Predicate_MIME, MIME_Type::isWildcard()
933     */
934    function predMIME($list)
935    {
936        require_once "File/Archive/Predicate/MIME.php";
937        return new File_Archive_Predicate_MIME($list);
938    }
939
940    /**
941     * Evaluates to true iif the name of the file follow a given regular
942     * expression
943     *
944     * @param string $preg regular expression that the filename must follow
945     * @see File_Archive_Predicate_Preg, preg_match()
946     */
947    function predPreg($preg)
948    {
949        require_once "File/Archive/Predicate/Preg.php";
950        return new File_Archive_Predicate_Preg($preg);
951    }
952
953    /**
954     * Evaluates to true iif the name of the file follow a given regular
955     * expression
956     *
957     * @param string $ereg regular expression that the filename must follow
958     * @see File_Archive_Predicate_Ereg, ereg()
959     * @deprecated Make use of predPreg instead for PHP 5.3+ compatability
960     */
961    function predEreg($ereg)
962    {
963        require_once "File/Archive/Predicate/Ereg.php";
964        return new File_Archive_Predicate_Ereg($ereg);
965    }
966    /**
967     * Evaluates to true iif the name of the file follow a given regular
968     * expression (case insensitive version)
969     *
970     * @param string $ereg regular expression that the filename must follow
971     * @see File_Archive_Predicate_Eregi, eregi
972     * @deprecated Make use of predPreg instead for PHP 5.3+ compatability
973     */
974    function predEregi($ereg)
975    {
976        require_once "File/Archive/Predicate/Eregi.php";
977        return new File_Archive_Predicate_Eregi($ereg);
978    }
979    /**
980     * Evaluates to true only after a given number of evaluations
981     * This can be used to select files by index since the evaluation is done
982     * once per file
983     *
984     * @param array The indexes for which the returned predicate will return true
985     *        are the keys of the array
986     *        The predicate will return true if isset($indexes[$pos])
987     */
988    function predIndex($indexes)
989    {
990        require_once "File/Archive/Predicate/Index.php";
991        return new File_Archive_Predicate_Index($indexes);
992    }
993    /**
994     * Custom predicate built by supplying a string expression
995     *
996     * Here are different ways to create a predicate that keeps only files
997     * with names shorter than 100 chars
998     * <sample>
999     *  File_Archive::predCustom("return strlen($name)<100;")
1000     *  File_Archive::predCustom("strlen($name)<100;")
1001     *  File_Archive::predCustom("strlen($name)<100")
1002     *  File_Archive::predCustom("strlen($source->getFilename())<100")
1003     * </sample>
1004     *
1005     * @param string $expression String containing an expression that evaluates
1006     *        to a boolean. If the expression doesn't contain a return
1007     *        statement, it will be added at the begining of the expression
1008     *        A ';' will be added at the end of the expression so that you don't
1009     *        have to write it. You may use the $name variable to refer to the
1010     *        current filename (with path...), $time for the modification time
1011     *        (unix timestamp), $size for the size of the file in bytes, $mime
1012     *        for the MIME type of the file
1013     * @see   File_Archive_Predicate_Custom
1014     */
1015    function predCustom($expression)
1016    {
1017        require_once "File/Archive/Predicate/Custom.php";
1018        return new File_Archive_Predicate_Custom($expression);
1019    }
1020
1021    /**
1022     * Send the files as a mail attachment
1023     *
1024     * @param Mail $mail Object used to send mail (see Mail::factory)
1025     * @param array or String $to An array or a string with comma separated
1026     *        recipients
1027     * @param array $headers The headers that will be passed to the Mail_mime
1028     *        object
1029     * @param string $message Text body of the mail
1030     * @see File_Archive_Writer_Mail
1031     */
1032    function toMail($to, $headers, $message, $mail = null)
1033    {
1034        require_once "File/Archive/Writer/Mail.php";
1035        return new File_Archive_Writer_Mail($to, $headers, $message, $mail);
1036    }
1037    /**
1038     * Write the files on the hard drive
1039     *
1040     * @param string $baseDir if specified, the files will be created in that
1041     *        directory. If they don't exist, the directories will automatically
1042     *        be created
1043     * @see   File_Archive_Writer_Files
1044     */
1045    function toFiles($baseDir = "")
1046    {
1047        require_once "File/Archive/Writer/Files.php";
1048        return new File_Archive_Writer_Files($baseDir);
1049    }
1050    /**
1051     * Send the content of the files to a memory buffer
1052     *
1053     * toMemory returns a writer where the data will be written.
1054     * In this case, the data is accessible using the getData member
1055     *
1056     * toVariable returns a writer that will write into the given
1057     * variable
1058     *
1059     * @param out $data if specified, the data will be written to this buffer
1060     *        Else, you can retrieve the buffer with the
1061     *        File_Archive_Writer_Memory::getData() function
1062     * @see   File_Archive_Writer_Memory
1063     */
1064    function toMemory()
1065    {
1066        $v = '';
1067        return File_Archive::toVariable($v);
1068    }
1069    function toVariable(&$v)
1070    {
1071        require_once "File/Archive/Writer/Memory.php";
1072        return new File_Archive_Writer_Memory($v);
1073    }
1074    /**
1075     * Duplicate the writing operation on two writers
1076     *
1077     * @param File_Archive_Writer $a, $b writers where data will be duplicated
1078     * @see File_Archive_Writer_Multi
1079     */
1080    function toMulti(&$aC, &$bC)
1081    {
1082        $a =& File_Archive::_convertToWriter($aC);
1083        $b =& File_Archive::_convertToWriter($bC);
1084
1085        if (PEAR::isError($a)) {
1086            return $a;
1087        }
1088        if (PEAR::isError($b)) {
1089            return $b;
1090        }
1091
1092        require_once "File/Archive/Writer/Multi.php";
1093        $writer = new File_Archive_Writer_Multi();
1094        $writer->addWriter($a);
1095        $writer->addWriter($b);
1096        return $writer;
1097    }
1098    /**
1099     * Send the content of the files to the standard output (so to the client
1100     * for a website)
1101     *
1102     * @param bool $sendHeaders If true some headers will be sent to force the
1103     *        download of the file. Default value is true
1104     * @see   File_Archive_Writer_Output
1105     */
1106    function toOutput($sendHeaders = true)
1107    {
1108        require_once "File/Archive/Writer/Output.php";
1109        return new File_Archive_Writer_Output($sendHeaders);
1110    }
1111    /**
1112     * Compress the data to a tar, gz, tar/gz or zip format
1113     *
1114     * @param string $filename name of the archive file
1115     * @param File_Archive_Writer $innerWriter writer where the archive will be
1116     *        written
1117     * @param string $type can be one of tgz, tbz, tar, zip, gz, gzip, bz2,
1118     *        bzip2 (default is the extension of $filename) or any composition
1119     *        of them (for example tar.gz or tar.bz2). The case of this
1120     *        parameter is not important.
1121     * @param array $stat Statistics of the archive (see stat function)
1122     * @param bool $autoClose If set to true, $innerWriter will be closed when
1123     *        the returned archive is close. Default value is true.
1124     */
1125    function toArchive($filename, &$toConvert, $type = null,
1126                       $stat = array(), $autoClose = true)
1127    {
1128        $innerWriter =& File_Archive::_convertToWriter($toConvert);
1129        if (PEAR::isError($innerWriter)) {
1130            return $innerWriter;
1131        }
1132        $shortcuts = array("tgz"   , "tbz"    );
1133        $reals     = array("tar.gz", "tar.bz2");
1134
1135        if ($type === null) {
1136            $extensions = strtolower($filename);
1137        } else {
1138            $extensions = strtolower($type);
1139        }
1140        $extensions = explode('.', str_replace($shortcuts, $reals, $extensions));
1141        if ($innerWriter !== null) {
1142            $writer =& $innerWriter;
1143        } else {
1144            $writer = File_Archive::toFiles();
1145        }
1146        $nbCompressions = 0;
1147        $currentFilename = $filename;
1148        while (($extension = array_pop($extensions)) !== null) {
1149            unset($next);
1150            switch($extension) {
1151            case "tar":
1152                require_once "File/Archive/Writer/Tar.php";
1153                $next = new File_Archive_Writer_Tar(
1154                    $currentFilename, $writer, $stat, $autoClose
1155                );
1156                unset($writer); $writer =& $next;
1157                break;
1158            case "zip":
1159                require_once "File/Archive/Writer/Zip.php";
1160                $next = new File_Archive_Writer_Zip(
1161                    $currentFilename, $writer, $stat, $autoClose
1162                );
1163                unset($writer); $writer =& $next;
1164                break;
1165            case "gz":
1166            case "gzip":
1167                require_once "File/Archive/Writer/Gzip.php";
1168                $next = new File_Archive_Writer_Gzip(
1169                    $currentFilename, $writer, $stat, $autoClose
1170                );
1171                unset($writer); $writer =& $next;
1172                break;
1173            case "bz2":
1174            case "bzip2":
1175                require_once "File/Archive/Writer/Bzip2.php";
1176                $next = new File_Archive_Writer_Bzip2(
1177                    $currentFilename, $writer, $stat, $autoClose
1178                );
1179                unset($writer); $writer =& $next;
1180                break;
1181            case "deb":
1182            case "ar":
1183                require_once "File/Archive/Writer/Ar.php";
1184                $next = new File_Archive_Writer_Ar(
1185                    $currentFilename, $writer, $stat, $autoClose
1186                );
1187                unset($writer); $writer =& $next;
1188                break;
1189            default:
1190                if ($type !== null || $nbCompressions == 0) {
1191                    return PEAR::raiseError("Archive $extension unknown");
1192                }
1193                break;
1194            }
1195            $nbCompressions ++;
1196            $autoClose = true;
1197            $currentFilename = implode(".", $extensions);
1198        }
1199        return $writer;
1200    }
1201
1202
1203    /**
1204     * File_Archive::extract($source, $dest) is equivalent to $source->extract($dest)
1205     * If $source is a PEAR error, the error will be returned
1206     * It is thus easier to use this function than $source->extract, since it reduces the number of
1207     * error checking and doesn't force you to define a variable $source
1208     *
1209     * You may use strings as source and dest. In that case the source is automatically
1210     * converted to a reader using File_Archive::read and the dest is converted to a
1211     * writer using File_Archive::appender
1212     * Since PHP doesn't allow to pass literal strings by ref, you will have to use temporary
1213     * variables.
1214     * File_Archive::extract($src = 'archive.zip/', $dest = 'dir') will extract the archive to 'dir'
1215     * It is the same as
1216     * File_Archive::extract(
1217     *    File_Archive::read('archive.zip/'),
1218     *    File_Archive::appender('dir')
1219     * );
1220     * You may use any variable in the extract function ($from/$to, $a/$b...).
1221     *
1222     * @param File_Archive_Reader $source The source that will be read
1223     * @param File_Archive_Writer $dest Where to copy $source files
1224     * @param bool $autoClose if true (default), $dest will be closed after the extraction
1225     * @param int $bufferSize Size of the buffer to use to move data from the reader to the buffer
1226     *        If $bufferSize <= 0 (default), the blockSize option is used
1227     *        You shouldn't need to change that
1228     * @return null or a PEAR error if an error occured
1229     */
1230    function extract(&$sourceToConvert, &$destToConvert, $autoClose = true, $bufferSize = 0)
1231    {
1232        $source =& File_Archive::_convertToReader($sourceToConvert);
1233        if (PEAR::isError($source)) {
1234            return $source;
1235        }
1236        $dest =& File_Archive::_convertToWriter($destToConvert);
1237        return $source->extract($dest, $autoClose, $bufferSize);
1238    }
1239
1240    /**
1241     * Create a writer that can be used to append files to an archive inside a source
1242     * If the archive can't be found in the source, it will be created
1243     * If source is set to null, File_Archive::toFiles will be assumed
1244     * If type is set to null, the type of the archive will be determined looking at
1245     * the extension in the URL
1246     * stat is the array of stat (returned by stat() PHP function of Reader getStat())
1247     * to use if the archive must be created
1248     *
1249     * This function allows to create or append data to nested archives. Only one
1250     * archive will be created and if your creation requires creating several nested
1251     * archives, a PEAR error will be returned
1252     *
1253     * After this call, $source will be closed and should not be used until the
1254     * returned writer is closed.
1255     *
1256     * @param File_Archive_Reader $source A reader where some files will be appended
1257     * @param string $URL URL to reach the archive in the source.
1258     *        if $URL is null, a writer to append files to the $source reader will
1259     *        be returned
1260     * @param bool $unique If true, the duplicate files will be deleted on close
1261     *        Default is false (and setting it to true may have some performance
1262     *        consequences)
1263     * @param string $type Extension of the archive (or null to use the one in the URL)
1264     * @param array $stat Used only if archive is created, array of stat as returned
1265     *        by PHP stat function or Reader getStat function: stats of the archive)
1266     *        Time (index 9) will be overwritten to current time
1267     * @return File_Archive_Writer a writer that you can use to append files to the reader
1268     */
1269    function appenderFromSource(&$toConvert, $URL = null, $unique = null,
1270                                 $type = null, $stat = array())
1271    {
1272        $source =& File_Archive::_convertToReader($toConvert);
1273        if (PEAR::isError($source)) {
1274            return $source;
1275        }
1276        if ($unique == null) {
1277            $unique = File_Archive::getOption("appendRemoveDuplicates");
1278        }
1279
1280        //Do not report the fact that the archive does not exist as an error
1281        PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
1282
1283        if ($URL === null) {
1284            $result =& $source;
1285        } else {
1286            if ($type === null) {
1287                $result = File_Archive::_readSource($source, $URL.'/', $reachable, $baseDir);
1288            } else {
1289                $result = File_Archive::readArchive(
1290                            $type,
1291                            File_Archive::_readSource($source, $URL, $reachable, $baseDir)
1292                          );
1293            }
1294        }
1295
1296        PEAR::popErrorHandling();
1297
1298        if (!PEAR::isError($result)) {
1299            if ($unique) {
1300                require_once "File/Archive/Writer/UniqueAppender.php";
1301                return new File_Archive_Writer_UniqueAppender($result);
1302            } else {
1303                return $result->makeAppendWriter();
1304            }
1305        }
1306
1307        //The source can't be found and has to be created
1308        $stat[9] = $stat['mtime'] = time();
1309
1310        if (empty($baseDir)) {
1311            if ($source !== null) {
1312                $writer =& $source->makeWriter();
1313            } else {
1314                $writer =& File_Archive::toFiles();
1315            }
1316            if (PEAR::isError($writer)) {
1317                return $writer;
1318            }
1319
1320            PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
1321            $result = File_Archive::toArchive($reachable, $writer, $type);
1322            PEAR::popErrorHandling();
1323
1324            if (PEAR::isError($result)) {
1325                $result = File_Archive::toFiles($reachable);
1326            }
1327        } else {
1328            $reachedSource = File_Archive::readSource($source, $reachable);
1329            if (PEAR::isError($reachedSource)) {
1330                return $reachedSource;
1331            }
1332            $writer = $reachedSource->makeWriter();
1333            if (PEAR::isError($writer)) {
1334                return $writer;
1335            }
1336
1337            PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
1338            $result = File_Archive::toArchive($baseDir, $writer, $type);
1339            PEAR::popErrorHandling();
1340
1341            if (PEAR::isError($result)) {
1342                require_once "File/Archive/Writer/AddBaseName.php";
1343                $result = new File_Archive_Writer_AddBaseName(
1344                                       $baseDir, $writer);
1345                if (PEAR::isError($result)) {
1346                    return $result;
1347                }
1348            }
1349        }
1350        return $result;
1351    }
1352
1353    /**
1354     * Create a writer that allows appending new files to an existing archive
1355     * This function actes as appendToSource with source being the system files
1356     * $URL can't be null here
1357     *
1358     * @param File_Archive_Reader $source A reader where some files will be appended
1359     * @return File_Archive_Writer a writer that you can use to append files to the reader
1360     */
1361    function appender($URL, $unique = null, $type = null, $stat = array())
1362    {
1363        $source = null;
1364        return File_Archive::appenderFromSource($source, $URL, $unique, $type, $stat);
1365    }
1366
1367    /**
1368     * Remove the files that follow a given predicate from the source
1369     * If URL is null, the files will be removed from the source directly
1370     * Else, URL must link to a source from which the files will be removed
1371     *
1372     * @param File_Archive_Predicate $pred The files that follow the predicate
1373     *        (for which $pred->isTrue($source) is true) will be erased
1374     * @param File_Archive_Reader $source A reader that contains the files to remove
1375     */
1376    function removeFromSource(&$pred, &$toConvert, $URL = null)
1377    {
1378        $source =& File_Archive::_convertToReader($toConvert);
1379        if (PEAR::isError($source)) {
1380            return $source;
1381        }
1382        if ($URL === null) {
1383            $result = &$source;
1384        } else {
1385            if (substr($URL, -1) !== '/') {
1386                $URL .= '/';
1387            }
1388            $result = File_Archive::readSource($source, $URL);
1389        }
1390
1391        $writer = $result->makeWriterRemoveFiles($pred);
1392        if (PEAR::isError($writer)) {
1393            return $writer;
1394        }
1395        $writer->close();
1396    }
1397
1398    /**
1399     * Remove the files that follow a given predicate from the archive specified
1400     * in $URL
1401     *
1402     * @param $URL URL of the archive where some files must be removed
1403     */
1404    function remove($pred, $URL)
1405    {
1406        $source = null;
1407        return File_Archive::removeFromSource($pred, $source, $URL);
1408    }
1409
1410    /**
1411     * Remove duplicates from a source, keeping the most recent one (or the one that has highest pos in
1412     * the archive if the files have same date or no date specified)
1413     *
1414     * @param File_Archive_Reader a reader that may contain duplicates
1415     */
1416    function removeDuplicatesFromSource(&$toConvert, $URL = null)
1417    {
1418        $source =& File_Archive::_convertToReader($toConvert);
1419        if (PEAR::isError($source)) {
1420            return $source;
1421        }
1422        if ($URL !== null && substr($URL, -1) != '/') {
1423            $URL .= '/';
1424        }
1425
1426        if ($source === null) {
1427            $source = File_Archive::read($URL);
1428        }
1429
1430        require_once "File/Archive/Predicate/Duplicate.php";
1431        $pred = new File_Archive_Predicate_Duplicate($source);
1432        $source->close();
1433        return File_Archive::removeFromSource(
1434            $pred,
1435            $source,
1436            null
1437        );
1438    }
1439
1440    /**
1441     * Remove duplicates from the archive specified in the URL
1442     */
1443    function removeDuplicates($URL)
1444    {
1445        $source = null;
1446        return File_Archive::removeDuplicatesFromSource($source, $URL);
1447    }
1448}
1449
1450?>
1451