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: Archive.php,v 1.85 2005/08/16 08:48:59 vincentlascaux Exp $
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 = split('_', $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::predEreg('^'.$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            require_once "File/Archive/Reader/ChangeName.php";
337
338            if ($uncompressionLevel != 0) {
339                require_once "File/Archive/Reader/Uncompress.php";
340                $result = new File_Archive_Reader_Uncompress(
341                    new File_Archive_Reader_Directory($std, '', $directoryDepth),
342                    $uncompressionLevel
343                );
344            } else {
345                $result = new File_Archive_Reader_Directory($std, '', $directoryDepth);
346            }
347
348            if ($directoryDepth >= 0) {
349                require_once 'File/Archive/Reader/Filter.php';
350                require_once 'File/Archive/Predicate/MaxDepth.php';
351
352                $tmp =& File_Archive::filter(
353                    new File_Archive_Predicate_MaxDepth($directoryDepth),
354                    $result
355                );
356                unset($result);
357                $result =& $tmp;
358            }
359            if (!empty($realSymbolic)) {
360                if ($symbolic === null) {
361                    $realSymbolic = '';
362                }
363                $tmp =& new File_Archive_Reader_AddBaseName(
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/Predicate/MaxDepth.php";
437
438                $tmp = new File_Archive_Reader_Filter(
439                    new File_Archive_Predicate(
440                        $directoryDepth +
441                        substr_count(substr($std, $pos+1), '/')
442                    ),
443                    $result
444                );
445                unset($result);
446                $result =& $tmp;
447            }
448
449            if ($std != $realSymbolic) {
450                require_once "File/Archive/Reader/ChangeName.php";
451
452                //Change the base name to the symbolic one if necessary
453                $tmp = new File_Archive_Reader_ChangeBaseName(
454                    $std,
455                    $realSymbolic,
456                    $result
457                );
458                unset($result);
459                $result =& $tmp;
460            }
461        }
462
463        $cacheCondition = File_Archive::getOption('cacheCondition');
464        if ($cacheCondition !== false &&
465            preg_match($cacheCondition, $URL)) {
466            $tmp =& File_Archive::cache($result);
467            unset($result);
468            $result =& $tmp;
469        }
470
471        return $result;
472    }
473    function read($URL, $symbolic = null,
474                  $uncompression = 0, $directoryDepth = -1)
475    {
476        $source = null;
477        return File_Archive::readSource($source, $URL, $symbolic, $uncompression, $directoryDepth);
478    }
479
480    /**
481     * Create a file reader on an uploaded file. The reader will read
482     * $_FILES[$name]['tmp_name'] and will have $_FILES[$name]['name']
483     * as a symbolic filename.
484     *
485     * A PEAR error is returned if one of the following happen
486     *  - $_FILES[$name] is not set
487     *  - $_FILES[$name]['error'] is not 0
488     *  - is_uploaded_file returns false
489     *
490     * @param string $name Index of the file in the $_FILES array
491     * @return File_Archive_Reader File reader on the uploaded file
492     */
493    function readUploadedFile($name)
494    {
495        if (!isset($_FILES[$name])) {
496            return PEAR::raiseError("File $name has not been uploaded");
497        }
498        switch ($_FILES[$name]['error']) {
499        case 0:
500            //No error
501            break;
502        case 1:
503            return PEAR::raiseError(
504                        "The upload size limit didn't allow to upload file ".
505                        $_FILES[$name]['name']
506                    );
507        case 2:
508            return PEAR::raiseError(
509                        "The form size limit didn't allow to upload file ".
510                        $_FILES[$name]['name']
511                   );
512        case 3:
513            return PEAR::raiseError(
514                        "The file was not entirely uploaded"
515                   );
516        case 4:
517            return PEAR::raiseError(
518                        "The uploaded file is empty"
519                   );
520        default:
521            return PEAR::raiseError(
522                        "Unknown error ".$_FILES[$name]['error']." in file upload. ".
523                        "Please, report a bug"
524                   );
525        }
526        if (!is_uploaded_file($_FILES[$name]['tmp_name'])) {
527            return PEAR::raiseError("The file is not an uploaded file");
528        }
529
530        require_once "File/Archive/Reader/File.php";
531        return new File_Archive_Reader_File(
532                    $_FILES[$name]['tmp_name'],
533                    $_FILES[$name]['name'],
534                    $_FILES[$name]['type']
535               );
536    }
537
538    /**
539     * Adds a cache layer above the specified reader
540     * The data of the reader is saved in a temporary file for future access.
541     * The cached reader will be read only once, even if you read it several times.
542     * This can be usefull to read compressed files or downloaded files (from http or ftp)
543     *
544     * @param mixed $toConvert The reader to cache
545     *        It can be a File_Archive_Reader or a string, which will be converted using the
546     *        read function
547     */
548    function cache(&$toConvert)
549    {
550        $source =& File_Archive::_convertToReader($toConvert);
551        if (PEAR::isError($source)) {
552            return $source;
553        }
554
555        require_once 'File/Archive/Reader/Cache.php';
556        return new File_Archive_Reader_Cache($source);
557    }
558
559    /**
560     * Try to interpret the object as a reader
561     * Strings are converted to readers using File_Archive::read
562     * Arrays are converted to readers using File_Archive::readMulti
563     *
564     * @access private
565     */
566    function &_convertToReader(&$source)
567    {
568        if (is_string($source)) {
569            $cacheCondition = File_Archive::getOption('cacheCondition');
570            if ($cacheCondition !== false &&
571                preg_match($cacheCondition, $source)) {
572                return File_Archive::cache(File_Archive::read($source));
573            } else {
574                return File_Archive::read($source);
575            }
576        } else if (is_array($source)) {
577            return File_Archive::readMulti($source);
578        } else {
579            return $source;
580        }
581     }
582
583    /**
584     * Try to interpret the object as a writer
585     * Strings are converted to writers using File_Archive::appender
586     * Arrays are converted to writers using a multi writer
587     *
588     * @access private
589     */
590    function &_convertToWriter(&$dest)
591    {
592        if (is_string($dest)) {
593            return File_Archive::appender($dest);
594        } else if (is_array($dest)) {
595            require_once 'File/Archive/Writer/Multi.php';
596            $writer = new File_Archive_Writer_Multi();
597            foreach($dest as $key => $foo) {
598                $writer->addWriter($dest[$key]);
599            }
600        } else {
601            return $dest;
602        }
603    }
604
605    /**
606     * Check if a file with a specific extension can be read as an archive
607     * with File_Archive::read*
608     * This function is case sensitive.
609     *
610     * @param string $extension the checked extension
611     * @return bool whether this file can be understood reading its extension
612     *         Currently, supported extensions are tar, zip, gz, tgz, tbz, bz2,
613     *         bzip2, ar, deb
614     */
615    function isKnownExtension($extension)
616    {
617        return $extension == 'tar'   ||
618               $extension == 'zip'   ||
619               $extension == 'gz'    ||
620               $extension == 'tgz'   ||
621               $extension == 'tbz'   ||
622               $extension == 'bz2'   ||
623               $extension == 'bzip2' ||
624               $extension == 'ar'    ||
625               $extension == 'deb'   /* ||
626               $extension == 'cab'   ||
627               $extension == 'rar' */;
628    }
629
630    /**
631     * Create a reader that will read the single file source $source as
632     * a specific archive
633     *
634     * @param string $extension determines the kind of archive $source contains
635     *        $extension is case sensitive
636     * @param File_Archive_Reader $source stores the archive
637     * @param bool $sourceOpened specifies if the archive is already opened
638     *        if false, next will be called on source
639     *        Closing the returned archive will close $source iif $sourceOpened
640     *        is true
641     * @return A File_Archive_Reader that uncompresses the archive contained in
642     *         $source interpreting it as a $extension archive
643     *         If $extension is not handled return false
644     */
645    function readArchive($extension, &$toConvert, $sourceOpened = false)
646    {
647        $source =& File_Archive::_convertToReader($toConvert);
648        if (PEAR::isError($source)) {
649            return $source;
650        }
651
652        switch($extension) {
653        case 'tgz':
654            return File_Archive::readArchive('tar',
655                    File_Archive::readArchive('gz', $source, $sourceOpened)
656                    );
657        case 'tbz':
658            return File_Archive::readArchive('tar',
659                    File_Archive::readArchive('bz2', $source, $sourceOpened)
660                    );
661        case 'tar':
662            require_once 'File/Archive/Reader/Tar.php';
663            return new File_Archive_Reader_Tar($source, $sourceOpened);
664
665        case 'gz':
666        case 'gzip':
667            require_once 'File/Archive/Reader/Gzip.php';
668            return new File_Archive_Reader_Gzip($source, $sourceOpened);
669
670        case 'zip':
671            require_once 'File/Archive/Reader/Zip.php';
672            return new File_Archive_Reader_Zip($source, $sourceOpened);
673
674        case 'bz2':
675        case 'bzip2':
676            require_once 'File/Archive/Reader/Bzip2.php';
677            return new File_Archive_Reader_Bzip2($source, $sourceOpened);
678
679        case 'deb':
680        case 'ar':
681            require_once 'File/Archive/Reader/Ar.php';
682            return new File_Archive_Reader_Ar($source, $sourceOpened);
683
684/*        case 'cab':
685            require_once 'File/Archive/Reader/Cab.php';
686            return new File_Archive_Reader_Cab($source, $sourceOpened);
687
688
689        case 'rar':
690            require_once "File/Archive/Reader/Rar.php";
691            return new File_Archive_Reader_Rar($source, $sourceOpened); */
692
693        default:
694            return false;
695        }
696    }
697
698    /**
699     * Contains only one file with data read from a memory buffer
700     *
701     * @param string $memory content of the file
702     * @param string $filename public name of the file
703     * @param array $stat statistics of the file. Index 7 (size) will be
704     *        overwritten to match the size of $memory
705     * @param string $mime mime type of the file. Default will determine the
706     *        mime type thanks to the extension of $filename
707     * @see File_Archive_Reader_Memory
708     */
709    function readMemory($memory, $filename, $stat=array(), $mime=null)
710    {
711        require_once "File/Archive/Reader/Memory.php";
712        return new File_Archive_Reader_Memory($memory, $filename, $stat, $mime);
713    }
714
715    /**
716     * Contains several other sources. Take care the sources don't have several
717     * files with the same filename. The sources are given as a parameter, or
718     * can be added thanks to the reader addSource method
719     *
720     * @param array $sources Array of strings or readers that will be added to
721     *        the multi reader. If the parameter is a string, a reader will be
722     *        built thanks to the read function
723     * @see   File_Archive_Reader_Multi, File_Archive::read()
724     */
725    function readMulti($sources = array())
726    {
727        require_once "File/Archive/Reader/Multi.php";
728        $result = new File_Archive_Reader_Multi();
729        foreach ($sources as $index => $foo) {
730            $s =& File_Archive::_convertToReader($sources[$index]);
731            if (PEAR::isError($s)) {
732                return $s;
733            } else {
734                $result->addSource($s);
735            }
736        }
737        return $result;
738    }
739    /**
740     * Make the files of a source appear as one large file whose content is the
741     * concatenation of the content of all the files
742     *
743     * @param File_Archive_Reader $source The source whose files must be
744     *        concatened
745     * @param string $filename name of the only file of the created reader
746     * @param array $stat statistics of the file. Index 7 (size) will be
747     *        overwritten to match the total size of the files
748     * @param string $mime mime type of the file. Default will determine the
749     *        mime type thanks to the extension of $filename
750     * @see   File_Archive_Reader_Concat
751     */
752    function readConcat(&$toConvert, $filename, $stat=array(), $mime=null)
753    {
754        $source =& File_Archive::_convertToReader($toConvert);
755        if (PEAR::isError($source)) {
756            return $source;
757        }
758
759        require_once "File/Archive/Reader/Concat.php";
760        return new File_Archive_Reader_Concat($source, $filename, $stat, $mime);
761    }
762
763    /**
764     * Removes from a source the files that do not follow a given predicat
765     *
766     * @param File_Archive_Predicate $predicate Only the files for which
767     *        $predicate->isTrue() will be kept
768     * @param File_Archive_Reader $source Source that will be filtered
769     * @see   File_Archive_Reader_Filter
770     */
771    function filter($predicate, &$toConvert)
772    {
773        $source =& File_Archive::_convertToReader($toConvert);
774        if (PEAR::isError($source)) {
775            return $source;
776        }
777
778        require_once "File/Archive/Reader/Filter.php";
779        return new File_Archive_Reader_Filter($predicate, $source);
780    }
781    /**
782     * Predicate that always evaluate to true
783     *
784     * @see File_Archive_Predicate_True
785     */
786    function predTrue()
787    {
788        require_once "File/Archive/Predicate/True.php";
789        return new File_Archive_Predicate_True();
790    }
791    /**
792     * Predicate that always evaluate to false
793     *
794     * @see File_Archive_Predicate_False
795     */
796    function predFalse()
797    {
798        require_once "File/Archive/Predicate/False.php";
799        return new File_Archive_Predicate_False();
800    }
801    /**
802     * Predicate that evaluates to the logical AND of the parameters
803     * You can add other predicates thanks to the
804     * File_Archive_Predicate_And::addPredicate() function
805     *
806     * @param File_Archive_Predicate (any number of them)
807     * @see File_Archive_Predicate_And
808     */
809    function predAnd()
810    {
811        require_once "File/Archive/Predicate/And.php";
812        $pred = new File_Archive_Predicate_And();
813        $args = func_get_args();
814        foreach ($args as $p) {
815            $pred->addPredicate($p);
816        }
817        return $pred;
818    }
819    /**
820     * Predicate that evaluates to the logical OR of the parameters
821     * You can add other predicates thanks to the
822     * File_Archive_Predicate_Or::addPredicate() function
823     *
824     * @param File_Archive_Predicate (any number of them)
825     * @see File_Archive_Predicate_Or
826     */
827    function predOr()
828    {
829        require_once "File/Archive/Predicate/Or.php";
830        $pred = new File_Archive_Predicate_Or();
831        $args = func_get_args();
832        foreach ($args as $p) {
833            $pred->addPredicate($p);
834        }
835        return $pred;
836    }
837    /**
838     * Negate a predicate
839     *
840     * @param File_Archive_Predicate $pred Predicate to negate
841     * @see File_Archive_Predicate_Not
842     */
843    function predNot($pred)
844    {
845        require_once "File/Archive/Predicate/Not.php";
846        return new File_Archive_Predicate_Not($pred);
847    }
848    /**
849     * Evaluates to true iif the file is larger than a given size
850     *
851     * @param int $size the minimal size of the files (in Bytes)
852     * @see File_Archive_Predicate_MinSize
853     */
854    function predMinSize($size)
855    {
856        require_once "File/Archive/Predicate/MinSize.php";
857        return new File_Archive_Predicate_MinSize($size);
858    }
859    /**
860     * Evaluates to true iif the file has been modified after a given time
861     *
862     * @param int $time Unix timestamp of the minimal modification time of the
863     *        files
864     * @see File_Archive_Predicate_MinTime
865     */
866    function predMinTime($time)
867    {
868        require_once "File/Archive/Predicate/MinTime.php";
869        return new File_Archive_Predicate_MinTime($time);
870    }
871    /**
872     * Evaluates to true iif the file has less that a given number of
873     * directories in its path
874     *
875     * @param int $depth Maximal number of directories in path of the files
876     * @see File_Archive_Predicate_MaxDepth
877     */
878    function predMaxDepth($depth)
879    {
880        require_once "File/Archive/Predicate/MaxDepth.php";
881        return new File_Archive_Predicate_MaxDepth($depth);
882    }
883    /**
884     * Evaluates to true iif the extension of the file is in a given list
885     *
886     * @param array or string $list List or comma separated string of possible
887     * extension of the files
888     * @see File_Archive_Predicate_Extension
889     */
890    function predExtension($list)
891    {
892        require_once "File/Archive/Predicate/Extension.php";
893        return new File_Archive_Predicate_Extension($list);
894    }
895    /**
896     * Evaluates to true iif the MIME type of the file is in a given list
897     *
898     * @param array or string $list List or comma separated string of possible
899     *        MIME types of the files. You may enter wildcards like "image/*" to
900     *        select all the MIME in class image
901     * @see   File_Archive_Predicate_MIME, MIME_Type::isWildcard()
902     */
903    function predMIME($list)
904    {
905        require_once "File/Archive/Predicate/MIME.php";
906        return new File_Archive_Predicate_MIME($list);
907    }
908    /**
909     * Evaluates to true iif the name of the file follow a given regular
910     * expression
911     *
912     * @param string $ereg regular expression that the filename must follow
913     * @see File_Archive_Predicate_Ereg, ereg()
914     */
915    function predEreg($ereg)
916    {
917        require_once "File/Archive/Predicate/Ereg.php";
918        return new File_Archive_Predicate_Ereg($ereg);
919    }
920    /**
921     * Evaluates to true iif the name of the file follow a given regular
922     * expression (case insensitive version)
923     *
924     * @param string $ereg regular expression that the filename must follow
925     * @see File_Archive_Predicate_Eregi, eregi
926     */
927    function predEregi($ereg)
928    {
929        require_once "File/Archive/Predicate/Eregi.php";
930        return new File_Archive_Predicate_Eregi($ereg);
931    }
932    /**
933     * Evaluates to true only after a given number of evaluations
934     * This can be used to select files by index since the evaluation is done
935     * once per file
936     *
937     * @param array The indexes for which the returned predicate will return true
938     *        are the keys of the array
939     *        The predicate will return true if isset($indexes[$pos])
940     */
941    function predIndex($indexes)
942    {
943        require_once "File/Archive/Predicate/Index.php";
944        return new File_Archive_Predicate_Index($indexes);
945    }
946    /**
947     * Custom predicate built by supplying a string expression
948     *
949     * Here are different ways to create a predicate that keeps only files
950     * with names shorter than 100 chars
951     * <sample>
952     *  File_Archive::predCustom("return strlen($name)<100;")
953     *  File_Archive::predCustom("strlen($name)<100;")
954     *  File_Archive::predCustom("strlen($name)<100")
955     *  File_Archive::predCustom("strlen($source->getFilename())<100")
956     * </sample>
957     *
958     * @param string $expression String containing an expression that evaluates
959     *        to a boolean. If the expression doesn't contain a return
960     *        statement, it will be added at the begining of the expression
961     *        A ';' will be added at the end of the expression so that you don't
962     *        have to write it. You may use the $name variable to refer to the
963     *        current filename (with path...), $time for the modification time
964     *        (unix timestamp), $size for the size of the file in bytes, $mime
965     *        for the MIME type of the file
966     * @see   File_Archive_Predicate_Custom
967     */
968    function predCustom($expression)
969    {
970        require_once "File/Archive/Predicate/Custom.php";
971        return new File_Archive_Predicate_Custom($expression);
972    }
973
974    /**
975     * Send the files as a mail attachment
976     *
977     * @param Mail $mail Object used to send mail (see Mail::factory)
978     * @param array or String $to An array or a string with comma separated
979     *        recipients
980     * @param array $headers The headers that will be passed to the Mail_mime
981     *        object
982     * @param string $message Text body of the mail
983     * @see File_Archive_Writer_Mail
984     */
985    function toMail($to, $headers, $message, $mail = null)
986    {
987        require_once "File/Archive/Writer/Mail.php";
988        return new File_Archive_Writer_Mail($to, $headers, $message, $mail);
989    }
990    /**
991     * Write the files on the hard drive
992     *
993     * @param string $baseDir if specified, the files will be created in that
994     *        directory. If they don't exist, the directories will automatically
995     *        be created
996     * @see   File_Archive_Writer_Files
997     */
998    function toFiles($baseDir = "")
999    {
1000        require_once "File/Archive/Writer/Files.php";
1001        return new File_Archive_Writer_Files($baseDir);
1002    }
1003    /**
1004     * Send the content of the files to a memory buffer
1005     *
1006     * toMemory returns a writer where the data will be written.
1007     * In this case, the data is accessible using the getData member
1008     *
1009     * toVariable returns a writer that will write into the given
1010     * variable
1011     *
1012     * @param out $data if specified, the data will be written to this buffer
1013     *        Else, you can retrieve the buffer with the
1014     *        File_Archive_Writer_Memory::getData() function
1015     * @see   File_Archive_Writer_Memory
1016     */
1017    function toMemory()
1018    {
1019        $v = '';
1020        return File_Archive::toVariable($v);
1021    }
1022    function toVariable(&$v)
1023    {
1024        require_once "File/Archive/Writer/Memory.php";
1025        return new File_Archive_Writer_Memory($v);
1026    }
1027    /**
1028     * Duplicate the writing operation on two writers
1029     *
1030     * @param File_Archive_Writer $a, $b writers where data will be duplicated
1031     * @see File_Archive_Writer_Multi
1032     */
1033    function toMulti(&$aC, &$bC)
1034    {
1035        $a =& File_Archive::_convertToWriter($aC);
1036        $b =& File_Archive::_convertToWriter($bC);
1037
1038        if (PEAR::isError($a)) {
1039            return $a;
1040        }
1041        if (PEAR::isError($b)) {
1042            return $b;
1043        }
1044
1045        require_once "File/Archive/Writer/Multi.php";
1046        $writer = new File_Archive_Writer_Multi();
1047        $writer->addWriter($a);
1048        $writer->addWriter($b);
1049        return $writer;
1050    }
1051    /**
1052     * Send the content of the files to the standard output (so to the client
1053     * for a website)
1054     *
1055     * @param bool $sendHeaders If true some headers will be sent to force the
1056     *        download of the file. Default value is true
1057     * @see   File_Archive_Writer_Output
1058     */
1059    function toOutput($sendHeaders = true)
1060    {
1061        require_once "File/Archive/Writer/Output.php";
1062        return new File_Archive_Writer_Output($sendHeaders);
1063    }
1064    /**
1065     * Compress the data to a tar, gz, tar/gz or zip format
1066     *
1067     * @param string $filename name of the archive file
1068     * @param File_Archive_Writer $innerWriter writer where the archive will be
1069     *        written
1070     * @param string $type can be one of tgz, tbz, tar, zip, gz, gzip, bz2,
1071     *        bzip2 (default is the extension of $filename) or any composition
1072     *        of them (for example tar.gz or tar.bz2). The case of this
1073     *        parameter is not important.
1074     * @param array $stat Statistics of the archive (see stat function)
1075     * @param bool $autoClose If set to true, $innerWriter will be closed when
1076     *        the returned archive is close. Default value is true.
1077     */
1078    function toArchive($filename, &$toConvert, $type = null,
1079                       $stat = array(), $autoClose = true)
1080    {
1081        $innerWriter =& File_Archive::_convertToWriter($toConvert);
1082        if (PEAR::isError($innerWriter)) {
1083            return $innerWriter;
1084        }
1085        $shortcuts = array("tgz"   , "tbz"    );
1086        $reals     = array("tar.gz", "tar.bz2");
1087
1088        if ($type === null) {
1089            $extensions = strtolower($filename);
1090        } else {
1091            $extensions = strtolower($type);
1092        }
1093        $extensions = explode('.', str_replace($shortcuts, $reals, $extensions));
1094        if ($innerWriter !== null) {
1095            $writer =& $innerWriter;
1096        } else {
1097            $writer = File_Archive::toFiles();
1098        }
1099        $nbCompressions = 0;
1100        $currentFilename = $filename;
1101        while (($extension = array_pop($extensions)) !== null) {
1102            unset($next);
1103            switch($extension) {
1104            case "tar":
1105                require_once "File/Archive/Writer/Tar.php";
1106                $next = new File_Archive_Writer_Tar(
1107                    $currentFilename, $writer, $stat, $autoClose
1108                );
1109                unset($writer); $writer =& $next;
1110                break;
1111            case "zip":
1112                require_once "File/Archive/Writer/Zip.php";
1113                $next = new File_Archive_Writer_Zip(
1114                    $currentFilename, $writer, $stat, $autoClose
1115                );
1116                unset($writer); $writer =& $next;
1117                break;
1118            case "gz":
1119            case "gzip":
1120                require_once "File/Archive/Writer/Gzip.php";
1121                $next = new File_Archive_Writer_Gzip(
1122                    $currentFilename, $writer, $stat, $autoClose
1123                );
1124                unset($writer); $writer =& $next;
1125                break;
1126            case "bz2":
1127            case "bzip2":
1128                require_once "File/Archive/Writer/Bzip2.php";
1129                $next = new File_Archive_Writer_Bzip2(
1130                    $currentFilename, $writer, $stat, $autoClose
1131                );
1132                unset($writer); $writer =& $next;
1133                break;
1134            case "deb":
1135            case "ar":
1136                require_once "File/Archive/Writer/Ar.php";
1137                $next = new File_Archive_Writer_Ar(
1138                    $currentFilename, $writer, $stat, $autoClose
1139                );
1140                unset($writer); $writer =& $next;
1141                break;
1142            default:
1143                if ($type !== null || $nbCompressions == 0) {
1144                    return PEAR::raiseError("Archive $extension unknown");
1145                }
1146                break;
1147            }
1148            $nbCompressions ++;
1149            $autoClose = true;
1150            $currentFilename = implode(".", $extensions);
1151        }
1152        return $writer;
1153    }
1154
1155
1156    /**
1157     * File_Archive::extract($source, $dest) is equivalent to $source->extract($dest)
1158     * If $source is a PEAR error, the error will be returned
1159     * It is thus easier to use this function than $source->extract, since it reduces the number of
1160     * error checking and doesn't force you to define a variable $source
1161     *
1162     * You may use strings as source and dest. In that case the source is automatically
1163     * converted to a reader using File_Archive::read and the dest is converted to a
1164     * writer using File_Archive::appender
1165     * Since PHP doesn't allow to pass literal strings by ref, you will have to use temporary
1166     * variables.
1167     * File_Archive::extract($src = 'archive.zip/', $dest = 'dir') will extract the archive to 'dir'
1168     * It is the same as
1169     * File_Archive::extract(
1170     *    File_Archive::read('archive.zip/'),
1171     *    File_Archive::appender('dir')
1172     * );
1173     * You may use any variable in the extract function ($from/$to, $a/$b...).
1174     *
1175     * @param File_Archive_Reader $source The source that will be read
1176     * @param File_Archive_Writer $dest Where to copy $source files
1177     * @param bool $autoClose if true (default), $dest will be closed after the extraction
1178     * @param int $bufferSize Size of the buffer to use to move data from the reader to the buffer
1179     *        If $bufferSize <= 0 (default), the blockSize option is used
1180     *        You shouldn't need to change that
1181     * @return null or a PEAR error if an error occured
1182     */
1183    function extract(&$sourceToConvert, &$destToConvert, $autoClose = true, $bufferSize = 0)
1184    {
1185        $source =& File_Archive::_convertToReader($sourceToConvert);
1186        if (PEAR::isError($source)) {
1187            return $source;
1188        }
1189        $dest =& File_Archive::_convertToWriter($destToConvert);
1190        return $source->extract($dest, $autoClose, $bufferSize);
1191    }
1192
1193    /**
1194     * Create a writer that can be used to append files to an archive inside a source
1195     * If the archive can't be found in the source, it will be created
1196     * If source is set to null, File_Archive::toFiles will be assumed
1197     * If type is set to null, the type of the archive will be determined looking at
1198     * the extension in the URL
1199     * stat is the array of stat (returned by stat() PHP function of Reader getStat())
1200     * to use if the archive must be created
1201     *
1202     * This function allows to create or append data to nested archives. Only one
1203     * archive will be created and if your creation requires creating several nested
1204     * archives, a PEAR error will be returned
1205     *
1206     * After this call, $source will be closed and should not be used until the
1207     * returned writer is closed.
1208     *
1209     * @param File_Archive_Reader $source A reader where some files will be appended
1210     * @param string $URL URL to reach the archive in the source.
1211     *        if $URL is null, a writer to append files to the $source reader will
1212     *        be returned
1213     * @param bool $unique If true, the duplicate files will be deleted on close
1214     *        Default is false (and setting it to true may have some performance
1215     *        consequences)
1216     * @param string $type Extension of the archive (or null to use the one in the URL)
1217     * @param array $stat Used only if archive is created, array of stat as returned
1218     *        by PHP stat function or Reader getStat function: stats of the archive)
1219     *        Time (index 9) will be overwritten to current time
1220     * @return File_Archive_Writer a writer that you can use to append files to the reader
1221     */
1222    function appenderFromSource(&$toConvert, $URL = null, $unique = null,
1223                                 $type = null, $stat = array())
1224    {
1225        $source =& File_Archive::_convertToReader($toConvert);
1226        if (PEAR::isError($source)) {
1227            return $source;
1228        }
1229        if ($unique == null) {
1230            $unique = File_Archive::getOption("appendRemoveDuplicates");
1231        }
1232
1233        //Do not report the fact that the archive does not exist as an error
1234        PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
1235
1236        if ($URL === null) {
1237            $result =& $source;
1238        } else {
1239            if ($type === null) {
1240                $result = File_Archive::_readSource($source, $URL.'/', $reachable, $baseDir);
1241            } else {
1242                $result = File_Archive::readArchive(
1243                            $type,
1244                            File_Archive::_readSource($source, $URL, $reachable, $baseDir)
1245                          );
1246            }
1247        }
1248
1249        PEAR::popErrorHandling();
1250
1251        if (!PEAR::isError($result)) {
1252            if ($unique) {
1253                require_once "File/Archive/Writer/UniqueAppender.php";
1254                return new File_Archive_Writer_UniqueAppender($result);
1255            } else {
1256                return $result->makeAppendWriter();
1257            }
1258        }
1259
1260        //The source can't be found and has to be created
1261        $stat[9] = $stat['mtime'] = time();
1262
1263        if (empty($baseDir)) {
1264            if ($source !== null) {
1265                $writer =& $source->makeWriter();
1266            } else {
1267                $writer =& File_Archive::toFiles();
1268            }
1269            if (PEAR::isError($writer)) {
1270                return $writer;
1271            }
1272
1273            PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
1274            $result = File_Archive::toArchive($reachable, $writer, $type);
1275            PEAR::popErrorHandling();
1276
1277            if (PEAR::isError($result)) {
1278                $result = File_Archive::toFiles($reachable);
1279            }
1280        } else {
1281            $reachedSource = File_Archive::readSource($source, $reachable);
1282            if (PEAR::isError($reachedSource)) {
1283                return $reachedSource;
1284            }
1285            $writer = $reachedSource->makeWriter();
1286            if (PEAR::isError($writer)) {
1287                return $writer;
1288            }
1289
1290            PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
1291            $result = File_Archive::toArchive($baseDir, $writer, $type);
1292            PEAR::popErrorHandling();
1293
1294            if (PEAR::isError($result)) {
1295                require_once "File/Archive/Writer/AddBaseName.php";
1296                $result = new File_Archive_Writer_AddBaseName(
1297                                       $baseDir, $writer);
1298                if (PEAR::isError($result)) {
1299                    return $result;
1300                }
1301            }
1302        }
1303        return $result;
1304    }
1305
1306    /**
1307     * Create a writer that allows appending new files to an existing archive
1308     * This function actes as appendToSource with source being the system files
1309     * $URL can't be null here
1310     *
1311     * @param File_Archive_Reader $source A reader where some files will be appended
1312     * @return File_Archive_Writer a writer that you can use to append files to the reader
1313     */
1314    function appender($URL, $unique = null, $type = null, $stat = array())
1315    {
1316        $source = null;
1317        return File_Archive::appenderFromSource($source, $URL, $unique, $type, $stat);
1318    }
1319
1320    /**
1321     * Remove the files that follow a given predicate from the source
1322     * If URL is null, the files will be removed from the source directly
1323     * Else, URL must link to a source from which the files will be removed
1324     *
1325     * @param File_Archive_Predicate $pred The files that follow the predicate
1326     *        (for which $pred->isTrue($source) is true) will be erased
1327     * @param File_Archive_Reader $source A reader that contains the files to remove
1328     */
1329    function removeFromSource(&$pred, &$toConvert, $URL = null)
1330    {
1331        $source =& File_Archive::_convertToReader($toConvert);
1332        if (PEAR::isError($source)) {
1333            return $source;
1334        }
1335        if ($URL === null) {
1336            $result = &$source;
1337        } else {
1338            if (substr($URL, -1) !== '/') {
1339                $URL .= '/';
1340            }
1341            $result = File_Archive::readSource($source, $URL);
1342        }
1343
1344        $writer = $result->makeWriterRemoveFiles($pred);
1345        if (PEAR::isError($writer)) {
1346            return $writer;
1347        }
1348        $writer->close();
1349    }
1350
1351    /**
1352     * Remove the files that follow a given predicate from the archive specified
1353     * in $URL
1354     *
1355     * @param $URL URL of the archive where some files must be removed
1356     */
1357    function remove($pred, $URL)
1358    {
1359        $source = null;
1360        return File_Archive::removeFromSource($pred, $source, $URL);
1361    }
1362
1363    /**
1364     * Remove duplicates from a source, keeping the most recent one (or the one that has highest pos in
1365     * the archive if the files have same date or no date specified)
1366     *
1367     * @param File_Archive_Reader a reader that may contain duplicates
1368     */
1369    function removeDuplicatesFromSource(&$toConvert, $URL = null)
1370    {
1371        $source =& File_Archive::_convertToReader($toConvert);
1372        if (PEAR::isError($source)) {
1373            return $source;
1374        }
1375        if ($URL !== null && substr($URL, -1) != '/') {
1376            $URL .= '/';
1377        }
1378
1379        if ($source === null) {
1380            $source = File_Archive::read($URL);
1381        }
1382
1383        require_once "File/Archive/Predicate/Duplicate.php";
1384        $pred = new File_Archive_Predicate_Duplicate($source);
1385        $source->close();
1386        return File_Archive::removeFromSource(
1387            $pred,
1388            $source,
1389            null
1390        );
1391    }
1392
1393    /**
1394     * Remove duplicates from the archive specified in the URL
1395     */
1396    function removeDuplicates($URL)
1397    {
1398        $source = null;
1399        return File_Archive::removeDuplicatesFromSource($source, $URL);
1400    }
1401}
1402
1403?>