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