1<?php
2/**
3 * PEAR_PackageFile, package.xml parsing utility class
4 *
5 * PHP versions 4 and 5
6 *
7 * @category   pear
8 * @package    PEAR
9 * @author     Greg Beaver <cellog@php.net>
10 * @copyright  1997-2009 The Authors
11 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
12 * @link       http://pear.php.net/package/PEAR
13 * @since      File available since Release 1.4.0a1
14 */
15
16/**
17 * needed for PEAR_VALIDATE_* constants
18 */
19require_once 'PEAR/Validate.php';
20/**
21 * Error code if the package.xml <package> tag does not contain a valid version
22 */
23define('PEAR_PACKAGEFILE_ERROR_NO_PACKAGEVERSION', 1);
24/**
25 * Error code if the package.xml <package> tag version is not supported (version 1.0 and 1.1 are the only supported versions,
26 * currently
27 */
28define('PEAR_PACKAGEFILE_ERROR_INVALID_PACKAGEVERSION', 2);
29/**
30 * Abstraction for the package.xml package description file
31 *
32 * @category   pear
33 * @package    PEAR
34 * @author     Greg Beaver <cellog@php.net>
35 * @copyright  1997-2009 The Authors
36 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
37 * @version    Release: @PEAR-VER@
38 * @link       http://pear.php.net/package/PEAR
39 * @since      Class available since Release 1.4.0a1
40 */
41class PEAR_PackageFile
42{
43    /**
44     * @var PEAR_Config
45     */
46    var $_config;
47    var $_debug;
48
49    var $_logger = false;
50    /**
51     * @var boolean
52     */
53    var $_rawReturn = false;
54
55    /**
56     * helper for extracting Archive_Tar errors
57     * @var array
58     * @access private
59     */
60    var $_extractErrors = array();
61
62    /**
63     *
64     * @param   PEAR_Config $config
65     * @param   ?   $debug
66     * @param   string @tmpdir Optional temporary directory for uncompressing
67     *          files
68     */
69    function __construct(&$config, $debug = false)
70    {
71        $this->_config = $config;
72        $this->_debug = $debug;
73    }
74
75    /**
76     * Turn off validation - return a parsed package.xml without checking it
77     *
78     * This is used by the package-validate command
79     */
80    function rawReturn()
81    {
82        $this->_rawReturn = true;
83    }
84
85    function setLogger(&$l)
86    {
87        $this->_logger = &$l;
88    }
89
90    /**
91     * Create a PEAR_PackageFile_Parser_v* of a given version.
92     * @param   int $version
93     * @return  PEAR_PackageFile_Parser_v1|PEAR_PackageFile_Parser_v1
94     */
95    function &parserFactory($version)
96    {
97        if (!in_array($version{0}, array('1', '2'))) {
98            $a = false;
99            return $a;
100        }
101
102        include_once 'PEAR/PackageFile/Parser/v' . $version{0} . '.php';
103        $version = $version{0};
104        $class = "PEAR_PackageFile_Parser_v$version";
105        $a = new $class;
106        return $a;
107    }
108
109    /**
110     * For simpler unit-testing
111     * @return string
112     */
113    function getClassPrefix()
114    {
115        return 'PEAR_PackageFile_v';
116    }
117
118    /**
119     * Create a PEAR_PackageFile_v* of a given version.
120     * @param   int $version
121     * @return  PEAR_PackageFile_v1|PEAR_PackageFile_v1
122     */
123    function &factory($version)
124    {
125        if (!in_array($version{0}, array('1', '2'))) {
126            $a = false;
127            return $a;
128        }
129
130        include_once 'PEAR/PackageFile/v' . $version{0} . '.php';
131        $version = $version{0};
132        $class = $this->getClassPrefix() . $version;
133        $a = new $class;
134        return $a;
135    }
136
137    /**
138     * Create a PEAR_PackageFile_v* from its toArray() method
139     *
140     * WARNING: no validation is performed, the array is assumed to be valid,
141     * always parse from xml if you want validation.
142     * @param   array $arr
143     * @return PEAR_PackageFileManager_v1|PEAR_PackageFileManager_v2
144     * @uses    factory() to construct the returned object.
145     */
146    function &fromArray($arr)
147    {
148        if (isset($arr['xsdversion'])) {
149            $obj = &$this->factory($arr['xsdversion']);
150            if ($this->_logger) {
151                $obj->setLogger($this->_logger);
152            }
153
154            $obj->setConfig($this->_config);
155            $obj->fromArray($arr);
156            return $obj;
157        }
158
159        if (isset($arr['package']['attribs']['version'])) {
160            $obj = &$this->factory($arr['package']['attribs']['version']);
161        } else {
162            $obj = &$this->factory('1.0');
163        }
164
165        if ($this->_logger) {
166            $obj->setLogger($this->_logger);
167        }
168
169        $obj->setConfig($this->_config);
170        $obj->fromArray($arr);
171        return $obj;
172    }
173
174    /**
175     * Create a PEAR_PackageFile_v* from an XML string.
176     * @access  public
177     * @param   string $data contents of package.xml file
178     * @param   int $state package state (one of PEAR_VALIDATE_* constants)
179     * @param   string $file full path to the package.xml file (and the files
180     *          it references)
181     * @param   string $archive optional name of the archive that the XML was
182     *          extracted from, if any
183     * @return  PEAR_PackageFile_v1|PEAR_PackageFile_v2
184     * @uses    parserFactory() to construct a parser to load the package.
185     */
186    function &fromXmlString($data, $state, $file, $archive = false)
187    {
188        if (preg_match('/<package[^>]+version=[\'"]([0-9]+\.[0-9]+)[\'"]/', $data, $packageversion)) {
189            if (!in_array($packageversion[1], array('1.0', '2.0', '2.1'))) {
190                return PEAR::raiseError('package.xml version "' . $packageversion[1] .
191                    '" is not supported, only 1.0, 2.0, and 2.1 are supported.');
192            }
193
194            $object = &$this->parserFactory($packageversion[1]);
195            if ($this->_logger) {
196                $object->setLogger($this->_logger);
197            }
198
199            $object->setConfig($this->_config);
200            $pf = $object->parse($data, $file, $archive);
201            if (PEAR::isError($pf)) {
202                return $pf;
203            }
204
205            if ($this->_rawReturn) {
206                return $pf;
207            }
208
209            if (!$pf->validate($state)) {;
210                if ($this->_config->get('verbose') > 0
211                    && $this->_logger && $pf->getValidationWarnings(false)
212                ) {
213                    foreach ($pf->getValidationWarnings(false) as $warning) {
214                        $this->_logger->log(0, 'ERROR: ' . $warning['message']);
215                    }
216                }
217
218                $a = PEAR::raiseError('Parsing of package.xml from file "' . $file . '" failed',
219                    2, null, null, $pf->getValidationWarnings());
220                return $a;
221            }
222
223            if ($this->_logger && $pf->getValidationWarnings(false)) {
224                foreach ($pf->getValidationWarnings() as $warning) {
225                    $this->_logger->log(0, 'WARNING: ' . $warning['message']);
226                }
227            }
228
229            if (method_exists($pf, 'flattenFilelist')) {
230                $pf->flattenFilelist(); // for v2
231            }
232
233            return $pf;
234        } elseif (preg_match('/<package[^>]+version=[\'"]([^"\']+)[\'"]/', $data, $packageversion)) {
235            $a = PEAR::raiseError('package.xml file "' . $file .
236                '" has unsupported package.xml <package> version "' . $packageversion[1] . '"');
237            return $a;
238        } else {
239            if (!class_exists('PEAR_ErrorStack')) {
240                require_once 'PEAR/ErrorStack.php';
241            }
242
243            PEAR_ErrorStack::staticPush('PEAR_PackageFile',
244                PEAR_PACKAGEFILE_ERROR_NO_PACKAGEVERSION,
245                'warning', array('xml' => $data), 'package.xml "' . $file .
246                    '" has no package.xml <package> version');
247            $object = &$this->parserFactory('1.0');
248            $object->setConfig($this->_config);
249            $pf = $object->parse($data, $file, $archive);
250            if (PEAR::isError($pf)) {
251                return $pf;
252            }
253
254            if ($this->_rawReturn) {
255                return $pf;
256            }
257
258            if (!$pf->validate($state)) {
259                $a = PEAR::raiseError('Parsing of package.xml from file "' . $file . '" failed',
260                    2, null, null, $pf->getValidationWarnings());
261                return $a;
262            }
263
264            if ($this->_logger && $pf->getValidationWarnings(false)) {
265                foreach ($pf->getValidationWarnings() as $warning) {
266                    $this->_logger->log(0, 'WARNING: ' . $warning['message']);
267                }
268            }
269
270            if (method_exists($pf, 'flattenFilelist')) {
271                $pf->flattenFilelist(); // for v2
272            }
273
274            return $pf;
275        }
276    }
277
278    /**
279     * Register a temporary file or directory.  When the destructor is
280     * executed, all registered temporary files and directories are
281     * removed.
282     *
283     * @param string  $file  name of file or directory
284     * @return  void
285     */
286    static function addTempFile($file)
287    {
288        $GLOBALS['_PEAR_Common_tempfiles'][] = $file;
289    }
290
291    /**
292     * Create a PEAR_PackageFile_v* from a compressed Tar or Tgz file.
293     * @access  public
294     * @param string contents of package.xml file
295     * @param int package state (one of PEAR_VALIDATE_* constants)
296     * @return  PEAR_PackageFile_v1|PEAR_PackageFile_v2
297     * @using   Archive_Tar to extract the files
298     * @using   fromPackageFile() to load the package after the package.xml
299     *          file is extracted.
300     */
301    function &fromTgzFile($file, $state)
302    {
303        if (!class_exists('Archive_Tar')) {
304            require_once 'Archive/Tar.php';
305        }
306
307        $tar = new Archive_Tar($file);
308        if ($this->_debug <= 1) {
309            $tar->pushErrorHandling(PEAR_ERROR_RETURN);
310        }
311
312        $content = $tar->listContent();
313        if ($this->_debug <= 1) {
314            $tar->popErrorHandling();
315        }
316
317        if (!is_array($content)) {
318            if (is_string($file) && strlen($file) < 255 &&
319                  (!file_exists($file) || !@is_file($file))) {
320                $ret = PEAR::raiseError("could not open file \"$file\"");
321                return $ret;
322            }
323
324            $file = realpath($file);
325            $ret = PEAR::raiseError("Could not get contents of package \"$file\"".
326                                     '. Invalid tgz file.');
327            return $ret;
328        }
329
330        if (!count($content) && !@is_file($file)) {
331            $ret = PEAR::raiseError("could not open file \"$file\"");
332            return $ret;
333        }
334
335        $xml      = null;
336        $origfile = $file;
337        foreach ($content as $file) {
338            $name = $file['filename'];
339            if ($name == 'package2.xml') { // allow a .tgz to distribute both versions
340                $xml = $name;
341                break;
342            }
343
344            if ($name == 'package.xml') {
345                $xml = $name;
346                break;
347            } elseif (preg_match('/package.xml$/', $name, $match)) {
348                $xml = $name;
349                break;
350            }
351        }
352
353        $tmpdir = System::mktemp('-t "' . $this->_config->get('temp_dir') . '" -d pear');
354        if ($tmpdir === false) {
355            $ret = PEAR::raiseError("there was a problem with getting the configured temp directory");
356            return $ret;
357        }
358
359        PEAR_PackageFile::addTempFile($tmpdir);
360
361        $this->_extractErrors();
362        PEAR::staticPushErrorHandling(PEAR_ERROR_CALLBACK, array($this, '_extractErrors'));
363
364        if (!$xml || !$tar->extractList(array($xml), $tmpdir)) {
365            $extra = implode("\n", $this->_extractErrors());
366            if ($extra) {
367                $extra = ' ' . $extra;
368            }
369
370            PEAR::staticPopErrorHandling();
371            $ret = PEAR::raiseError('could not extract the package.xml file from "' .
372                $origfile . '"' . $extra);
373            return $ret;
374        }
375
376        PEAR::staticPopErrorHandling();
377        $ret = &PEAR_PackageFile::fromPackageFile("$tmpdir/$xml", $state, $origfile);
378        return $ret;
379    }
380
381    /**
382     * helper callback for extracting Archive_Tar errors
383     *
384     * @param PEAR_Error|null $err
385     * @return array
386     * @access private
387     */
388    function _extractErrors($err = null)
389    {
390        static $errors = array();
391        if ($err === null) {
392            $e = $errors;
393            $errors = array();
394            return $e;
395        }
396        $errors[] = $err->getMessage();
397    }
398
399    /**
400     * Create a PEAR_PackageFile_v* from a package.xml file.
401     *
402     * @access public
403     * @param   string  $descfile  name of package xml file
404     * @param   int     $state package state (one of PEAR_VALIDATE_* constants)
405     * @param   string|false $archive name of the archive this package.xml came
406     *          from, if any
407     * @return  PEAR_PackageFile_v1|PEAR_PackageFile_v2
408     * @uses    PEAR_PackageFile::fromXmlString to create the oject after the
409     *          XML is loaded from the package.xml file.
410     */
411    function &fromPackageFile($descfile, $state, $archive = false)
412    {
413        $fp = false;
414        if (is_string($descfile) && strlen($descfile) < 255 &&
415             (
416              !file_exists($descfile) || !is_file($descfile) || !is_readable($descfile)
417              || (!$fp = @fopen($descfile, 'r'))
418             )
419        ) {
420            $a = PEAR::raiseError("Unable to open $descfile");
421            return $a;
422        }
423
424        // read the whole thing so we only get one cdata callback
425        // for each block of cdata
426        fclose($fp);
427        $data = file_get_contents($descfile);
428        $ret = &PEAR_PackageFile::fromXmlString($data, $state, $descfile, $archive);
429        return $ret;
430    }
431
432    /**
433     * Create a PEAR_PackageFile_v* from a .tgz archive or package.xml file.
434     *
435     * This method is able to extract information about a package from a .tgz
436     * archive or from a XML package definition file.
437     *
438     * @access public
439     * @param   string  $info file name
440     * @param   int     $state package state (one of PEAR_VALIDATE_* constants)
441     * @return  PEAR_PackageFile_v1|PEAR_PackageFile_v2
442     * @uses    fromPackageFile() if the file appears to be XML
443     * @uses    fromTgzFile() to load all non-XML files
444     */
445    function &fromAnyFile($info, $state)
446    {
447        if (is_dir($info)) {
448            $dir_name = realpath($info);
449            if (file_exists($dir_name . '/package.xml')) {
450                $info = PEAR_PackageFile::fromPackageFile($dir_name .  '/package.xml', $state);
451            } elseif (file_exists($dir_name .  '/package2.xml')) {
452                $info = PEAR_PackageFile::fromPackageFile($dir_name .  '/package2.xml', $state);
453            } else {
454                $info = PEAR::raiseError("No package definition found in '$info' directory");
455            }
456
457            return $info;
458        }
459
460        $fp = false;
461        if (is_string($info) && strlen($info) < 255 &&
462             (file_exists($info) || ($fp = @fopen($info, 'r')))
463        ) {
464
465            if ($fp) {
466                fclose($fp);
467            }
468
469            $tmp = substr($info, -4);
470            if ($tmp == '.xml') {
471                $info = &PEAR_PackageFile::fromPackageFile($info, $state);
472            } elseif ($tmp == '.tar' || $tmp == '.tgz') {
473                $info = &PEAR_PackageFile::fromTgzFile($info, $state);
474            } else {
475                $fp   = fopen($info, 'r');
476                $test = fread($fp, 5);
477                fclose($fp);
478                if ($test == '<?xml') {
479                    $info = &PEAR_PackageFile::fromPackageFile($info, $state);
480                } else {
481                    $info = &PEAR_PackageFile::fromTgzFile($info, $state);
482                }
483            }
484
485            return $info;
486        }
487
488        $info = PEAR::raiseError("Cannot open '$info' for parsing");
489        return $info;
490    }
491}
492