1<?php
2/**
3 * PEAR_PackageFileManager2, like PEAR_PackageFileManager, is designed to
4 * create and manipulate package.xml version 2.0.
5 *
6 * PHP versions 5 and 7
7 *
8 * @category  PEAR
9 * @package   PEAR_PackageFileManager2
10 * @author    Greg Beaver <cellog@php.net>
11 * @copyright 2003-2015 The PEAR Group
12 * @license   New BSD, Revised
13 * @link      http://pear.php.net/package/PEAR_PackageFileManager2
14 * @since     File available since Release 1.0.0alpha1
15 */
16/**
17 * PEAR Packagefile parser
18 */
19require_once 'PEAR/PackageFile.php';
20
21/**
22 * PEAR Packagefile version 2.0
23 */
24require_once 'PEAR/PackageFile/v2/rw.php';
25/**#@+
26 * Error Codes
27 */
28define('PEAR_PACKAGEFILEMANAGER2_NOPKGDIR',                     3);
29define('PEAR_PACKAGEFILEMANAGER2_NOBASEDIR',                    4);
30define('PEAR_PACKAGEFILEMANAGER2_GENERATOR_NOTFOUND',           5);
31define('PEAR_PACKAGEFILEMANAGER2_GENERATOR_NOTFOUND_ANYWHERE',  6);
32define('PEAR_PACKAGEFILEMANAGER2_CANTWRITE_PKGFILE',            7);
33define('PEAR_PACKAGEFILEMANAGER2_DEST_UNWRITABLE',              8);
34define('PEAR_PACKAGEFILEMANAGER2_CANTCOPY_PKGFILE',             9);
35define('PEAR_PACKAGEFILEMANAGER2_CANTOPEN_TMPPKGFILE',         10);
36define('PEAR_PACKAGEFILEMANAGER2_PATH_DOESNT_EXIST',           11);
37define('PEAR_PACKAGEFILEMANAGER2_DIR_DOESNT_EXIST',            13);
38define('PEAR_PACKAGEFILEMANAGER2_RUN_SETOPTIONS',              14);
39define('PEAR_PACKAGEFILEMANAGER2_NO_FILES',                    20);
40define('PEAR_PACKAGEFILEMANAGER2_IGNORED_EVERYTHING',          21);
41define('PEAR_PACKAGEFILEMANAGER2_INVALID_PACKAGE',             22);
42define('PEAR_PACKAGEFILEMANAGER2_INVALID_REPLACETYPE',         23);
43define('PEAR_PACKAGEFILEMANAGER2_INVALID_ROLE',                24);
44define('PEAR_PACKAGEFILEMANAGER2_CVS_PACKAGED',                26);
45define('PEAR_PACKAGEFILEMANAGER2_NO_PHPCOMPATINFO',            27);
46define('PEAR_PACKAGEFILEMANAGER2_INVALID_POSTINSTALLSCRIPT',   28);
47define('PEAR_PACKAGEFILEMANAGER2_PKGDIR_NOTREAL',              29);
48define('PEAR_PACKAGEFILEMANAGER2_OUTPUTDIR_NOTREAL',           30);
49define('PEAR_PACKAGEFILEMANAGER2_PATHTOPKGDIR_NOTREAL',        31);
50/**#@-*/
51/**
52 * Error messages
53 * @global array $GLOBALS['_PEAR_PACKAGEFILEMANAGER2_ERRORS']
54 * @access private
55 */
56$GLOBALS['_PEAR_PACKAGEFILEMANAGER2_ERRORS'] =
57array(
58    'en' =>
59    array(
60        PEAR_PACKAGEFILEMANAGER2_NOPKGDIR =>
61            'Package source base directory (option \'packagedirectory\') must be ' .
62            'specified in PEAR_PackageFileManager2 setOptions',
63        PEAR_PACKAGEFILEMANAGER2_PKGDIR_NOTREAL =>
64            'Package source base directory (option \'packagedirectory\') must be ' .
65            'an existing directory (was "%s")',
66        PEAR_PACKAGEFILEMANAGER2_PATHTOPKGDIR_NOTREAL =>
67            'Path to a Package file to read in (option \'pathtopackagefile\') must be ' .
68            'an existing directory (was "%s")',
69        PEAR_PACKAGEFILEMANAGER2_OUTPUTDIR_NOTREAL =>
70            'output directory (option \'outputdirectory\') must be ' .
71            'an existing directory (was "%s")',
72        PEAR_PACKAGEFILEMANAGER2_NOBASEDIR =>
73            'Package install base directory (option \'baseinstalldir\') must be ' .
74            'specified in PEAR_PackageFileManager2 setOptions',
75        PEAR_PACKAGEFILEMANAGER2_GENERATOR_NOTFOUND =>
76            'Base class "%s" can\'t be located',
77        PEAR_PACKAGEFILEMANAGER2_GENERATOR_NOTFOUND_ANYWHERE =>
78            'Base class "%s" can\'t be located in default or user-specified directories',
79        PEAR_PACKAGEFILEMANAGER2_CANTWRITE_PKGFILE =>
80            'Failed to write package.xml file to destination directory',
81        PEAR_PACKAGEFILEMANAGER2_DEST_UNWRITABLE =>
82            'Destination directory "%s" is unwritable',
83        PEAR_PACKAGEFILEMANAGER2_CANTCOPY_PKGFILE =>
84            'Failed to copy package.xml.tmp file to package.xml',
85        PEAR_PACKAGEFILEMANAGER2_CANTOPEN_TMPPKGFILE =>
86            'Failed to open temporary file "%s" for writing',
87        PEAR_PACKAGEFILEMANAGER2_PATH_DOESNT_EXIST =>
88            'package.xml file path "%s" doesn\'t exist or isn\'t a directory',
89        PEAR_PACKAGEFILEMANAGER2_DIR_DOESNT_EXIST =>
90            'Package source base directory "%s" doesn\'t exist or isn\'t a directory',
91        PEAR_PACKAGEFILEMANAGER2_RUN_SETOPTIONS =>
92            'Run $managerclass->setOptions() before any other methods',
93        PEAR_PACKAGEFILEMANAGER2_NO_FILES =>
94            'No files found, check the path "%s"',
95        PEAR_PACKAGEFILEMANAGER2_IGNORED_EVERYTHING =>
96            'No files left, check the path "%s" and ignore option "%s"',
97        PEAR_PACKAGEFILEMANAGER2_INVALID_PACKAGE =>
98            'Package validation failed:%s%s',
99        PEAR_PACKAGEFILEMANAGER2_INVALID_REPLACETYPE =>
100            'Replacement Type must be one of "%s", was passed "%s"',
101        PEAR_PACKAGEFILEMANAGER2_INVALID_POSTINSTALLSCRIPT =>
102            'Invalid post-install script task: %s',
103        PEAR_PACKAGEFILEMANAGER2_INVALID_ROLE =>
104            'Invalid file role passed to addRole, must be one of "%s", was passed "%s"',
105        PEAR_PACKAGEFILEMANAGER2_CVS_PACKAGED =>
106            'path "%path%" contains CVS directory',
107        PEAR_PACKAGEFILEMANAGER2_NO_PHPCOMPATINFO =>
108            'pear/PHP_CompatInfo is not installed, cannot detect dependencies',
109       ),
110        // other language translations go here
111     );
112/**
113 * PEAR_PackageFileManager2, like PEAR_PackageFileManager, is designed to
114 * create and manipulate package.xml version 2.0.
115 *
116 * The PEAR_PackageFileManager2 class can work directly with PEAR_PackageFileManager
117 * to create parallel package.xml files, version 1.0 and 2.0, that represent the
118 * same project, but take advantage of package.xml 2.0-specific features.
119 *
120 * Like PEAR_PackageFileManager, The PEAR_PackageFileManager2 class uses a plugin system
121 * to generate the list of files in a package.  This allows both standard recursive
122 * directory parsing (plugin type file) and more intelligent options
123 * such as the CVS browser {@link PEAR_PackageFileManager_Cvs}, which
124 * grabs all files in a local CVS checkout to create the list, ignoring
125 * any other local files.
126 *
127 * Example usage is similar to PEAR_PackageFileManager:
128 * <code>
129 * <?php
130 * require_once('PEAR/PackageFileManager2.php');
131 * PEAR::setErrorHandling(PEAR_ERROR_DIE);
132 * //require_once 'PEAR/Config.php';
133 * //PEAR_Config::singleton('/path/to/unusualpearconfig.ini');
134 * // use the above lines if the channel information is not validating
135 * $pfm = new PEAR_PackageFileManager2;
136 * // for an existing package.xml use
137 * // $pfm = {@link importOptions()} instead
138 * $e = $pfm->setOptions(
139 * array('baseinstalldir' => 'PhpDocumentor',
140 *  'packagedirectory' => 'C:/Web Pages/chiara/phpdoc2/',
141 *  'filelistgenerator' => 'cvs', // generate from cvs, use file for directory
142 *  'ignore' => array('TODO', 'tests/'), // ignore TODO, all files in tests/
143 *  'installexceptions' => array('phpdoc' => '/*'), // baseinstalldir ="/" for phpdoc
144 *  'dir_roles' => array('tutorials' => 'doc'),
145 *  'exceptions' => array('README' => 'doc', // README would be data, now is doc
146 *                        'PHPLICENSE.txt' => 'doc'))); // same for the license
147 * $pfm->setPackage('MyPackage');
148 * $pfm->setSummary('this is my package');
149 * $pfm->setDescription('this is my package description');
150 * $pfm->setChannel('mychannel.example.com');
151 * $pfm->setAPIVersion('1.0.0');
152 * $pfm->setReleaseVersion('1.2.1');
153 * $pfm->setReleaseStability('stable');
154 * $pfm->setAPIStability('stable');
155 * $pfm->setNotes("We've implemented many new and exciting features");
156 * $pfm->setPackageType('php'); // this is a PEAR-style php script package
157 * $pfm->addRelease(); // set up a release section
158 * $pfm->setOSInstallCondition('windows');
159 * $pfm->addInstallAs('pear-phpdoc.bat', 'phpdoc.bat');
160 * $pfm->addIgnoreToRelease('pear-phpdoc');
161 * $pfm->addRelease(); // add another release section for all other OSes
162 * $pfm->addInstallAs('pear-phpdoc', 'phpdoc');
163 * $pfm->addIgnoreToRelease('pear-phpdoc.bat');
164 * $pfm->addRole('pkg', 'doc'); // add a new role mapping
165 * $pfm->setPhpDep('4.2.0');
166 * $pfm->setPearinstallerDep('1.4.0a12');
167 * $pfm->addMaintainer('lead', 'cellog', 'Greg Beaver', 'cellog@php.net');
168 * $pfm->setLicense('PHP License', 'http://www.php.net/license');
169 * $pfm->generateContents(); // create the <contents> tag
170 * // replace @PHP-BIN@ in this file with the path to php executable!  pretty neat
171 * $test->addReplacement('pear-phpdoc', 'pear-config', '@PHP-BIN@', 'php_bin');
172 * $test->addReplacement('pear-phpdoc.bat', 'pear-config', '@PHP-BIN@', 'php_bin');
173 * $pkg = &$pfm->exportCompatiblePackageFile1(); // get a PEAR_PackageFile object
174 * // note use of {@link debugPackageFile()} - this is VERY important
175 * if (isset($_GET['make']) || (isset($_SERVER['argv']) && @$_SERVER['argv'][1] == 'make')) {
176 *     $pkg->writePackageFile();
177 *     $pfm->writePackageFile();
178 * } else {
179 *     $pkg->debugPackageFile();
180 *     $pfm->debugPackageFile();
181 * }
182 * ?>
183 * </code>
184 *
185 * In addition, a package.xml file can now be generated from
186 * scratch, with the usage of new options package, summary, description, and
187 * the use of the {@link addLead(), addDeveloper(), addContributor(), addHelper()} methods
188 *
189 * @category  PEAR
190 * @package   PEAR_PackageFileManager2
191 * @author    Greg Beaver <cellog@php.net>
192 * @copyright 2003-2015 The PEAR Group
193 * @license   New BSD, Revised
194 * @version   Release: 1.0.4
195 * @link      http://pear.php.net/package/PEAR_PackageFileManager2
196 * @since     Class available since Release 1.0.0alpha1
197 */
198class PEAR_PackageFileManager2 extends PEAR_PackageFile_v2_rw
199{
200    /**
201     * Format: array(array(regexp-ready string to search for whole path,
202     * regexp-ready string to search for basename of ignore strings),...)
203     * @var false|array
204     * @access private
205     * @since  1.0.0a1
206     */
207    var $_ignore = false;
208
209    /**
210     * Contents of the package.xml file
211     * @var PEAR_PackageFile_v2
212     * @access private
213     * @since  1.0.0a1
214     */
215    var $_packageXml = false;
216
217    /**
218     * List of warnings
219     * @var array
220     * @access private
221     * @since  1.0.0a1
222     */
223    var $_warningStack = array();
224
225    /**
226     * flag used to determine whether to use PHP_CompatInfo to detect deps
227     * @var boolean
228     * @access private
229     * @since  1.0.0a1
230     */
231    var $_detectDependencies = false;
232
233    /**
234     * The original contents of the old package.xml, if any
235     * @var PEAR_PackageFile_v2|false
236     * @access private
237     * @since  1.0.0a1
238     */
239    var $_oldPackageFile = false;
240
241    /**
242     * Collection of subpackages
243     *
244     * This collection is used to handle coordination between the contents of
245     * related packages whose files reside in the same development directory
246     * @var array
247     * @access private
248     * @since  1.0.0a1
249     */
250    var $_subpackages = array();
251
252    /**
253     * List of package.xml generation options
254     * @var string
255     * @access private
256     * @since  1.0.0a1
257     */
258    var $_options = array(
259                      'packagefile'       => 'package.xml',
260                      'filelistgenerator' => 'file',
261                      'license'           => 'New BSD License',
262                      'baseinstalldir'    => '',
263                      'changelogoldtonew' => true,
264                      'roles' =>
265                        array(
266                            'h'    => 'src',
267                            'c'    => 'src',
268                            'cpp'  => 'src',
269                            'in'   => 'src',
270                            'm4'   => 'src',
271                            'w32'  => 'src',
272                            'dll'  => 'ext',
273                            'php'  => 'php',
274                            'html' => 'doc',
275                            '*'    => 'data',
276                             ),
277                      'dir_roles' =>
278                        array(
279                            'docs'     => 'doc',
280                            'examples' => 'doc',
281                            'tests'    => 'test',
282                            'scripts'  => 'script',
283                             ),
284                      'exceptions'        => array(),
285                      'installexceptions' => array(),
286                      'ignore'  => array(),
287                      'include' => false,
288                      'notes'   => '',
289                      'changelognotes'    => false,
290                      'outputdirectory'   => false,
291                      'pathtopackagefile' => false,
292                      'lang' => 'en',
293                      'configure_options'       => array(),
294                      'replacements'            => array(),
295                      'globalreplacements'      => array(),
296                      'globalreplaceexceptions' => array(),
297                      'simpleoutput'      => false,
298                      'addhiddenfiles'    => false,
299                      'cleardependencies' => false,
300                      'clearcontents'     => true,
301                      'clearchangelog'    => false,
302                      );
303
304    /**
305     * @see    setOptions()
306     * @access public
307     * @since  1.0.0a1
308     */
309    function __construct()
310    {
311        parent::__construct();
312        $config = &PEAR_Config::singleton();
313        $this->setConfig($config);
314    }
315
316    /**
317     * Add a pattern to include when generating the file list.
318     *
319     * If any include options are specified, all files that do not match the
320     * inclusion patterns will be ignored
321     *
322     * Note that to match partial path entries, you must start with a *,
323     * so to match "data/README" you need to use "*data/README"
324     *
325     * @param string|array $include file pattern to include
326     * @param bool         $clear   (optional) if true, the include array will be reset
327     *                        (useful for cloned packagefiles)
328     *
329     * @return void
330     * @access public
331     * @since  1.6.0a2
332     */
333    function addInclude($include, $clear = false)
334    {
335        if ($clear) {
336            $this->_options['include'] = array();
337        }
338
339        if (is_array($include)) {
340            foreach ($include as $fn) {
341                if (is_string($fn)) {
342                    $this->_options['include'][] = $fn;
343                }
344            }
345            return;
346        }
347        $this->_options['include'][] = $include;
348    }
349
350    /**
351     * Add an <ignore> tag to a <phprelease> tag
352     *
353     * @param string $path full path to filename to ignore
354     *
355     * @return void
356     * @access public
357     * @see    PEAR_PackageFile_v2_rw::addIgnore()
358     * @since  1.6.0a3
359     */
360    function addIgnoreToRelease($path)
361    {
362        return parent::addIgnore($path);
363    }
364
365    /**
366     * Add a pattern or patterns to ignore when generating the file list
367     *
368     * @param string|array $ignore file pattern to ignore
369     * @param bool         $clear  (optional) if true, the include array will be reset
370     *                             (useful for cloned packagefiles)
371     *
372     * @return void
373     * @access public
374     * @since  1.6.0a2
375     */
376    function addIgnore($ignore, $clear = false)
377    {
378        if ($clear) {
379            $this->_options['ignore'] = array();
380        }
381        if (is_array($ignore)) {
382            foreach ($ignore as $fn) {
383                if (is_string($fn)) {
384                    $this->_options['ignore'][] = $fn;
385                }
386            }
387            return;
388        }
389        $this->_options['ignore'][] = $ignore;
390    }
391
392    /**
393     * Set package.xml generation options
394     *
395     * The options array is indexed as follows:
396     * <code>
397     * $options = array('option_name' => <optionvalue>);
398     * </code>
399     *
400     * The documentation below simplifies this description through
401     * the use of option_name without quotes
402     *
403     * Configuration options:
404     * - lang: lang controls the language in which error messages are
405     *         displayed.  There are currently only English error messages,
406     *         but any contributed will be added over time.<br>
407     *         Possible values: en (default)
408     * - packagefile: the name of the packagefile, defaults to package.xml
409     * - pathtopackagefile: the path to an existing package file to read in,
410     *                      if different from the packagedirectory
411     * - packagedirectory: the path to the base directory of the package.  For
412     *                     package PEAR_PackageFileManager, this path is
413     *                     /path/to/pearcvs/pear/PEAR_PackageFileManager where
414     *                     /path/to/pearcvs is a local path on your hard drive
415     * - outputdirectory: the path in which to place the generated package.xml
416     *                    by default, this is ignored, and the package.xml is
417     *                    created in the packagedirectory
418     * - filelistgenerator: the <filelist> section plugin which will be used.
419     *                      In this release, there are two generator plugins,
420     *                      file and cvs.  For details, see the docs for these
421     *                      plugins
422     * - usergeneratordir: For advanced users.  If you write your own filelist
423     *                     generator plugin, use this option to tell
424     *                     PEAR_PackageFileManager where to find the file that
425     *                     contains it.  If the plugin is named foo, the class
426     *                     must be named PEAR_PackageFileManager_Foo
427     *                     no matter where it is located.  By default, the Foo
428     *                     plugin is located in PEAR/PackageFileManager/Foo.php.
429     *                     If you pass /path/to/foo in this option, setOptions
430     *                     will look for PEAR_PackageFileManager_Foo in
431     *                     /path/to/foo/Foo.php
432     * - changelogoldtonew: True if the ChangeLog should list from oldest entry to
433     *                      newest.  Set to false if you would like new entries first
434     * - simpleoutput: True if the package.xml should be human-readable
435     * - clearchangelog: True if change log should not be generated/updated
436     * - addhiddenfiles: True if you wish to add hidden files/directories that begin with .
437     *                   like .bashrc.  This is only used by the File generator.  The CVS
438     *                   generator will use all files in CVS regardless of format
439     *
440     * package.xml simple options:
441     * - baseinstalldir: The base directory to install this package in.  For
442     *                   package PEAR_PackageFileManager, this is "PEAR", for
443     *                   package PEAR, this is "/"
444     * - changelognotes: notes for the changelog, this should be more detailed than
445     *                   the release notes.  By default, PEAR_PackageFileManager uses
446     *                   the notes option for the changelog as well
447     *
448     * <b>WARNING</b>: all complex options that require a file path are case-sensitive
449     *
450     * package.xml complex options:
451     * - ignore: an array of filenames, directory names, or wildcard expressions specifying
452     *           files to exclude entirely from the package.xml.  Wildcards are operating system
453     *           wildcards * and ?.  file*foo.php will exclude filefoo.php, fileabrfoo.php and
454     *           filewho_is_thisfoo.php.  file?foo.php will exclude fileafoo.php and will not
455     *           exclude fileaafoo.php.  test/ will exclude all directories and subdirectories of
456     *           ANY directory named test encountered in directory parsing.  *test* will exclude
457     *           all files and directories that contain test in their name
458     * - include: an array of filenames, directory names, or wildcard expressions specifying
459     *            files to include in the listing.  All other files will be ignored.
460     *            Wildcards are in the same format as ignore
461     * - roles: this is an array mapping file extension to install role.  This
462     *          specifies default behavior that can be overridden by the exceptions
463     *          option and dir_roles option.  use {@link addRole()} to add a new
464     *          role to the pre-existing array
465     * - dir_roles: this is an array mapping directory name to install role.  All
466     *              files in a directory whose name matches the directory will be
467     *              given the install role specified.  Single files can be excluded
468     *              from this using the exceptions option.  The directory should be
469     *              a relative path from the baseinstalldir, or "/" for the baseinstalldir
470     * - exceptions: specify file role for specific files.  This array maps all files
471     *               matching the exact name of a file to a role as in "file.ext" => "role"
472     * - globalreplacements: a list of replacements that should be performed on every single file.
473     *                       The format is the same as replacements
474     * - globalreplaceexceptions: a list of exact filenames that should not have global
475     *                            replacements performed (useful for images and large files)
476     *                            note that this is not exported to package.xml 1.0!!
477     *
478     * @param array   $options  (optional) list of generation options
479     * @param boolean $internal (optional) private function call
480     *
481     * @see    PEAR_PackageFileManager_File
482     * @see    PEAR_PackageFileManager_CVS
483     * @return void|PEAR_Error
484     * @throws PEAR_PACKAGEFILEMANAGER2_NOPKGDIR
485     * @throws PEAR_PACKAGEFILEMANAGER2_PKGDIR_NOTREAL
486     * @throws PEAR_PACKAGEFILEMANAGER2_PATHTOPKGDIR_NOTREAL
487     * @throws PEAR_PACKAGEFILEMANAGER2_OUTPUTDIR_NOTREAL
488     * @throws PEAR_PACKAGEFILEMANAGER2_NOBASEDIR
489     * @throws PEAR_PACKAGEFILEMANAGER2_GENERATOR_NOTFOUND_ANYWHERE
490     * @throws PEAR_PACKAGEFILEMANAGER2_GENERATOR_NOTFOUND
491     * @access public
492     * @since  1.0.0a1
493     */
494    function setOptions($options = array(), $internal = false)
495    {
496        if (!isset($options['packagedirectory']) || !$options['packagedirectory']) {
497            return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_NOPKGDIR);
498        }
499
500        if (!file_exists($options['packagedirectory'])) {
501            return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_PKGDIR_NOTREAL,
502                $options['packagedirectory']);
503        }
504
505        $options['packagedirectory'] = str_replace(DIRECTORY_SEPARATOR,
506                                                 '/',
507                                                 realpath($options['packagedirectory']));
508        if ($options['packagedirectory']{strlen($options['packagedirectory']) - 1} != '/') {
509            $options['packagedirectory'] .= '/';
510        }
511
512        if (isset($options['pathtopackagefile']) && $options['pathtopackagefile']) {
513            if (!file_exists($options['pathtopackagefile'])) {
514                return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_PATHTOPKGDIR_NOTREAL,
515                    $options['pathtopackagefile']);
516            }
517            $options['pathtopackagefile'] = str_replace(DIRECTORY_SEPARATOR,
518                                                     '/',
519                                                     realpath($options['pathtopackagefile']));
520            if ($options['pathtopackagefile']{strlen($options['pathtopackagefile']) - 1} != '/') {
521                $options['pathtopackagefile'] .= '/';
522            }
523        }
524
525        if (isset($options['outputdirectory']) && $options['outputdirectory']) {
526            if (!file_exists($options['outputdirectory'])) {
527                return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_OUTPUTDIR_NOTREAL,
528                    $options['outputdirectory']);
529            }
530
531            $options['outputdirectory'] = str_replace(DIRECTORY_SEPARATOR,
532                                                     '/',
533                                                     realpath($options['outputdirectory']));
534            if ($options['outputdirectory']{strlen($options['outputdirectory']) - 1} != '/') {
535                $options['outputdirectory'] .= '/';
536            }
537        }
538
539        if (!isset($options['baseinstalldir']) || !$options['baseinstalldir']) {
540            return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_NOBASEDIR);
541        }
542
543        $this->_options = array_merge($this->_options, $options);
544        if (!isset($this->_options['roles']['*'])) {
545            $this->_options['roles']['*'] = 'data';
546        }
547
548        $path = ($this->_options['pathtopackagefile'] ?
549                    $this->_options['pathtopackagefile'] : $this->_options['packagedirectory']);
550        $this->_options['filelistgenerator'] =
551            ucfirst(strtolower($this->_options['filelistgenerator']));
552        if (!$internal) {
553            if (PEAR::isError($res = PEAR_PackageFileManager2::_getExistingPackageXML($path,
554                  $this->_options['packagefile'], array('cleardependencies' => true)))) {
555                return $res;
556            }
557            $this->_oldPackageFile = $res;
558        }
559
560        // file generator resource to load
561        $resource = 'PEAR/PackageFileManager/' . ucfirst(strtolower($this->_options['filelistgenerator'])) . '.php';
562        // file generator class name
563        $className = substr($resource, 0, -4);
564        $className = str_replace('/', '_', $className);
565
566        if (class_exists($className)) {
567            return;
568        }
569
570        // attempt to load the interface from the standard PEAR location
571        if ($this->isIncludeable($resource)) {
572            include_once $resource;
573        } elseif (isset($this->_options['usergeneratordir'])) {
574            // attempt to load from a user-specified directory
575            if (is_dir(realpath($this->_options['usergeneratordir']))) {
576                $this->_options['usergeneratordir'] =
577                    str_replace(DIRECTORY_SEPARATOR,
578                                '/',
579                                realpath($this->_options['usergeneratordir']));
580                if ($this->_options['usergeneratordir']{strlen($this->_options['usergeneratordir']) - 1} != '/') {
581                    $this->_options['usergeneratordir'] .= '/';
582                }
583            } else {
584                $this->_options['usergeneratordir'] = '////';
585            }
586
587            $generator = $this->_options['usergeneratordir'] .
588                             ucfirst(strtolower($this->_options['filelistgenerator'])) . '.php';
589            if (file_exists($generator) && is_readable($generator)) {
590                include_once $generator;
591            }
592
593            if (!class_exists($className)) {
594                return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_GENERATOR_NOTFOUND_ANYWHERE,
595                    $className);
596            }
597        }
598    }
599
600    /**
601     * Define a link between a subpackage and the parent package
602     *
603     * In many cases, a subpackage is developed in the same directory
604     * as the parent package, and the files should be excluded from the package.xml
605     * version 2.0.
606     *
607     * @param object  &$pm        PEAR_PackageFileManager2 object representing the subpackage's package.xml
608     * @param boolean $dependency dependency type to add, use true for a package dependency,
609     *                 false for a subpackage dependency
610     * @param boolean $required   (optional) whether the dependency should be required or optional
611     *
612     * @return void|false
613     * @access public
614     * @since  1.0.0a1
615     */
616    function specifySubpackage(&$pm, $dependency = null, $required = false)
617    {
618        if (!$pm->getDate()) {
619            $pm->setDate(date('Y-m-d'));
620        }
621
622        if (!$pm->validate(PEAR_VALIDATE_NORMAL)) {
623            return false;
624        }
625
626        $this->_subpackages[] = &$pm;
627        if ($dependency !== null) {
628            $type = $required ? 'required' : 'optional';
629            if ($pm->getChannel()) {
630                if ($dependency) {
631                    $this->addPackageDepWithChannel($type, $pm->getPackage(), $pm->getChannel(),
632                        $pm->getVersion(), false, false, false, $pm->getProvidesExtension());
633                } else {
634                    $this->addSubPackageDepWithChannel($type, $pm->getPackage(), $pm->getChannel(),
635                        $pm->getVersion(), false, false, false, $pm->getProvidesExtension());
636                }
637            } else {
638                if ($dependency) {
639                    $this->addPackageDepWithUri($type, $pm->getPackage(), $pm->getUri(),
640                        $this->getProvidesExtension());
641                } else {
642                    $this->addSubpackageDepWithUri($type, $pm->getPackage(), $pm->getUri(),
643                        $this->getProvidesExtension());
644                }
645            }
646        }
647    }
648
649    /**
650     * Convert a package xml 1.0 to 2.0 with user and default options
651     *
652     * @param string $packagefile name of package file
653     * @param array  $options     (optional) list of generation options
654     *
655     * @return PEAR_PackageFileManager2|PEAR_Error
656     * @static
657     * @access public
658     * @since  1.0.0a1
659     */
660    public static function &importFromPackageFile1($packagefile, $options = array())
661    {
662        $z   = &PEAR_Config::singleton();
663        $pkg = new PEAR_PackageFile($z);
664        $pf  = $pkg->fromPackageFile($packagefile, PEAR_VALIDATE_NORMAL);
665        if (PEAR::isError($pf)) {
666            return $pf;
667        }
668
669        if ($pf->getPackagexmlVersion() == '1.0') {
670            $packagefile = &$pf;
671        }
672
673        $a = &PEAR_PackageFileManager2::importOptions($packagefile, $options);
674        return $a;
675    }
676
677    /**
678     * Import options from an existing package.xml
679     *
680     * @param string $packagefile name of package file
681     * @param array  $options     (optional) list of generation options
682     *
683     * @return PEAR_PackageFileManager2|PEAR_Error
684     * @static
685     * @access public
686     * @since  1.0.0a1
687     */
688    public static function &importOptions($packagefile, $options = array())
689    {
690        if (is_a($packagefile, 'PEAR_PackageFile_v1')) {
691            $gen = &$packagefile->getDefaultGenerator();
692            $res = $gen->toV2('PEAR_PackageFileManager2');
693            if (PEAR::isError($res)) {
694                return $res;
695            }
696
697            $res->setOld();
698            if (isset($options['cleardependencies']) && $options['cleardependencies']) {
699                $res->clearDeps();
700            }
701
702            if (!isset($options['clearcontents']) || $options['clearcontents']) {
703                $res->clearContents();
704            } else {
705                $res->_importTasks($options);
706            }
707            $packagefile = $packagefile->getPackageFile();
708        }
709
710        if (!isset($res)) {
711            $res = &PEAR_PackageFileManager2::_getExistingPackageXML(dirname($packagefile) .
712                    DIRECTORY_SEPARATOR, basename($packagefile), $options);
713            if (PEAR::isError($res)) {
714                return $res;
715            }
716        }
717
718        if (PEAR::isError($ret = $res->_importOptions($packagefile, $options))) {
719            return $ret;
720        }
721
722        return $res;
723    }
724
725    /**
726     * Import options from an existing package.xml 2.0
727     *
728     * @param string $packagefile name of package file
729     * @param array  $options     list of generation options
730     *
731     * @return void|PEAR_Error
732     * @access private
733     * @since  1.0.0a1
734     */
735    function _importOptions($packagefile, $options)
736    {
737        $this->_options['packagedirectory']  = dirname($packagefile);
738        $this->_options['pathtopackagefile'] = dirname($packagefile);
739        $this->_options['baseinstalldir']    = '/';
740        return $this->setOptions(array_merge($this->_options, $options), true);
741    }
742
743    /**
744     * Get the existing options
745     *
746     * @param bool $withTasks (optional) Returns full options (=false)
747     *                                   or without replacements (=true)
748     *
749     * @return array
750     * @access public
751     * @since  1.0.0a1
752     */
753    function getOptions($withTasks = false)
754    {
755        if ($withTasks === false) {
756            return $this->_options;
757        }
758        $opt = $this->_options;
759        unset($opt['replacements']);
760        return $opt;
761    }
762
763    /**
764     * Add an extension/role mapping to the role mapping option
765     *
766     * Roles influence both where a file is installed and how it is installed.
767     * Files with role="data" are in a completely different directory hierarchy
768     * from the program files of role="php"
769     *
770     * In PEAR 1.3b2, these roles are
771     * - php (most common)
772     * - data
773     * - doc
774     * - test
775     * - script (gives the file an executable attribute)
776     * - src
777     *
778     * @param string $extension file extension
779     * @param string $role      file role
780     *
781     * @return void|PEAR_Error
782     * @throws PEAR_PACKAGEFILEMANAGER2_INVALID_ROLE
783     * @access public
784     * @since  1.0.0a1
785     */
786    function addRole($extension, $role)
787    {
788        include_once 'PEAR/Installer/Role.php';
789        $roles = PEAR_Installer_Role::getValidRoles($this->getPackageType());
790        if (!in_array($role, $roles)) {
791            return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_INVALID_ROLE, implode($roles, ', '), $role);
792        }
793        $this->_options['roles'][$extension] = $role;
794    }
795
796    /**
797     * Add a replacement option for all files
798     *
799     * This sets an install-time complex search-and-replace function
800     * allowing the setting of platform-specific variables in all
801     * installed files.
802     *
803     * if $type is php-const, then $to must be the name of a PHP Constant.
804     * If $type is pear-config, then $to must be the name of a PEAR config
805     * variable accessible through a {@link PEAR_Config::get()} method.  If
806     * type is package-info, then $to must be the name of a section from
807     * the package.xml file used to install this file.
808     *
809     * @param string $type variable type, either php-const, pear-config or package-info
810     * @param string $from text to replace in the source file
811     * @param string $to   variable name to use for replacement
812     *
813     * @return void|PEAR_Error
814     * @throws PEAR_PACKAGEFILEMANAGER2_INVALID_REPLACETYPE
815     * @access public
816     * @since  1.0.0a1
817     */
818    function addGlobalReplacement($type, $from, $to)
819    {
820        include_once 'PEAR/Task/Replace/rw.php';
821        if (!isset($this->_options['globalreplacements'])) {
822            $this->_options['globalreplacements'] = array();
823        }
824
825        $l = null;
826        $task = new PEAR_Task_Replace_rw($this, $this->_config, $l, '');
827        $task->setInfo($from, $to, $type);
828        if (is_array($res = $task->validate())) {
829            return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_INVALID_REPLACETYPE,
830                implode(', ', $res[3]), $res[1] . ': ' . $res[2]);
831        }
832
833        $this->_options['globalreplacements'][] = $task;
834    }
835
836    /**
837     * Add a replacement option for a file, or files matching the glob pattern
838     *
839     * This sets an install-time complex search-and-replace function
840     * allowing the setting of platform-specific variables in an
841     * installed file.
842     *
843     * if $type is php-const, then $to must be the name of a PHP Constant.
844     * If $type is pear-config, then $to must be the name of a PEAR config
845     * variable accessible through a {@link PEAR_Config::get()} method.  If
846     * type is package-info, then $to must be the name of a section from
847     * the package.xml file used to install this file.
848     *
849     * @param string $path relative path of file (relative to packagedirectory option)
850     *                     glob patterns are allowed (eg. {Dir1,Dir2}/*.php)
851     * @param string $type variable type, either php-const, pear-config or package-info
852     * @param string $from text to replace in the source file
853     * @param string $to   variable name to use for replacement
854     *
855     * @return void|PEAR_Error
856     * @throws PEAR_PACKAGEFILEMANAGER2_INVALID_REPLACETYPE
857     * @access public
858     * @since  1.0.0a1
859     */
860    function addReplacement($path, $type, $from, $to)
861    {
862        if (!isset($this->_options['replacements'])) {
863            $this->_options['replacements'] = array();
864        }
865
866        include_once 'PEAR/Task/Replace/rw.php';
867        $l = null;
868        $task = new PEAR_Task_Replace_rw($this, $this->_config, $l, '');
869        $task->setInfo($from, $to, $type);
870        if (is_array($res = $task->validate())) {
871            return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_INVALID_REPLACETYPE,
872                implode(', ', $res[3]), $res[1] . ': ' . $res[2]);
873        }
874
875        $current_dir = getcwd();
876        chdir($this->_options['packagedirectory']);
877        $glob = defined('GLOB_BRACE') ? glob($path, GLOB_BRACE) : glob($path);
878        chdir($current_dir);
879
880        if (false !== $glob) {
881            foreach ($glob as $pathItem) {
882                $this->_options['replacements'][$pathItem][] = $task;
883            }
884        }
885    }
886
887    /**
888     * Convert a file to windows line endings on installation
889     *
890     * @param string $path relative path of file (relative to packagedirectory option)
891     *
892     * @return void
893     * @access public
894     * @since  1.0.0a1
895     */
896    function addWindowsEol($path)
897    {
898        if (!isset($this->_options['replacements'])) {
899            $this->_options['replacements'] = array();
900        }
901        include_once 'PEAR/Task/Windowseol/rw.php';
902        $l = null;
903        $task = new PEAR_Task_Windowseol_rw($this, $this->_config, $l, '');
904        // we'll use this because it will still work
905        $this->_options['replacements'][$path][] = $task;
906    }
907
908    /**
909     * Convert a file to unix line endings on installation
910     *
911     * @param string $path relative path of file (relative to packagedirectory option)
912     *
913     * @return void
914     * @access public
915     * @since  1.0.0a1
916     */
917    function addUnixEol($path)
918    {
919        if (!isset($this->_options['replacements'])) {
920            $this->_options['replacements'] = array();
921        }
922        include_once 'PEAR/Task/Unixeol/rw.php';
923        $l = null;
924        $task = new PEAR_Task_Unixeol_rw($this, $this->_config, $l, '');
925        // we'll use this because it will still work
926        $this->_options['replacements'][$path][] = $task;
927    }
928
929    /**
930     * Get a post-installation task object for manipulation prior to adding it
931     *
932     * @param string $path relative path of file (relative to packagedirectory option)
933     *
934     * @return PEAR_Task_Postinstallscript_rw
935     * @access public
936     * @since  1.0.0a1
937     */
938    function &initPostinstallScript($path)
939    {
940        include_once 'PEAR/Task/Postinstallscript/rw.php';
941        $options = array('name' => $path, 'role' => 'php');
942        $task = new PEAR_Task_Postinstallscript_rw($this, $this->_config, $l, $options);
943        return $task;
944    }
945
946    /**
947     * Add post-installation script task to a post-install script.
948     *
949     * The script must have been created with {@link initPostinstallScript()} and
950     * then populated using the API of PEAR_Task_Postinstallscript_rw.
951     *
952     * @param object $task PEAR_Task_Postinstallscript_rw
953     * @param string $path relative path of file (relative to packagedirectory option)
954     *
955     * @return void|PEAR_Error
956     * @throws PEAR_PACKAGEFILEMANAGER2_INVALID_POSTINSTALLSCRIPT
957     * @access public
958     * @since  1.0.0a1
959     */
960    function addPostinstallTask($task, $path)
961    {
962        if (!is_a($task, 'PEAR_Task_Postinstallscript')) {
963            return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_INVALID_POSTINSTALLSCRIPT,
964                'Task passed in is not a PEAR_Task_Postinstallscript task');
965        }
966
967        // necessary for validation
968        $this->addFile('', $path, array('role' => 'php', 'name' => $path));
969        $this->setPackagefile($this->_options['packagedirectory'] .
970            DIRECTORY_SEPARATOR . $this->_options['packagefile']);
971        if (is_array($res = $task->validate())) {
972            return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_INVALID_POSTINSTALLSCRIPT,
973                $res[1]);
974        }
975
976        if (!isset($this->_options['replacements'])) {
977            $this->_options['replacements'] = array();
978        }
979        $this->_options['replacements'][$path][] = $task;
980    }
981
982    /**
983     * Uses PEAR::PHP_CompatInfo package to detect dependencies (extensions, php version)
984     *
985     * @param array $options (optional) parser options for PHP_CompatInfo
986     *
987     * @return void|PEAR_Error
988     * @throws PEAR_PACKAGEFILEMANAGER2_RUN_SETOPTIONS
989     * @throws PEAR_PACKAGEFILEMANAGER2_NO_PHPCOMPATINFO
990     * @access public
991     * @since  1.0.0a1
992     */
993    function detectDependencies($options = array())
994    {
995        if (!$this->isIncludeable('PHP/CompatInfo.php')) {
996            return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_NO_PHPCOMPATINFO);
997        }
998
999        include_once 'PHP/CompatInfo.php';
1000        if (!is_array($options)) {
1001            $options = array();
1002        }
1003        $this->_detectDependencies = $options;
1004    }
1005
1006    /**
1007     * Returns whether or not a file is in the include path.
1008     *
1009     * @param string $file path to filename
1010     *
1011     * @return boolean true if the file is in the include path, false otherwise
1012     * @access public
1013     * @since  1.0.0a1
1014     */
1015    function isIncludeable($file)
1016    {
1017        foreach (explode(PATH_SEPARATOR, ini_get('include_path')) as $path) {
1018            $p = $path . DIRECTORY_SEPARATOR . $file;
1019            if (file_exists($p) && is_readable($p)) {
1020                return true;
1021            }
1022        }
1023        return false;
1024    }
1025
1026    /**
1027     * Writes the package.xml file out with the newly created <release></release> tag
1028     *
1029     * ALWAYS use {@link debugPackageFile} to verify that output is correct before
1030     * overwriting your package.xml
1031     *
1032     * @param boolean $debuginterface null if no debugging, true if web interface, false if command-line
1033     *
1034     * @throws PEAR_PACKAGEFILEMANAGER2_INVALID_PACKAGE
1035     * @throws PEAR_PACKAGEFILEMANAGER2_CANTWRITE_PKGFILE
1036     * @throws PEAR_PACKAGEFILEMANAGER2_CANTCOPY_PKGFILE
1037     * @throws PEAR_PACKAGEFILEMANAGER2_CANTOPEN_TMPPKGFILE
1038     * @throws PEAR_PACKAGEFILEMANAGER2_DEST_UNWRITABLE
1039     * @return true|PEAR_Error
1040     * @access public
1041     * @since  1.0.0a1
1042     */
1043    function writePackageFile($debuginterface = null)
1044    {
1045        $warnings = $this->_stack->getErrors(true);
1046        $this->setDate(date('Y-m-d'));
1047        if (count($warnings)) {
1048            $nl = (isset($debuginterface) && $debuginterface ? '<br />' : "\n");
1049            foreach ($warnings as $errmsg) {
1050                echo 'WARNING: ' . $errmsg['message'] . $nl;
1051            }
1052        }
1053
1054        if ($this->_options['simpleoutput']) {
1055            $state = PEAR_VALIDATE_NORMAL;
1056        } else {
1057            $state = PEAR_VALIDATE_PACKAGING;
1058        }
1059
1060        $this->_getDependencies();
1061        if ($this->_options['clearchangelog']) {
1062            $this->clearChangeLog();
1063        } else {
1064            $this->_updateChangeLog();
1065        }
1066
1067        $outputdir = ($this->_options['outputdirectory'] ?
1068                        $this->_options['outputdirectory'] : $this->_options['packagedirectory']);
1069        $this->setPackagefile($this->_options['packagedirectory'] . $this->_options['packagefile']);
1070        if (!$this->validate($state)) {
1071            $errors = $this->getValidationWarnings();
1072            $ret = '';
1073            $nl = (isset($debuginterface) && $debuginterface ? '<br />' : "\n");
1074            $haserror = false;
1075            foreach ($errors as $err) {
1076                if (!$haserror && $err['level'] == 'error') {
1077                    $haserror = true;
1078                }
1079                if (isset($debuginterface) && $debuginterface) {
1080                    $msg = htmlspecialchars($err['message']);
1081                } else {
1082                    $msg = $err['message'];
1083                }
1084                $ret .= ucfirst($err['level']) . ': ' . $msg . $nl;
1085            }
1086            if ($haserror) {
1087                return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_INVALID_PACKAGE, $nl, $ret);
1088            }
1089        }
1090
1091        $gen = &$this->getDefaultGenerator();
1092        $pfm = $gen->toXml($state);
1093        if (isset($debuginterface)) {
1094            if ($debuginterface) {
1095                echo '<pre>' . htmlentities($pfm) . '</pre>';
1096            } else {
1097                echo $pfm;
1098            }
1099            return true;
1100        }
1101
1102        $file = $outputdir . $this->_options['packagefile'];
1103        if ((file_exists($file) && is_writable($file)) || @touch($file)) {
1104            if ($fp = @fopen($file . '.tmp', "w")) {
1105                $written = @fwrite($fp, $pfm);
1106                @fclose($fp);
1107                if ($written === false) {
1108                    return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_CANTWRITE_PKGFILE);
1109                }
1110
1111                if (!@copy($file . '.tmp', $file)) {
1112                    return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_CANTCOPY_PKGFILE);
1113                }
1114
1115                @unlink($file . '.tmp');
1116                return true;
1117            }
1118
1119            return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_CANTOPEN_TMPPKGFILE,
1120                    $outputdir . $this->_options['packagefile'] . '.tmp');
1121        }
1122
1123        return $this->raiseError(PEAR_PACKAGEFILEMANAGER2_DEST_UNWRITABLE, $outputdir);
1124    }
1125
1126    /**
1127     * ALWAYS use this to test output before overwriting your package.xml!!
1128     *
1129     * This method instructs writePackageFile() to simply print the package.xml
1130     * to output, either command-line or web-friendly (this is automatic
1131     * based on the existence of $_SERVER['PATH_TRANSLATED']
1132     *
1133     * @uses   writePackageFile() calls with the debug parameter set based on
1134     *           whether it is called from the command-line or web interface
1135     * @return true|PEAR_Error
1136     * @access public
1137     * @since  1.0.0a1
1138     */
1139    function debugPackageFile()
1140    {
1141        $webinterface = (php_sapi_name() != 'cli');
1142        return $this->writePackageFile($webinterface);
1143    }
1144
1145    /**
1146     * Store a warning on the warning stack
1147     *
1148     * @param integer $code error code
1149     * @param array   $info additional specific error info
1150     *
1151     * @return void
1152     * @access public
1153     * @since  1.0.0a1
1154     */
1155    function pushWarning($code, $info)
1156    {
1157        $this->_warningStack[] = array('code' => $code,
1158                                       'message' => $this->_getMessage($code, $info));
1159    }
1160
1161    /**
1162     * Retrieve the list of warnings
1163     *
1164     * @return array
1165     * @access public
1166     * @since  1.0.0a1
1167     */
1168    function getWarnings()
1169    {
1170        $a = $this->_warningStack;
1171        $this->_warningStack = array();
1172        return $a;
1173    }
1174
1175    /**
1176     * Retrieve an error message from a code
1177     *
1178     * @param integer $code error code
1179     * @param array   $info additional specific error info
1180     *
1181     * @return string Error message
1182     * @access private
1183     * @since  1.0.0a1
1184     */
1185    function _getMessage($code, $info)
1186    {
1187        $msg = $GLOBALS['_PEAR_PACKAGEFILEMANAGER2_ERRORS'][$this->_options['lang']][$code];
1188        foreach ($info as $name => $value) {
1189            $msg = str_replace('%' . $name . '%', $value, $msg);
1190        }
1191        return $msg;
1192    }
1193
1194    /**
1195     * Utility function to shorten error generation code
1196     *
1197     * {@source}
1198     *
1199     * @param integer $code error code
1200     * @param string  $i1   (optional) additional specific error info #1
1201     * @param string  $i2   (optional) additional specific error info #2
1202     *
1203     * @return PEAR_Error
1204     * @access public
1205     * @since  1.0.0a1
1206     */
1207    function raiseError($code, $i1 = '', $i2 = '')
1208    {
1209        return PEAR::raiseError('PEAR_PackageFileManager2 Error: ' .
1210                    sprintf($GLOBALS['_PEAR_PACKAGEFILEMANAGER2_ERRORS'][$this->_options['lang']][$code],
1211                    $i1, $i2), $code);
1212    }
1213
1214    /**
1215     * Generates file list contents of package.xml
1216     *
1217     * @uses _getDirTag()     generate the xml from the array
1218     * @uses _getSimpleDirTag generate the xml from the array for human reading
1219     * @return void|PEAR_Error
1220     * @access private
1221     * @since  1.0.0a1
1222     */
1223    function generateContents()
1224    {
1225        $this->addIgnore(array('package.xml', 'package2.xml'));
1226        $options = $this->_options;
1227        if (count($this->_subpackages)) {
1228            if (!is_array($options['ignore'])) {
1229                $options['ignore'] = array();
1230            }
1231
1232            $subp = count($this->_subpackages);
1233            for ($i = 0; $i < $subp; $i++) {
1234                $save     = $this->_subpackages[$i]->getArray();
1235                $filelist = $this->_subpackages[$i]->getFileList();
1236                foreach ($filelist as $file => $atts) {
1237                    $options['ignore'][] = '*' . $file; // ignore all subpackage files
1238                }
1239                $this->_subpackages[$i]->fromArray($save);
1240            }
1241        }
1242
1243        $generatorclass = 'PEAR_PackageFileManager_' . ucfirst(strtolower($this->_options['filelistgenerator']));
1244        $generator      = new $generatorclass($options);
1245        $this->clearContents($this->_options['baseinstalldir']);
1246        $this->_struc = $generator->getFileList();
1247        if ($this->_options['simpleoutput']) {
1248            return $this->_getSimpleDirTag($this->_struc);
1249        }
1250
1251        return $this->_getDirTag($this->_struc);
1252    }
1253
1254    /**
1255     * Recursively generate the <filelist> section's <dir> and <file> tags, but with
1256     * simple human-readable output
1257     *
1258     * @param array|PEAR_Error $struc   the sorted directory structure, or an error
1259     *                                  from filelist generation
1260     * @param false|string     $role    (optional) whether the parent directory has a role this should
1261     *                         inherit
1262     * @param string           $_curdir (optional) indentation level
1263     *
1264     * @return array|PEAR_Error
1265     * @access private
1266     * @since  1.0.0a1
1267     */
1268    function _getSimpleDirTag($struc, $role = false, $_curdir = '')
1269    {
1270        if (PEAR::isError($struc)) {
1271            return $struc;
1272        }
1273
1274        extract($this->_options);
1275        $ret = array();
1276        foreach ($struc as $dir => $files) {
1277            if (false && $dir === '/') {
1278                // global directory role? overrides all exceptions except file exceptions
1279                if (isset($dir_roles['/'])) {
1280                    $role = $dir_roles['/'];
1281                }
1282                return $this->_getSimpleDirTag($struc[$dir], $role, '');
1283            }
1284
1285            // directory
1286            if (!isset($files['file']) || is_array($files['file'])) {
1287                // contains only directories
1288                if (isset($dir_roles[$_curdir . $dir])) {
1289                    $myrole = $dir_roles[$_curdir . $dir];
1290                } else {
1291                    $myrole = $role;
1292                }
1293                $recurdir = ($_curdir == '') ? $dir . '/' : $_curdir . $dir . '/';
1294                if ($recurdir == '//') {
1295                    $recurdir = '';
1296                }
1297                $this->_getSimpleDirTag($files, $myrole, $recurdir);
1298            } else {
1299                // contains files
1300                $myrole = '';
1301                if (!$role) {
1302                    $myrole = false;
1303                    if (isset($exceptions[$files['path']])) {
1304                        $myrole = $exceptions[$files['path']];
1305                    } elseif (isset($roles[$files['ext']])) {
1306                        $myrole = $roles[$files['ext']];
1307                    } else {
1308                        $myrole = $roles['*'];
1309                    }
1310                } else {
1311                    $myrole = $role;
1312                    if (isset($exceptions[$files['path']])) {
1313                        $myrole = $exceptions[$files['path']];
1314                    }
1315                }
1316
1317                $test = explode('/', $files['path']);
1318                foreach ($test as $subpath) {
1319                    if ($subpath == 'CVS') {
1320                        $this->pushWarning(PEAR_PACKAGEFILEMANAGER2_CVS_PACKAGED,
1321                            array('path' => $files['path']));
1322                    }
1323                }
1324
1325                $atts = array('role' => $myrole);
1326                if (isset($installexceptions[$files['path']])) {
1327                    $atts['baseinstalldir'] = $installexceptions[$files['path']];
1328                }
1329
1330                $diradd = dirname($files['path']);
1331                $this->addFile($diradd == '.' ? '/' : $diradd, $files['file'], $atts);
1332                if (isset($globalreplacements) &&
1333                      !in_array($files['path'], $globalreplaceexceptions, true)) {
1334                    foreach ($globalreplacements as $task) {
1335                        $this->addTaskToFile($files['path'], $task);
1336                    }
1337                }
1338
1339                if (isset($replacements[$files['path']])) {
1340                    foreach ($replacements[$files['path']] as $task) {
1341                        $this->addTaskToFile($files['path'], $task);
1342                    }
1343                }
1344            }
1345        }
1346
1347        return;
1348    }
1349
1350    /**
1351     * Recursively generate the <filelist> section's <dir> and <file> tags
1352     *
1353     * @param array|PEAR_Error $struc   the sorted directory structure, or an error
1354     *                         from filelist generation
1355     * @param false|string     $role    (optional) whether the parent directory has a role this should
1356     *                         inherit
1357     * @param string           $_curdir (optional) indentation level
1358     *
1359     * @return array|PEAR_Error
1360     * @access private
1361     * @since  1.0.0a1
1362     */
1363    function _getDirTag($struc, $role = false, $_curdir = '')
1364    {
1365        if (PEAR::isError($struc)) {
1366            return $struc;
1367        }
1368
1369        extract($this->_options);
1370        foreach ($struc as $dir => $files) {
1371            if ($dir === '/') {
1372                // global directory role? overrides all exceptions except file exceptions
1373                if (isset($dir_roles['/'])) {
1374                    $role = $dir_roles['/'];
1375                }
1376                return $this->_getDirTag($struc[$dir], $role, '');
1377            }
1378
1379            // non-global directory
1380            if (!isset($files['file']) || is_array($files['file'])) {
1381                // contains only other directories
1382                $myrole = '';
1383                if (isset($dir_roles[$_curdir . $dir])) {
1384                    $myrole = $dir_roles[$_curdir . $dir];
1385                } elseif ($role) {
1386                    $myrole = $role;
1387                }
1388                $this->_getDirTag($files, $myrole, $_curdir . $dir . '/');
1389            } else {
1390                // contains files
1391                $myrole = '';
1392                if (!$role) {
1393                    $myrole = false;
1394                    if (isset($exceptions[$files['path']])) {
1395                        $myrole = $exceptions[$files['path']];
1396                    } elseif (isset($roles[$files['ext']])) {
1397                        $myrole = $roles[$files['ext']];
1398                    } else {
1399                        $myrole = $roles['*'];
1400                    }
1401                } else {
1402                    $myrole = $role;
1403                    if (isset($exceptions[$files['path']])) {
1404                        $myrole = $exceptions[$files['path']];
1405                    }
1406                }
1407                if (isset($installexceptions[$files['path']])) {
1408                    $bi = $installexceptions[$files['path']];
1409                } else {
1410                    $bi = $this->_options['baseinstalldir'];
1411                }
1412                $test = explode('/', $files['path']);
1413                foreach ($test as $subpath) {
1414                    if ($subpath == 'CVS') {
1415                        $this->pushWarning(PEAR_PACKAGEFILEMANAGER2_CVS_PACKAGED,
1416                            array('path' => $files['path']));
1417                    }
1418                }
1419                $atts =
1420                    array('role' => $myrole,
1421                          'baseinstalldir' => $bi,
1422                          );
1423                if (!isset($this->_options['simpleoutput']) || !$this->_options['simpleoutput']) {
1424                    $md5sum = @md5_file($this->_options['packagedirectory'] . $files['path']);
1425                    if (!empty($md5sum)) {
1426                        $atts['md5sum'] = $md5sum;
1427                    }
1428                }
1429                $diradd = dirname($files['path']);
1430                $this->addFile($diradd == '.' ? '/' : $diradd, $files['file'], $atts);
1431                if (isset($globalreplacements) &&
1432                      !in_array($files['path'], $globalreplaceexceptions, true)) {
1433                    foreach ($globalreplacements as $task) {
1434                        $this->addTaskToFile($files['path'], $task);
1435                    }
1436                }
1437                if (isset($replacements[$files['path']])) {
1438                    foreach ($replacements[$files['path']] as $task) {
1439                        $this->addTaskToFile($files['path'], $task);
1440                    }
1441                }
1442            }
1443        }
1444
1445        return;
1446    }
1447
1448    /**
1449     * @param array $files
1450     * @param array &$ret
1451     *
1452     * @return array
1453     * @access private
1454     * @since  1.0.0a1
1455     */
1456    function _traverseFileArray($files, &$ret)
1457    {
1458        foreach ($files as $file) {
1459            if (!isset($file['fullpath'])) {
1460                $this->_traverseFileArray($file, $ret);
1461            } else {
1462                $ret[] = $file['fullpath'];
1463            }
1464        }
1465    }
1466
1467    /**
1468     * Retrieve the 'deps' option passed to the constructor
1469     *
1470     * @access private
1471     * @return void|PEAR_Error
1472     * @since  1.0.0a1
1473     */
1474    function _getDependencies()
1475    {
1476        if ($this->_detectDependencies) {
1477            $this->_traverseFileArray($this->_struc, $ret);
1478            $compatinfo  = new PHP_CompatInfo();
1479            $info        = $compatinfo->parseArray($ret, $this->_detectDependencies);
1480            $max_version = (empty($info['max_version'])) ? false : $info['max_version'];
1481            $ret = $this->setPhpDep($info['version'], $max_version);
1482            if (is_a($ret, 'PEAR_Error')) {
1483                return $ret;
1484            }
1485
1486            foreach ($info['extensions'] as $ext) {
1487                $this->addExtensionDep('required', $ext);
1488            }
1489        }
1490        return;
1491    }
1492
1493    /**
1494     * Creates a changelog entry with the current release
1495     * notes and dates, or overwrites a previous creation
1496     *
1497     * @return void
1498     * @access private
1499     * @since  1.0.0a1
1500     */
1501    function _updateChangeLog()
1502    {
1503        $changelog = $this->_oldPackageFile ? $this->_oldPackageFile->getChangelog() : false;
1504        $notes = $this->_options['changelognotes'];
1505        if (!$changelog) {
1506            $this->setChangelogEntry($this->getVersion(), $this->generateChangeLogEntry($notes));
1507            return;
1508        }
1509
1510        if (!isset($changelog['release'][0])) {
1511            $changelog['release'] = array($changelog['release']);
1512        }
1513        $found = false;
1514        foreach ($changelog['release'] as $i => $centry) {
1515            $changelog['release'][$i]['notes'] = trim($changelog['release'][$i]['notes']);
1516            if ($centry['version']['release'] == $this->getVersion()) {
1517                $changelog['release'][$i] = $this->generateChangeLogEntry($notes);
1518                $found = true;
1519            }
1520        }
1521        if (!$found) {
1522            $changelog['release'][] = $this->generateChangeLogEntry($notes);
1523        }
1524        usort($changelog['release'], array($this, '_changelogsort'));
1525        $this->clearChangeLog();
1526        foreach ($changelog['release'] as $entry) {
1527            $this->setChangelogEntry($entry['version']['release'], $entry);
1528        }
1529    }
1530
1531    /**
1532     * User-defined comparison function to sort changelog array
1533     *
1534     * @param array $a first array to compare items
1535     * @param array $b second array to compare items
1536     *
1537     * @return integer sort comparaison result (-1, 0, +1) of two elements $a and $b
1538     * @access private
1539     * @since  1.0.0a1
1540     */
1541    function _changelogsort($a, $b)
1542    {
1543        if (isset($a['date']) && isset($b['date'])) {
1544            if ($this->_options['changelogoldtonew']) {
1545                $c = strtotime($a['date']);
1546                $d = strtotime($b['date']);
1547            } else {
1548                $d = strtotime($a['date']);
1549                $c = strtotime($b['date']);
1550            }
1551
1552            if ($c - $d > 0) {
1553                return 1;
1554            } elseif ($c - $d < 0) {
1555                return -1;
1556            }
1557        }
1558
1559        if (isset($a['version']['release']) && isset($b['version']['release'])) {
1560            if ($this->_options['changelogoldtonew']) {
1561                $v1 = $a['version']['release'];
1562                $v2 = $b['version']['release'];
1563            } else {
1564                $v2 = $a['version']['release'];
1565                $v1 = $b['version']['release'];
1566            }
1567
1568            return version_compare($v1, $v2);
1569        }
1570
1571        return 0;
1572    }
1573
1574    /**
1575     * @return void
1576     * @since  1.0.0a1
1577     */
1578    function setOld()
1579    {
1580        $this->_oldPackageFile = new PEAR_PackageFile_v2_rw();
1581        $this->_oldPackageFile->fromArray($this->getArray());
1582    }
1583
1584    /**
1585     * Import tasks options and files roles (if exceptions)
1586     * from an existing package.xml
1587     *
1588     * @param array $options list of generation options
1589     *
1590     * @return void|PEAR_Error
1591     * @access private
1592     * @since  1.6.0b5
1593     */
1594    function _importTasks($options)
1595    {
1596        $filelist = $this->getFilelist(true);
1597        $vroles   = array_values($this->_options['roles']);
1598
1599        foreach ($filelist as $file => $contents) {
1600            $atts = $contents['attribs'];
1601            unset($contents['attribs']);
1602            // check for tasks replacement, eol
1603            if (count($contents)) {
1604                foreach ($contents as $tag => $raw) {
1605                    $taskNs = $this->getTasksNs();
1606                    $task = str_replace("$taskNs:", '', $tag);
1607                    if ($task == 'replace') {
1608                        if (!isset($raw[0])) {
1609                            $raw = array($raw);
1610                        }
1611                        foreach ($raw as $attrs) {
1612                            $a = $attrs['attribs'];
1613                            $this->addReplacement($file, $a['type'], $a['from'], $a['to']);
1614                        }
1615
1616                    } elseif ($task == 'windowseol') {
1617                        $this->addWindowsEol($file);
1618
1619                    } elseif ($task == 'unixeol') {
1620                        $this->addUnixEol($file);
1621
1622                    } elseif ($task == 'postinstallscript') {
1623                        $script = &$this->initPostinstallScript($file);
1624                        $raw = $this->_stripNamespace($raw);
1625
1626                        foreach ($raw['paramgroup'] as $paramgroup) {
1627                            if (isset($paramgroup['instructions'])) {
1628                                $instructions = $paramgroup['instructions'];
1629                            } else {
1630                                $instructions = false;
1631                            }
1632
1633                            if (isset($paramgroup['param'][0])) {
1634                                $params = $paramgroup['param'];
1635                            } else {
1636                                $params = array($paramgroup['param']);
1637                            }
1638                            $param = array();
1639                            foreach ($params as $p) {
1640                                $default = isset($p['default']) ? $p['default'] : null;
1641                                $param[] = $script->getParam($p['name'],
1642                                    $p['prompt'], $p['type'], $default);
1643                            }
1644                            $script->addParamGroup($paramgroup['id'], $param, $instructions);
1645                        }
1646                        $ret = $this->addPostinstallTask($script, $file);
1647                        if (PEAR::isError($ret)) {
1648                            return $ret;
1649                        }
1650                    }
1651                }
1652            }
1653            // check for role attribute
1654            if (isset($atts['role'])) {
1655                $myrole = $atts['role'];
1656                if (!in_array($myrole, $vroles)) {
1657                    $this->_options['exceptions'][$file] = $myrole;
1658                } else {
1659                    $inf = pathinfo($file);
1660                    if (isset($inf['extension'])) {
1661                        if (isset($this->_options['roles'][$inf['extension']])) {
1662                            $role = $this->_options['roles'][$inf['extension']];
1663                        } else {
1664                            $role = $this->_options['roles']['*'];
1665                        }
1666                        if ($role != $myrole) {
1667                            $this->_options['exceptions'][$file] = $myrole;
1668                        }
1669                    } else {
1670                        $this->_options['exceptions'][$file] = $myrole;
1671                    }
1672                }
1673            }
1674            // check for baseinstalldir attribute
1675            if (isset($options['baseinstalldir'])
1676                && isset($atts['baseinstalldir'])
1677                && $atts['baseinstalldir'] != $options['baseinstalldir']
1678            ) {
1679                $this->_options['installexceptions'][$file] = $atts['baseinstalldir'];
1680            }
1681        }
1682    }
1683
1684    /**
1685     * Strip namespace from postinstallscript task array
1686     *
1687     * @param array $params tasks options
1688     *
1689     * @return array
1690     * @access private
1691     * @since  1.6.0b5
1692     */
1693    function _stripNamespace($params)
1694    {
1695        $newparams = array();
1696        foreach ($params as $i => $param) {
1697            if (is_array($param)) {
1698                $param = $this->_stripNamespace($param);
1699            }
1700            $newparams[str_replace($this->getTasksNs() . ':', '', $i)] = $param;
1701        }
1702        return $newparams;
1703    }
1704
1705    /**
1706     * @param string $path        full path to package file
1707     * @param string $packagefile (optional) name of package file
1708     * @param array  $options     (optional) list of generation options
1709     *
1710     * @throws PEAR_PACKAGEFILEMANAGER2_INVALID_PACKAGE
1711     * @throws PEAR_PACKAGEFILEMANAGER2_PATH_DOESNT_EXIST
1712     * @return true|PEAR_Error
1713     * @uses   _generateNewPackageXML() if no package.xml is found, it
1714     *          calls this to create a new one
1715     * @access private
1716     * @static
1717     * @since  1.0.0a1
1718     */
1719    protected static function &_getExistingPackageXML($path, $packagefile = 'package.xml', $options = array())
1720    {
1721        if (is_string($path) && is_dir($path)) {
1722            $contents = false;
1723            if (file_exists($path . $packagefile)) {
1724                $contents = file_get_contents($path . $packagefile);
1725            }
1726
1727            if (!$contents) {
1728                $a = PEAR_PackageFileManager2::_generateNewPackageXML();
1729                return $a;
1730            }
1731
1732            include_once 'PEAR/PackageFile/Parser/v2.php';
1733            $pkg = new PEAR_PackageFile_Parser_v2();
1734            $z = &PEAR_Config::singleton();
1735            $pkg->setConfig($z);
1736            $pf = $pkg->parse($contents, $path . $packagefile, false,
1737                'PEAR_PackageFileManager2');
1738            if (PEAR::isError($pf)) {
1739                return $pf;
1740            }
1741
1742            if (!$pf->validate(PEAR_VALIDATE_DOWNLOADING)) {
1743                $errors = '';
1744                foreach ($pf->getValidationWarnings() as $warning) {
1745                    $errors .= "\n" . ucfirst($warning['level']) . ': ' .
1746                        $warning['message'];
1747                }
1748                if (php_sapi_name() != 'cli') {
1749                    $errors = nl2br(htmlspecialchars($errors));
1750                }
1751                $a = $pf->raiseError(PEAR_PACKAGEFILEMANAGER2_INVALID_PACKAGE, $errors);
1752                return $a;
1753            }
1754
1755            $pf->setOld();
1756            if (isset($options['cleardependencies']) && $options['cleardependencies']) {
1757                $pf->clearDeps();
1758            }
1759
1760            if (!isset($options['clearcontents']) || $options['clearcontents']) {
1761                $pf->clearContents();
1762            } else {
1763                // merge options is required to use PEAR_PackageFileManager2::addPostinstallTask()
1764                $ret = $pf->_importOptions($packagefile, $options);
1765                if (PEAR::isError($ret)) {
1766                    return $ret;
1767                }
1768                $pf->_importTasks($options);
1769            }
1770
1771            return $pf;
1772        }
1773
1774        if (!is_string($path)) {
1775            $path = gettype($path);
1776        }
1777        include_once 'PEAR.php';
1778        $a = PEAR::raiseError('Path does not exist: ' . $path, PEAR_PACKAGEFILEMANAGER2_PATH_DOESNT_EXIST);
1779        return $a;
1780    }
1781
1782    /**
1783     * Create the structure for a new package.xml
1784     *
1785     * @uses   $_packageXml emulates reading in a package.xml
1786     *           by using the package, summary and description
1787     *           options
1788     * @return PEAR_PackageFileManager2
1789     * @access private
1790     * @static
1791     * @since  1.0.0a1
1792     */
1793    protected static function &_generateNewPackageXML()
1794    {
1795        $pf = new PEAR_PackageFileManager2();
1796        $pf->_oldPackageFile = false;
1797        return $pf;
1798    }
1799}
1800