1<?php
2/**
3 * PEAR_Installer
4 *
5 * PHP versions 4 and 5
6 *
7 * @category   pear
8 * @package    PEAR
9 * @author     Stig Bakken <ssb@php.net>
10 * @author     Tomas V.V. Cox <cox@idecnet.com>
11 * @author     Martin Jansen <mj@php.net>
12 * @author     Greg Beaver <cellog@php.net>
13 * @copyright  1997-2009 The Authors
14 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
15 * @link       http://pear.php.net/package/PEAR
16 * @since      File available since Release 0.1
17 */
18
19/**
20 * Used for installation groups in package.xml 2.0 and platform exceptions
21 */
22require_once 'OS/Guess.php';
23require_once 'PEAR/Downloader.php';
24
25define('PEAR_INSTALLER_NOBINARY', -240);
26/**
27 * Administration class used to install PEAR packages and maintain the
28 * installed package database.
29 *
30 * @category   pear
31 * @package    PEAR
32 * @author     Stig Bakken <ssb@php.net>
33 * @author     Tomas V.V. Cox <cox@idecnet.com>
34 * @author     Martin Jansen <mj@php.net>
35 * @author     Greg Beaver <cellog@php.net>
36 * @copyright  1997-2009 The Authors
37 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
38 * @version    Release: @package_version@
39 * @link       http://pear.php.net/package/PEAR
40 * @since      Class available since Release 0.1
41 */
42class PEAR_Installer extends PEAR_Downloader
43{
44    // {{{ properties
45
46    /** name of the package directory, for example Foo-1.0
47     * @var string
48     */
49    var $pkgdir;
50
51    /** directory where PHP code files go
52     * @var string
53     */
54    var $phpdir;
55
56    /** directory where PHP extension files go
57     * @var string
58     */
59    var $extdir;
60
61    /** directory where documentation goes
62     * @var string
63     */
64    var $docdir;
65
66    /** installation root directory (ala PHP's INSTALL_ROOT or
67     * automake's DESTDIR
68     * @var string
69     */
70    var $installroot = '';
71
72    /** debug level
73     * @var int
74     */
75    var $debug = 1;
76
77    /** temporary directory
78     * @var string
79     */
80    var $tmpdir;
81
82    /**
83     * PEAR_Registry object used by the installer
84     * @var PEAR_Registry
85     */
86    var $registry;
87
88    /**
89     * array of PEAR_Downloader_Packages
90     * @var array
91     */
92    var $_downloadedPackages;
93
94    /** List of file transactions queued for an install/upgrade/uninstall.
95     *
96     *  Format:
97     *    array(
98     *      0 => array("rename => array("from-file", "to-file")),
99     *      1 => array("delete" => array("file-to-delete")),
100     *      ...
101     *    )
102     *
103     * @var array
104     */
105    var $file_operations = array();
106
107    // }}}
108
109    // {{{ constructor
110
111    /**
112     * PEAR_Installer constructor.
113     *
114     * @param object $ui user interface object (instance of PEAR_Frontend_*)
115     *
116     * @access public
117     */
118    function __construct(&$ui)
119    {
120        parent::__construct($ui, array(), null);
121        $this->setFrontendObject($ui);
122        $this->debug = $this->config->get('verbose');
123    }
124
125    function setOptions($options)
126    {
127        $this->_options = $options;
128    }
129
130    function setConfig(&$config)
131    {
132        $this->config    = &$config;
133        $this->_registry = &$config->getRegistry();
134    }
135
136    // }}}
137
138    function _removeBackups($files)
139    {
140        foreach ($files as $path) {
141            $this->addFileOperation('removebackup', array($path));
142        }
143    }
144
145    // {{{ _deletePackageFiles()
146
147    /**
148     * Delete a package's installed files, does not remove empty directories.
149     *
150     * @param string package name
151     * @param string channel name
152     * @param bool if true, then files are backed up first
153     * @return bool TRUE on success, or a PEAR error on failure
154     * @access protected
155     */
156    function _deletePackageFiles($package, $channel = false, $backup = false)
157    {
158        if (!$channel) {
159            $channel = 'pear.php.net';
160        }
161
162        if (!strlen($package)) {
163            return $this->raiseError("No package to uninstall given");
164        }
165
166        if (strtolower($package) == 'pear' && $channel == 'pear.php.net') {
167            // to avoid race conditions, include all possible needed files
168            require_once 'PEAR/Task/Common.php';
169            require_once 'PEAR/Task/Replace.php';
170            require_once 'PEAR/Task/Unixeol.php';
171            require_once 'PEAR/Task/Windowseol.php';
172            require_once 'PEAR/PackageFile/v1.php';
173            require_once 'PEAR/PackageFile/v2.php';
174            require_once 'PEAR/PackageFile/Generator/v1.php';
175            require_once 'PEAR/PackageFile/Generator/v2.php';
176        }
177
178        $filelist = $this->_registry->packageInfo($package, 'filelist', $channel);
179        if ($filelist == null) {
180            return $this->raiseError("$channel/$package not installed");
181        }
182
183        $ret = array();
184        foreach ($filelist as $file => $props) {
185            if (empty($props['installed_as'])) {
186                continue;
187            }
188
189            $path = $props['installed_as'];
190            if ($backup) {
191                $this->addFileOperation('backup', array($path));
192                $ret[] = $path;
193            }
194
195            $this->addFileOperation('delete', array($path));
196        }
197
198        if ($backup) {
199            return $ret;
200        }
201
202        return true;
203    }
204
205    // }}}
206    // {{{ _installFile()
207
208    /**
209     * @param string filename
210     * @param array attributes from <file> tag in package.xml
211     * @param string path to install the file in
212     * @param array options from command-line
213     * @access private
214     */
215    function _installFile($file, $atts, $tmp_path, $options)
216    {
217        // {{{ return if this file is meant for another platform
218        static $os;
219        if (!isset($this->_registry)) {
220            $this->_registry = &$this->config->getRegistry();
221        }
222
223        if (isset($atts['platform'])) {
224            if (empty($os)) {
225                $os = new OS_Guess();
226            }
227
228            if (strlen($atts['platform']) && $atts['platform']{0} == '!') {
229                $negate   = true;
230                $platform = substr($atts['platform'], 1);
231            } else {
232                $negate    = false;
233                $platform = $atts['platform'];
234            }
235
236            if ((bool) $os->matchSignature($platform) === $negate) {
237                $this->log(3, "skipped $file (meant for $atts[platform], we are ".$os->getSignature().")");
238                return PEAR_INSTALLER_SKIPPED;
239            }
240        }
241        // }}}
242
243        $channel = $this->pkginfo->getChannel();
244        // {{{ assemble the destination paths
245        switch ($atts['role']) {
246            case 'src':
247            case 'extsrc':
248                $this->source_files++;
249                return;
250            case 'doc':
251            case 'data':
252            case 'test':
253                $dest_dir = $this->config->get($atts['role'] . '_dir', null, $channel) .
254                            DIRECTORY_SEPARATOR . $this->pkginfo->getPackage();
255                unset($atts['baseinstalldir']);
256                break;
257            case 'ext':
258            case 'php':
259                $dest_dir = $this->config->get($atts['role'] . '_dir', null, $channel);
260                break;
261            case 'script':
262                $dest_dir = $this->config->get('bin_dir', null, $channel);
263                break;
264            default:
265                return $this->raiseError("Invalid role `$atts[role]' for file $file");
266        }
267
268        $save_destdir = $dest_dir;
269        if (!empty($atts['baseinstalldir'])) {
270            $dest_dir .= DIRECTORY_SEPARATOR . $atts['baseinstalldir'];
271        }
272
273        if (dirname($file) != '.' && empty($atts['install-as'])) {
274            $dest_dir .= DIRECTORY_SEPARATOR . dirname($file);
275        }
276
277        if (empty($atts['install-as'])) {
278            $dest_file = $dest_dir . DIRECTORY_SEPARATOR . basename($file);
279        } else {
280            $dest_file = $dest_dir . DIRECTORY_SEPARATOR . $atts['install-as'];
281        }
282        $orig_file = $tmp_path . DIRECTORY_SEPARATOR . $file;
283
284        // Clean up the DIRECTORY_SEPARATOR mess
285        $ds2 = DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR;
286        list($dest_file, $orig_file) = preg_replace(array('!\\\\+!', '!/!', "!$ds2+!"),
287                                                    array(DIRECTORY_SEPARATOR,
288                                                          DIRECTORY_SEPARATOR,
289                                                          DIRECTORY_SEPARATOR),
290                                                    array($dest_file, $orig_file));
291        $final_dest_file = $installed_as = $dest_file;
292        if (isset($this->_options['packagingroot'])) {
293            $installedas_dest_dir  = dirname($final_dest_file);
294            $installedas_dest_file = $dest_dir . DIRECTORY_SEPARATOR . '.tmp' . basename($final_dest_file);
295            $final_dest_file = $this->_prependPath($final_dest_file, $this->_options['packagingroot']);
296        } else {
297            $installedas_dest_dir  = dirname($final_dest_file);
298            $installedas_dest_file = $installedas_dest_dir . DIRECTORY_SEPARATOR . '.tmp' . basename($final_dest_file);
299        }
300
301        $dest_dir  = dirname($final_dest_file);
302        $dest_file = $dest_dir . DIRECTORY_SEPARATOR . '.tmp' . basename($final_dest_file);
303        if (preg_match('~/\.\.(/|\\z)|^\.\./~', str_replace('\\', '/', $dest_file))) {
304            return $this->raiseError("SECURITY ERROR: file $file (installed to $dest_file) contains parent directory reference ..", PEAR_INSTALLER_FAILED);
305        }
306        // }}}
307
308        if (empty($this->_options['register-only']) &&
309              (!file_exists($dest_dir) || !is_dir($dest_dir))) {
310            if (!$this->mkDirHier($dest_dir)) {
311                return $this->raiseError("failed to mkdir $dest_dir",
312                                         PEAR_INSTALLER_FAILED);
313            }
314            $this->log(3, "+ mkdir $dest_dir");
315        }
316
317        // pretty much nothing happens if we are only registering the install
318        if (empty($this->_options['register-only'])) {
319            if (empty($atts['replacements'])) {
320                if (!file_exists($orig_file)) {
321                    return $this->raiseError("file $orig_file does not exist",
322                                             PEAR_INSTALLER_FAILED);
323                }
324
325                if (!@copy($orig_file, $dest_file)) {
326                    return $this->raiseError(
327                        "failed to write $dest_file: " . error_get_last()["message"],
328                        PEAR_INSTALLER_FAILED);
329                }
330
331                $this->log(3, "+ cp $orig_file $dest_file");
332                if (isset($atts['md5sum'])) {
333                    $md5sum = md5_file($dest_file);
334                }
335            } else {
336                // {{{ file with replacements
337                if (!file_exists($orig_file)) {
338                    return $this->raiseError("file does not exist",
339                                             PEAR_INSTALLER_FAILED);
340                }
341
342                $contents = file_get_contents($orig_file);
343                if ($contents === false) {
344                    $contents = '';
345                }
346
347                if (isset($atts['md5sum'])) {
348                    $md5sum = md5($contents);
349                }
350
351                $subst_from = $subst_to = array();
352                foreach ($atts['replacements'] as $a) {
353                    $to = '';
354                    if ($a['type'] == 'php-const') {
355                        if (preg_match('/^[a-z0-9_]+\\z/i', $a['to'])) {
356                            eval("\$to = $a[to];");
357                        } else {
358                            if (!isset($options['soft'])) {
359                                $this->log(0, "invalid php-const replacement: $a[to]");
360                            }
361                            continue;
362                        }
363                    } elseif ($a['type'] == 'pear-config') {
364                        if ($a['to'] == 'master_server') {
365                            $chan = $this->_registry->getChannel($channel);
366                            if (!PEAR::isError($chan)) {
367                                $to = $chan->getServer();
368                            } else {
369                                $to = $this->config->get($a['to'], null, $channel);
370                            }
371                        } else {
372                            $to = $this->config->get($a['to'], null, $channel);
373                        }
374                        if (is_null($to)) {
375                            if (!isset($options['soft'])) {
376                                $this->log(0, "invalid pear-config replacement: $a[to]");
377                            }
378                            continue;
379                        }
380                    } elseif ($a['type'] == 'package-info') {
381                        if ($t = $this->pkginfo->packageInfo($a['to'])) {
382                            $to = $t;
383                        } else {
384                            if (!isset($options['soft'])) {
385                                $this->log(0, "invalid package-info replacement: $a[to]");
386                            }
387                            continue;
388                        }
389                    }
390                    if (!is_null($to)) {
391                        $subst_from[] = $a['from'];
392                        $subst_to[] = $to;
393                    }
394                }
395
396                $this->log(3, "doing ".sizeof($subst_from)." substitution(s) for $final_dest_file");
397                if (sizeof($subst_from)) {
398                    $contents = str_replace($subst_from, $subst_to, $contents);
399                }
400
401                $wp = @fopen($dest_file, "wb");
402                if (!is_resource($wp)) {
403                    return $this->raiseError(
404                        "failed to create $dest_file: " . error_get_last()["message"],
405                        PEAR_INSTALLER_FAILED);
406                }
407
408                if (@fwrite($wp, $contents) === false) {
409                    return $this->raiseError(
410                        "failed writing to $dest_file: " . error_get_last()["message"],
411                        PEAR_INSTALLER_FAILED);
412                }
413
414                fclose($wp);
415                // }}}
416            }
417
418            // {{{ check the md5
419            if (isset($md5sum)) {
420                if (strtolower($md5sum) === strtolower($atts['md5sum'])) {
421                    $this->log(2, "md5sum ok: $final_dest_file");
422                } else {
423                    if (empty($options['force'])) {
424                        // delete the file
425                        if (file_exists($dest_file)) {
426                            unlink($dest_file);
427                        }
428
429                        if (!isset($options['ignore-errors'])) {
430                            return $this->raiseError("bad md5sum for file $final_dest_file",
431                                                 PEAR_INSTALLER_FAILED);
432                        }
433
434                        if (!isset($options['soft'])) {
435                            $this->log(0, "warning : bad md5sum for file $final_dest_file");
436                        }
437                    } else {
438                        if (!isset($options['soft'])) {
439                            $this->log(0, "warning : bad md5sum for file $final_dest_file");
440                        }
441                    }
442                }
443            }
444            // }}}
445            // {{{ set file permissions
446            if (!OS_WINDOWS) {
447                if ($atts['role'] == 'script') {
448                    $mode = 0777 & ~(int)octdec($this->config->get('umask'));
449                    $this->log(3, "+ chmod +x $dest_file");
450                } else {
451                    $mode = 0666 & ~(int)octdec($this->config->get('umask'));
452                }
453
454                if ($atts['role'] != 'src') {
455                    $this->addFileOperation("chmod", array($mode, $dest_file));
456                    if (!@chmod($dest_file, $mode)) {
457                        if (!isset($options['soft'])) {
458                            $this->log(0, "failed to change mode of $dest_file: " .
459                                          error_get_last()["message"]);
460                        }
461                    }
462                }
463            }
464            // }}}
465
466            if ($atts['role'] == 'src') {
467                rename($dest_file, $final_dest_file);
468                $this->log(2, "renamed source file $dest_file to $final_dest_file");
469            } else {
470                $this->addFileOperation("rename", array($dest_file, $final_dest_file,
471                    $atts['role'] == 'ext'));
472            }
473        }
474
475        // Store the full path where the file was installed for easy unistall
476        if ($atts['role'] != 'script') {
477            $loc = $this->config->get($atts['role'] . '_dir');
478        } else {
479            $loc = $this->config->get('bin_dir');
480        }
481
482        if ($atts['role'] != 'src') {
483            $this->addFileOperation("installed_as", array($file, $installed_as,
484                                    $loc,
485                                    dirname(substr($installedas_dest_file, strlen($loc)))));
486        }
487
488        //$this->log(2, "installed: $dest_file");
489        return PEAR_INSTALLER_OK;
490    }
491
492    // }}}
493    // {{{ _installFile2()
494
495    /**
496     * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2
497     * @param string filename
498     * @param array attributes from <file> tag in package.xml
499     * @param string path to install the file in
500     * @param array options from command-line
501     * @access private
502     */
503    function _installFile2(&$pkg, $file, &$real_atts, $tmp_path, $options)
504    {
505        $atts = $real_atts;
506        if (!isset($this->_registry)) {
507            $this->_registry = &$this->config->getRegistry();
508        }
509
510        $channel = $pkg->getChannel();
511        // {{{ assemble the destination paths
512        if (!in_array($atts['attribs']['role'],
513              PEAR_Installer_Role::getValidRoles($pkg->getPackageType()))) {
514            return $this->raiseError('Invalid role `' . $atts['attribs']['role'] .
515                    "' for file $file");
516        }
517
518        $role = &PEAR_Installer_Role::factory($pkg, $atts['attribs']['role'], $this->config);
519        $err  = $role->setup($this, $pkg, $atts['attribs'], $file);
520        if (PEAR::isError($err)) {
521            return $err;
522        }
523
524        if (!$role->isInstallable()) {
525            return;
526        }
527
528        $info = $role->processInstallation($pkg, $atts['attribs'], $file, $tmp_path);
529        if (PEAR::isError($info)) {
530            return $info;
531        }
532
533        list($save_destdir, $dest_dir, $dest_file, $orig_file) = $info;
534        if (preg_match('~/\.\.(/|\\z)|^\.\./~', str_replace('\\', '/', $dest_file))) {
535            return $this->raiseError("SECURITY ERROR: file $file (installed to $dest_file) contains parent directory reference ..", PEAR_INSTALLER_FAILED);
536        }
537
538        $final_dest_file = $installed_as = $dest_file;
539        if (isset($this->_options['packagingroot'])) {
540            $final_dest_file = $this->_prependPath($final_dest_file,
541                $this->_options['packagingroot']);
542        }
543
544        $dest_dir  = dirname($final_dest_file);
545        $dest_file = $dest_dir . DIRECTORY_SEPARATOR . '.tmp' . basename($final_dest_file);
546        // }}}
547
548        if (empty($this->_options['register-only'])) {
549            if (!file_exists($dest_dir) || !is_dir($dest_dir)) {
550                if (!$this->mkDirHier($dest_dir)) {
551                    return $this->raiseError("failed to mkdir $dest_dir",
552                                             PEAR_INSTALLER_FAILED);
553                }
554                $this->log(3, "+ mkdir $dest_dir");
555            }
556        }
557
558        $attribs = $atts['attribs'];
559        unset($atts['attribs']);
560        // pretty much nothing happens if we are only registering the install
561        if (empty($this->_options['register-only'])) {
562            if (!count($atts)) { // no tasks
563                if (!file_exists($orig_file)) {
564                    return $this->raiseError("file $orig_file does not exist",
565                                             PEAR_INSTALLER_FAILED);
566                }
567
568                if (!@copy($orig_file, $dest_file)) {
569                    return $this->raiseError(
570                        "failed to write $dest_file: " . error_get_last()["message"],
571                        PEAR_INSTALLER_FAILED);
572                }
573
574                $this->log(3, "+ cp $orig_file $dest_file");
575                if (isset($attribs['md5sum'])) {
576                    $md5sum = md5_file($dest_file);
577                }
578            } else { // file with tasks
579                if (!file_exists($orig_file)) {
580                    return $this->raiseError("file $orig_file does not exist",
581                                             PEAR_INSTALLER_FAILED);
582                }
583
584                $contents = file_get_contents($orig_file);
585                if ($contents === false) {
586                    $contents = '';
587                }
588
589                if (isset($attribs['md5sum'])) {
590                    $md5sum = md5($contents);
591                }
592
593                foreach ($atts as $tag => $raw) {
594                    $tag = str_replace(array($pkg->getTasksNs() . ':', '-'), array('', '_'), $tag);
595                    $task = "PEAR_Task_$tag";
596                    $task = new $task($this->config, $this, PEAR_TASK_INSTALL);
597                    if (!$task->isScript()) { // scripts are only handled after installation
598                        $task->init($raw, $attribs, $pkg->getLastInstalledVersion());
599                        $res = $task->startSession($pkg, $contents, $final_dest_file);
600                        if ($res === false) {
601                            continue; // skip this file
602                        }
603
604                        if (PEAR::isError($res)) {
605                            return $res;
606                        }
607
608                        $contents = $res; // save changes
609                    }
610
611                    $wp = @fopen($dest_file, "wb");
612                    if (!is_resource($wp)) {
613                        return $this->raiseError(
614                            "failed to create $dest_file: " . error_get_last()["message"],
615                            PEAR_INSTALLER_FAILED);
616                    }
617
618                    if (fwrite($wp, $contents) === false) {
619                        return $this->raiseError(
620                            "failed writing to $dest_file: " . error_get_last()["message"],
621                            PEAR_INSTALLER_FAILED);
622                    }
623
624                    fclose($wp);
625                }
626            }
627
628            // {{{ check the md5
629            if (isset($md5sum)) {
630                // Make sure the original md5 sum matches with expected
631                if (strtolower($md5sum) === strtolower($attribs['md5sum'])) {
632                    $this->log(2, "md5sum ok: $final_dest_file");
633
634                    if (isset($contents)) {
635                        // set md5 sum based on $content in case any tasks were run.
636                        $real_atts['attribs']['md5sum'] = md5($contents);
637                    }
638                } else {
639                    if (empty($options['force'])) {
640                        // delete the file
641                        if (file_exists($dest_file)) {
642                            unlink($dest_file);
643                        }
644
645                        if (!isset($options['ignore-errors'])) {
646                            return $this->raiseError("bad md5sum for file $final_dest_file",
647                                                     PEAR_INSTALLER_FAILED);
648                        }
649
650                        if (!isset($options['soft'])) {
651                            $this->log(0, "warning : bad md5sum for file $final_dest_file");
652                        }
653                    } else {
654                        if (!isset($options['soft'])) {
655                            $this->log(0, "warning : bad md5sum for file $final_dest_file");
656                        }
657                    }
658                }
659            } else {
660                $real_atts['attribs']['md5sum'] = md5_file($dest_file);
661            }
662
663            // }}}
664            // {{{ set file permissions
665            if (!OS_WINDOWS) {
666                if ($role->isExecutable()) {
667                    $mode = 0777 & ~(int)octdec($this->config->get('umask'));
668                    $this->log(3, "+ chmod +x $dest_file");
669                } else {
670                    $mode = 0666 & ~(int)octdec($this->config->get('umask'));
671                }
672
673                if ($attribs['role'] != 'src') {
674                    $this->addFileOperation("chmod", array($mode, $dest_file));
675                    if (!@chmod($dest_file, $mode)) {
676                        if (!isset($options['soft'])) {
677                            $this->log(0, "failed to change mode of $dest_file: " .
678                                          error_get_last()["message"]);
679                        }
680                    }
681                }
682            }
683            // }}}
684
685            if ($attribs['role'] == 'src') {
686                rename($dest_file, $final_dest_file);
687                $this->log(2, "renamed source file $dest_file to $final_dest_file");
688            } else {
689                $this->addFileOperation("rename", array($dest_file, $final_dest_file, $role->isExtension()));
690            }
691        }
692
693        // Store the full path where the file was installed for easy uninstall
694        if ($attribs['role'] != 'src') {
695            $loc = $this->config->get($role->getLocationConfig(), null, $channel);
696            $this->addFileOperation('installed_as', array($file, $installed_as,
697                                $loc,
698                                dirname(substr($installed_as, strlen($loc)))));
699        }
700
701        //$this->log(2, "installed: $dest_file");
702        return PEAR_INSTALLER_OK;
703    }
704
705    // }}}
706    // {{{ addFileOperation()
707
708    /**
709     * Add a file operation to the current file transaction.
710     *
711     * @see startFileTransaction()
712     * @param string $type This can be one of:
713     *    - rename:  rename a file ($data has 3 values)
714     *    - backup:  backup an existing file ($data has 1 value)
715     *    - removebackup:  clean up backups created during install ($data has 1 value)
716     *    - chmod:   change permissions on a file ($data has 2 values)
717     *    - delete:  delete a file ($data has 1 value)
718     *    - rmdir:   delete a directory if empty ($data has 1 value)
719     *    - installed_as: mark a file as installed ($data has 4 values).
720     * @param array $data For all file operations, this array must contain the
721     *    full path to the file or directory that is being operated on.  For
722     *    the rename command, the first parameter must be the file to rename,
723     *    the second its new name, the third whether this is a PHP extension.
724     *
725     *    The installed_as operation contains 4 elements in this order:
726     *    1. Filename as listed in the filelist element from package.xml
727     *    2. Full path to the installed file
728     *    3. Full path from the php_dir configuration variable used in this
729     *       installation
730     *    4. Relative path from the php_dir that this file is installed in
731     */
732    function addFileOperation($type, $data)
733    {
734        if (!is_array($data)) {
735            return $this->raiseError('Internal Error: $data in addFileOperation'
736                . ' must be an array, was ' . gettype($data));
737        }
738
739        if ($type == 'chmod') {
740            $octmode = decoct($data[0]);
741            $this->log(3, "adding to transaction: $type $octmode $data[1]");
742        } else {
743            $this->log(3, "adding to transaction: $type " . implode(" ", $data));
744        }
745        $this->file_operations[] = array($type, $data);
746    }
747
748    // }}}
749    // {{{ startFileTransaction()
750
751    function startFileTransaction($rollback_in_case = false)
752    {
753        if (count($this->file_operations) && $rollback_in_case) {
754            $this->rollbackFileTransaction();
755        }
756        $this->file_operations = array();
757    }
758
759    // }}}
760    // {{{ commitFileTransaction()
761
762    function commitFileTransaction()
763    {
764        // {{{ first, check permissions and such manually
765        $errors = array();
766        foreach ($this->file_operations as $key => $tr) {
767            list($type, $data) = $tr;
768            switch ($type) {
769                case 'rename':
770                    if (!file_exists($data[0])) {
771                        $errors[] = "cannot rename file $data[0], doesn't exist";
772                    }
773
774                    // check that dest dir. is writable
775                    if (!is_writable(dirname($data[1]))) {
776                        $errors[] = "permission denied ($type): $data[1]";
777                    }
778                    break;
779                case 'chmod':
780                    // check that file is writable
781                    if (!is_writable($data[1])) {
782                        $errors[] = "permission denied ($type): $data[1] " . decoct($data[0]);
783                    }
784                    break;
785                case 'delete':
786                    if (!file_exists($data[0])) {
787                        $this->log(2, "warning: file $data[0] doesn't exist, can't be deleted");
788                    }
789                    // check that directory is writable
790                    if (file_exists($data[0])) {
791                        if (!is_writable(dirname($data[0]))) {
792                            $errors[] = "permission denied ($type): $data[0]";
793                        } else {
794                            // make sure the file to be deleted can be opened for writing
795                            $fp = false;
796                            if (!is_dir($data[0]) &&
797                                  (!is_writable($data[0]) || !($fp = @fopen($data[0], 'a')))) {
798                                $errors[] = "permission denied ($type): $data[0]";
799                            } elseif ($fp) {
800                                fclose($fp);
801                            }
802                        }
803
804                        /* Verify we are not deleting a file owned by another package
805                         * This can happen when a file moves from package A to B in
806                         * an upgrade ala http://pear.php.net/17986
807                         */
808                        $info = array(
809                            'package' => strtolower($this->pkginfo->getName()),
810                            'channel' => strtolower($this->pkginfo->getChannel()),
811                        );
812                        $result = $this->_registry->checkFileMap($data[0], $info, '1.1');
813                        if (is_array($result)) {
814                            $res = array_diff($result, $info);
815                            if (!empty($res)) {
816                                $new = $this->_registry->getPackage($result[1], $result[0]);
817                                $this->file_operations[$key] = false;
818                                $pkginfoName = $this->pkginfo->getName();
819                                $newChannel  = $new->getChannel();
820                                $newPackage  = $new->getName();
821                                $this->log(3, "file $data[0] was scheduled for removal from $pkginfoName but is owned by $newChannel/$newPackage, removal has been cancelled.");
822                            }
823                        }
824                    }
825                    break;
826            }
827
828        }
829        // }}}
830
831        $n = count($this->file_operations);
832        $this->log(2, "about to commit $n file operations for " . $this->pkginfo->getName());
833
834        $m = count($errors);
835        if ($m > 0) {
836            foreach ($errors as $error) {
837                if (!isset($this->_options['soft'])) {
838                    $this->log(1, $error);
839                }
840            }
841
842            if (!isset($this->_options['ignore-errors'])) {
843                return false;
844            }
845        }
846
847        $this->_dirtree = array();
848        // {{{ really commit the transaction
849        foreach ($this->file_operations as $i => $tr) {
850            if (!$tr) {
851                // support removal of non-existing backups
852                continue;
853            }
854
855            list($type, $data) = $tr;
856            switch ($type) {
857                case 'backup':
858                    if (!file_exists($data[0])) {
859                        $this->file_operations[$i] = false;
860                        break;
861                    }
862
863                    if (!@copy($data[0], $data[0] . '.bak')) {
864                        $this->log(1, 'Could not copy ' . $data[0] . ' to ' . $data[0] .
865                            '.bak ' . error_get_last()["message"]);
866                        return false;
867                    }
868                    $this->log(3, "+ backup $data[0] to $data[0].bak");
869                    break;
870                case 'removebackup':
871                    if (file_exists($data[0] . '.bak') && is_writable($data[0] . '.bak')) {
872                        unlink($data[0] . '.bak');
873                        $this->log(3, "+ rm backup of $data[0] ($data[0].bak)");
874                    }
875                    break;
876                case 'rename':
877                    $test = file_exists($data[1]) ? @unlink($data[1]) : null;
878                    if (!$test && file_exists($data[1])) {
879                        if ($data[2]) {
880                            $extra = ', this extension must be installed manually.  Rename to "' .
881                                basename($data[1]) . '"';
882                        } else {
883                            $extra = '';
884                        }
885
886                        if (!isset($this->_options['soft'])) {
887                            $this->log(1, 'Could not delete ' . $data[1] . ', cannot rename ' .
888                                $data[0] . $extra);
889                        }
890
891                        if (!isset($this->_options['ignore-errors'])) {
892                            return false;
893                        }
894                    }
895
896                    // permissions issues with rename - copy() is far superior
897                    $perms = @fileperms($data[0]);
898                    if (!@copy($data[0], $data[1])) {
899                        $this->log(1, 'Could not rename ' . $data[0] . ' to ' . $data[1] .
900                            ' ' . error_get_last()["message"]);
901                        return false;
902                    }
903
904                    // copy over permissions, otherwise they are lost
905                    @chmod($data[1], $perms);
906                    @unlink($data[0]);
907                    $this->log(3, "+ mv $data[0] $data[1]");
908                    break;
909                case 'chmod':
910                    if (!@chmod($data[1], $data[0])) {
911                        $this->log(1, 'Could not chmod ' . $data[1] . ' to ' .
912                            decoct($data[0]) . ' ' . error_get_last()["message"]);
913                        return false;
914                    }
915
916                    $octmode = decoct($data[0]);
917                    $this->log(3, "+ chmod $octmode $data[1]");
918                    break;
919                case 'delete':
920                    if (file_exists($data[0])) {
921                        if (!@unlink($data[0])) {
922                            $this->log(1, 'Could not delete ' . $data[0] . ' ' .
923                                error_get_last()["message"]);
924                            return false;
925                        }
926                        $this->log(3, "+ rm $data[0]");
927                    }
928                    break;
929                case 'rmdir':
930                    if (file_exists($data[0])) {
931                        do {
932                            $testme = opendir($data[0]);
933                            while (false !== ($entry = readdir($testme))) {
934                                if ($entry == '.' || $entry == '..') {
935                                    continue;
936                                }
937                                closedir($testme);
938                                break 2; // this directory is not empty and can't be
939                                         // deleted
940                            }
941
942                            closedir($testme);
943                            if (!@rmdir($data[0])) {
944                                $this->log(1, 'Could not rmdir ' . $data[0] . ' ' .
945                                    error_get_last()["message"]);
946                                return false;
947                            }
948                            $this->log(3, "+ rmdir $data[0]");
949                        } while (false);
950                    }
951                    break;
952                case 'installed_as':
953                    $this->pkginfo->setInstalledAs($data[0], $data[1]);
954                    if (!isset($this->_dirtree[dirname($data[1])])) {
955                        $this->_dirtree[dirname($data[1])] = true;
956                        $this->pkginfo->setDirtree(dirname($data[1]));
957
958                        while(!empty($data[3]) && dirname($data[3]) != $data[3] &&
959                                $data[3] != '/' && $data[3] != '\\') {
960                            $this->pkginfo->setDirtree($pp =
961                                $this->_prependPath($data[3], $data[2]));
962                            $this->_dirtree[$pp] = true;
963                            $data[3] = dirname($data[3]);
964                        }
965                    }
966                    break;
967            }
968        }
969        // }}}
970        $this->log(2, "successfully committed $n file operations");
971        $this->file_operations = array();
972        return true;
973    }
974
975    // }}}
976    // {{{ rollbackFileTransaction()
977
978    function rollbackFileTransaction()
979    {
980        $n = count($this->file_operations);
981        $this->log(2, "rolling back $n file operations");
982        foreach ($this->file_operations as $tr) {
983            list($type, $data) = $tr;
984            switch ($type) {
985                case 'backup':
986                    if (file_exists($data[0] . '.bak')) {
987                        if (file_exists($data[0] && is_writable($data[0]))) {
988                            unlink($data[0]);
989                        }
990                        @copy($data[0] . '.bak', $data[0]);
991                        $this->log(3, "+ restore $data[0] from $data[0].bak");
992                    }
993                    break;
994                case 'removebackup':
995                    if (file_exists($data[0] . '.bak') && is_writable($data[0] . '.bak')) {
996                        unlink($data[0] . '.bak');
997                        $this->log(3, "+ rm backup of $data[0] ($data[0].bak)");
998                    }
999                    break;
1000                case 'rename':
1001                    @unlink($data[0]);
1002                    $this->log(3, "+ rm $data[0]");
1003                    break;
1004                case 'mkdir':
1005                    @rmdir($data[0]);
1006                    $this->log(3, "+ rmdir $data[0]");
1007                    break;
1008                case 'chmod':
1009                    break;
1010                case 'delete':
1011                    break;
1012                case 'installed_as':
1013                    $this->pkginfo->setInstalledAs($data[0], false);
1014                    break;
1015            }
1016        }
1017        $this->pkginfo->resetDirtree();
1018        $this->file_operations = array();
1019    }
1020
1021    // }}}
1022    // {{{ mkDirHier($dir)
1023
1024    function mkDirHier($dir)
1025    {
1026        $this->addFileOperation('mkdir', array($dir));
1027        return parent::mkDirHier($dir);
1028    }
1029
1030    // }}}
1031    // {{{ _parsePackageXml()
1032
1033    function _parsePackageXml(&$descfile)
1034    {
1035        // Parse xml file -----------------------------------------------
1036        $pkg = new PEAR_PackageFile($this->config, $this->debug);
1037        PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
1038        $p = &$pkg->fromAnyFile($descfile, PEAR_VALIDATE_INSTALLING);
1039        PEAR::staticPopErrorHandling();
1040        if (PEAR::isError($p)) {
1041            if (is_array($p->getUserInfo())) {
1042                foreach ($p->getUserInfo() as $err) {
1043                    $loglevel = $err['level'] == 'error' ? 0 : 1;
1044                    if (!isset($this->_options['soft'])) {
1045                        $this->log($loglevel, ucfirst($err['level']) . ': ' . $err['message']);
1046                    }
1047                }
1048            }
1049            return $this->raiseError('Installation failed: invalid package file');
1050        }
1051
1052        $descfile = $p->getPackageFile();
1053        return $p;
1054    }
1055
1056    // }}}
1057    /**
1058     * Set the list of PEAR_Downloader_Package objects to allow more sane
1059     * dependency validation
1060     * @param array
1061     */
1062    function setDownloadedPackages(&$pkgs)
1063    {
1064        PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
1065        $err = $this->analyzeDependencies($pkgs);
1066        PEAR::popErrorHandling();
1067        if (PEAR::isError($err)) {
1068            return $err;
1069        }
1070        $this->_downloadedPackages = &$pkgs;
1071    }
1072
1073    /**
1074     * Set the list of PEAR_Downloader_Package objects to allow more sane
1075     * dependency validation
1076     * @param array
1077     */
1078    function setUninstallPackages(&$pkgs)
1079    {
1080        $this->_downloadedPackages = &$pkgs;
1081    }
1082
1083    function getInstallPackages()
1084    {
1085        return $this->_downloadedPackages;
1086    }
1087
1088    // {{{ install()
1089
1090    /**
1091     * Installs the files within the package file specified.
1092     *
1093     * @param string|PEAR_Downloader_Package $pkgfile path to the package file,
1094     *        or a pre-initialized packagefile object
1095     * @param array $options
1096     * recognized options:
1097     * - installroot   : optional prefix directory for installation
1098     * - force         : force installation
1099     * - register-only : update registry but don't install files
1100     * - upgrade       : upgrade existing install
1101     * - soft          : fail silently
1102     * - nodeps        : ignore dependency conflicts/missing dependencies
1103     * - alldeps       : install all dependencies
1104     * - onlyreqdeps   : install only required dependencies
1105     *
1106     * @return array|PEAR_Error package info if successful
1107     */
1108    function install($pkgfile, $options = array())
1109    {
1110        $this->_options = $options;
1111        $this->_registry = &$this->config->getRegistry();
1112        if (is_object($pkgfile)) {
1113            $dlpkg    = &$pkgfile;
1114            $pkg      = $pkgfile->getPackageFile();
1115            $pkgfile  = $pkg->getArchiveFile();
1116            $descfile = $pkg->getPackageFile();
1117        } else {
1118            $descfile = $pkgfile;
1119            $pkg      = $this->_parsePackageXml($descfile);
1120            if (PEAR::isError($pkg)) {
1121                return $pkg;
1122            }
1123        }
1124
1125        $tmpdir = dirname($descfile);
1126        if (realpath($descfile) != realpath($pkgfile)) {
1127            // Use the temp_dir since $descfile can contain the download dir path
1128            $tmpdir = $this->config->get('temp_dir', null, 'pear.php.net');
1129            $tmpdir = System::mktemp('-d -t "' . $tmpdir . '"');
1130
1131            $tar = new Archive_Tar($pkgfile);
1132            if (!$tar->extract($tmpdir)) {
1133                return $this->raiseError("unable to unpack $pkgfile");
1134            }
1135        }
1136
1137        $pkgname = $pkg->getName();
1138        $channel = $pkg->getChannel();
1139
1140        if (isset($options['installroot'])) {
1141            $this->config->setInstallRoot($options['installroot']);
1142            $this->_registry = &$this->config->getRegistry();
1143            $installregistry = &$this->_registry;
1144            $this->installroot = ''; // all done automagically now
1145            $php_dir = $this->config->get('php_dir', null, $channel);
1146        } else {
1147            $this->config->setInstallRoot(false);
1148            $this->_registry = &$this->config->getRegistry();
1149            if (isset($this->_options['packagingroot'])) {
1150                $regdir = $this->_prependPath(
1151                    $this->config->get('php_dir', null, 'pear.php.net'),
1152                    $this->_options['packagingroot']);
1153
1154                $metadata_dir = $this->config->get('metadata_dir', null, 'pear.php.net');
1155                if ($metadata_dir) {
1156                    $metadata_dir = $this->_prependPath(
1157                        $metadata_dir,
1158                        $this->_options['packagingroot']);
1159                }
1160                $packrootphp_dir = $this->_prependPath(
1161                    $this->config->get('php_dir', null, $channel),
1162                    $this->_options['packagingroot']);
1163
1164                $installregistry = new PEAR_Registry($regdir, false, false, $metadata_dir);
1165                if (!$installregistry->channelExists($channel, true)) {
1166                    // we need to fake a channel-discover of this channel
1167                    $chanobj = $this->_registry->getChannel($channel, true);
1168                    $installregistry->addChannel($chanobj);
1169                }
1170                $php_dir = $packrootphp_dir;
1171            } else {
1172                $installregistry = &$this->_registry;
1173                $php_dir = $this->config->get('php_dir', null, $channel);
1174            }
1175            $this->installroot = '';
1176        }
1177
1178        // {{{ checks to do when not in "force" mode
1179        if (empty($options['force']) &&
1180              (file_exists($this->config->get('php_dir')) &&
1181               is_dir($this->config->get('php_dir')))) {
1182            $testp = $channel == 'pear.php.net' ? $pkgname : array($channel, $pkgname);
1183            $instfilelist = $pkg->getInstallationFileList(true);
1184            if (PEAR::isError($instfilelist)) {
1185                return $instfilelist;
1186            }
1187
1188            // ensure we have the most accurate registry
1189            $installregistry->flushFileMap();
1190            $test = $installregistry->checkFileMap($instfilelist, $testp, '1.1');
1191            if (PEAR::isError($test)) {
1192                return $test;
1193            }
1194
1195            if (sizeof($test)) {
1196                $pkgs = $this->getInstallPackages();
1197                $found = false;
1198                foreach ($pkgs as $param) {
1199                    if ($pkg->isSubpackageOf($param)) {
1200                        $found = true;
1201                        break;
1202                    }
1203                }
1204
1205                if ($found) {
1206                    // subpackages can conflict with earlier versions of parent packages
1207                    $parentreg = $installregistry->packageInfo($param->getPackage(), null, $param->getChannel());
1208                    $tmp = $test;
1209                    foreach ($tmp as $file => $info) {
1210                        if (is_array($info)) {
1211                            if (strtolower($info[1]) == strtolower($param->getPackage()) &&
1212                                  strtolower($info[0]) == strtolower($param->getChannel())
1213                            ) {
1214                                if (isset($parentreg['filelist'][$file])) {
1215                                    unset($parentreg['filelist'][$file]);
1216                                } else{
1217                                    $pos     = strpos($file, '/');
1218                                    $basedir = substr($file, 0, $pos);
1219                                    $file2   = substr($file, $pos + 1);
1220                                    if (isset($parentreg['filelist'][$file2]['baseinstalldir'])
1221                                        && $parentreg['filelist'][$file2]['baseinstalldir'] === $basedir
1222                                    ) {
1223                                        unset($parentreg['filelist'][$file2]);
1224                                    }
1225                                }
1226
1227                                unset($test[$file]);
1228                            }
1229                        } else {
1230                            if (strtolower($param->getChannel()) != 'pear.php.net') {
1231                                continue;
1232                            }
1233
1234                            if (strtolower($info) == strtolower($param->getPackage())) {
1235                                if (isset($parentreg['filelist'][$file])) {
1236                                    unset($parentreg['filelist'][$file]);
1237                                } else{
1238                                    $pos     = strpos($file, '/');
1239                                    $basedir = substr($file, 0, $pos);
1240                                    $file2   = substr($file, $pos + 1);
1241                                    if (isset($parentreg['filelist'][$file2]['baseinstalldir'])
1242                                        && $parentreg['filelist'][$file2]['baseinstalldir'] === $basedir
1243                                    ) {
1244                                        unset($parentreg['filelist'][$file2]);
1245                                    }
1246                                }
1247
1248                                unset($test[$file]);
1249                            }
1250                        }
1251                    }
1252
1253                    $pfk = new PEAR_PackageFile($this->config);
1254                    $parentpkg = &$pfk->fromArray($parentreg);
1255                    $installregistry->updatePackage2($parentpkg);
1256                }
1257
1258                if ($param->getChannel() == 'pecl.php.net' && isset($options['upgrade'])) {
1259                    $tmp = $test;
1260                    foreach ($tmp as $file => $info) {
1261                        if (is_string($info)) {
1262                            // pear.php.net packages are always stored as strings
1263                            if (strtolower($info) == strtolower($param->getPackage())) {
1264                                // upgrading existing package
1265                                unset($test[$file]);
1266                            }
1267                        }
1268                    }
1269                }
1270
1271                if (count($test)) {
1272                    $msg = "$channel/$pkgname: conflicting files found:\n";
1273                    $longest = max(array_map("strlen", array_keys($test)));
1274                    $fmt = "%${longest}s (%s)\n";
1275                    foreach ($test as $file => $info) {
1276                        if (!is_array($info)) {
1277                            $info = array('pear.php.net', $info);
1278                        }
1279                        $info = $info[0] . '/' . $info[1];
1280                        $msg .= sprintf($fmt, $file, $info);
1281                    }
1282
1283                    if (!isset($options['ignore-errors'])) {
1284                        return $this->raiseError($msg);
1285                    }
1286
1287                    if (!isset($options['soft'])) {
1288                        $this->log(0, "WARNING: $msg");
1289                    }
1290                }
1291            }
1292        }
1293        // }}}
1294
1295        $this->startFileTransaction();
1296
1297        $usechannel = $channel;
1298        if ($channel == 'pecl.php.net') {
1299            $test = $installregistry->packageExists($pkgname, $channel);
1300            if (!$test) {
1301                $test = $installregistry->packageExists($pkgname, 'pear.php.net');
1302                $usechannel = 'pear.php.net';
1303            }
1304        } else {
1305            $test = $installregistry->packageExists($pkgname, $channel);
1306        }
1307
1308        if (empty($options['upgrade']) && empty($options['soft'])) {
1309            // checks to do only when installing new packages
1310            if (empty($options['force']) && $test) {
1311                return $this->raiseError("$channel/$pkgname is already installed");
1312            }
1313        } else {
1314            // Upgrade
1315            if ($test) {
1316                $v1 = $installregistry->packageInfo($pkgname, 'version', $usechannel);
1317                $v2 = $pkg->getVersion();
1318                $cmp = version_compare("$v1", "$v2", 'gt');
1319                if (empty($options['force']) && !version_compare("$v2", "$v1", 'gt')) {
1320                    return $this->raiseError("upgrade to a newer version ($v2 is not newer than $v1)");
1321                }
1322            }
1323        }
1324
1325        // Do cleanups for upgrade and install, remove old release's files first
1326        if ($test && empty($options['register-only'])) {
1327            // when upgrading, remove old release's files first:
1328            if (PEAR::isError($err = $this->_deletePackageFiles($pkgname, $usechannel,
1329                  true))) {
1330                if (!isset($options['ignore-errors'])) {
1331                    return $this->raiseError($err);
1332                }
1333
1334                if (!isset($options['soft'])) {
1335                    $this->log(0, 'WARNING: ' . $err->getMessage());
1336                }
1337            } else {
1338                $backedup = $err;
1339            }
1340        }
1341
1342        // {{{ Copy files to dest dir ---------------------------------------
1343
1344        // info from the package it self we want to access from _installFile
1345        $this->pkginfo = &$pkg;
1346        // used to determine whether we should build any C code
1347        $this->source_files = 0;
1348
1349        $savechannel = $this->config->get('default_channel');
1350        if (empty($options['register-only']) && !is_dir($php_dir)) {
1351            if (PEAR::isError(System::mkdir(array('-p'), $php_dir))) {
1352                return $this->raiseError("no installation destination directory '$php_dir'\n");
1353            }
1354        }
1355
1356        if (substr($pkgfile, -4) != '.xml') {
1357            $tmpdir .= DIRECTORY_SEPARATOR . $pkgname . '-' . $pkg->getVersion();
1358        }
1359
1360        $this->configSet('default_channel', $channel);
1361        // {{{ install files
1362
1363        $ver = $pkg->getPackagexmlVersion();
1364        if (version_compare($ver, '2.0', '>=')) {
1365            $filelist = $pkg->getInstallationFilelist();
1366        } else {
1367            $filelist = $pkg->getFileList();
1368        }
1369
1370        if (PEAR::isError($filelist)) {
1371            return $filelist;
1372        }
1373
1374        $p = &$installregistry->getPackage($pkgname, $channel);
1375        $dirtree = (empty($options['register-only']) && $p) ? $p->getDirTree() : false;
1376
1377        $pkg->resetFilelist();
1378        $pkg->setLastInstalledVersion($installregistry->packageInfo($pkg->getPackage(),
1379            'version', $pkg->getChannel()));
1380        foreach ($filelist as $file => $atts) {
1381            $this->expectError(PEAR_INSTALLER_FAILED);
1382            if ($pkg->getPackagexmlVersion() == '1.0') {
1383                $res = $this->_installFile($file, $atts, $tmpdir, $options);
1384            } else {
1385                $res = $this->_installFile2($pkg, $file, $atts, $tmpdir, $options);
1386            }
1387            $this->popExpect();
1388
1389            if (PEAR::isError($res)) {
1390                if (empty($options['ignore-errors'])) {
1391                    $this->rollbackFileTransaction();
1392                    if ($res->getMessage() == "file does not exist") {
1393                        $this->raiseError("file $file in package.xml does not exist");
1394                    }
1395
1396                    return $this->raiseError($res);
1397                }
1398
1399                if (!isset($options['soft'])) {
1400                    $this->log(0, "Warning: " . $res->getMessage());
1401                }
1402            }
1403
1404            $real = isset($atts['attribs']) ? $atts['attribs'] : $atts;
1405            if ($res == PEAR_INSTALLER_OK && $real['role'] != 'src') {
1406                // Register files that were installed
1407                $pkg->installedFile($file, $atts);
1408            }
1409        }
1410        // }}}
1411
1412        // {{{ compile and install source files
1413        if ($this->source_files > 0 && empty($options['nobuild'])) {
1414            if (PEAR::isError($err =
1415                  $this->_compileSourceFiles($savechannel, $pkg))) {
1416                return $err;
1417            }
1418        }
1419        // }}}
1420
1421        if (isset($backedup)) {
1422            $this->_removeBackups($backedup);
1423        }
1424
1425        if (!$this->commitFileTransaction()) {
1426            $this->rollbackFileTransaction();
1427            $this->configSet('default_channel', $savechannel);
1428            return $this->raiseError("commit failed", PEAR_INSTALLER_FAILED);
1429        }
1430        // }}}
1431
1432        $ret          = false;
1433        $installphase = 'install';
1434        $oldversion   = false;
1435        // {{{ Register that the package is installed -----------------------
1436        if (empty($options['upgrade'])) {
1437            // if 'force' is used, replace the info in registry
1438            $usechannel = $channel;
1439            if ($channel == 'pecl.php.net') {
1440                $test = $installregistry->packageExists($pkgname, $channel);
1441                if (!$test) {
1442                    $test = $installregistry->packageExists($pkgname, 'pear.php.net');
1443                    $usechannel = 'pear.php.net';
1444                }
1445            } else {
1446                $test = $installregistry->packageExists($pkgname, $channel);
1447            }
1448
1449            if (!empty($options['force']) && $test) {
1450                $oldversion = $installregistry->packageInfo($pkgname, 'version', $usechannel);
1451                $installregistry->deletePackage($pkgname, $usechannel);
1452            }
1453            $ret = $installregistry->addPackage2($pkg);
1454        } else {
1455            if ($dirtree) {
1456                $this->startFileTransaction();
1457                // attempt to delete empty directories
1458                uksort($dirtree, array($this, '_sortDirs'));
1459                foreach($dirtree as $dir => $notused) {
1460                    $this->addFileOperation('rmdir', array($dir));
1461                }
1462                $this->commitFileTransaction();
1463            }
1464
1465            $usechannel = $channel;
1466            if ($channel == 'pecl.php.net') {
1467                $test = $installregistry->packageExists($pkgname, $channel);
1468                if (!$test) {
1469                    $test = $installregistry->packageExists($pkgname, 'pear.php.net');
1470                    $usechannel = 'pear.php.net';
1471                }
1472            } else {
1473                $test = $installregistry->packageExists($pkgname, $channel);
1474            }
1475
1476            // new: upgrade installs a package if it isn't installed
1477            if (!$test) {
1478                $ret = $installregistry->addPackage2($pkg);
1479            } else {
1480                if ($usechannel != $channel) {
1481                    $installregistry->deletePackage($pkgname, $usechannel);
1482                    $ret = $installregistry->addPackage2($pkg);
1483                } else {
1484                    $ret = $installregistry->updatePackage2($pkg);
1485                }
1486                $installphase = 'upgrade';
1487            }
1488        }
1489
1490        if (!$ret) {
1491            $this->configSet('default_channel', $savechannel);
1492            return $this->raiseError("Adding package $channel/$pkgname to registry failed");
1493        }
1494        // }}}
1495
1496        $this->configSet('default_channel', $savechannel);
1497        if (class_exists('PEAR_Task_Common')) { // this is auto-included if any tasks exist
1498            if (PEAR_Task_Common::hasPostinstallTasks()) {
1499                PEAR_Task_Common::runPostinstallTasks($installphase);
1500            }
1501        }
1502
1503        return $pkg->toArray(true);
1504    }
1505
1506    // }}}
1507
1508    // {{{ _compileSourceFiles()
1509    /**
1510     * @param string
1511     * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2
1512     */
1513    function _compileSourceFiles($savechannel, &$filelist)
1514    {
1515        require_once 'PEAR/Builder.php';
1516        $this->log(1, "$this->source_files source files, building");
1517        $bob = new PEAR_Builder($this->ui);
1518        $bob->debug = $this->debug;
1519        $built = $bob->build($filelist, array(&$this, '_buildCallback'));
1520        if (PEAR::isError($built)) {
1521            $this->rollbackFileTransaction();
1522            $this->configSet('default_channel', $savechannel);
1523            return $built;
1524        }
1525
1526        $this->log(1, "\nBuild process completed successfully");
1527        foreach ($built as $ext) {
1528            $bn = basename($ext['file']);
1529            list($_ext_name, $_ext_suff) = explode('.', $bn);
1530            if ($_ext_suff == '.so' || $_ext_suff == '.dll') {
1531                if (extension_loaded($_ext_name)) {
1532                    $this->raiseError("Extension '$_ext_name' already loaded. " .
1533                                      'Please unload it in your php.ini file ' .
1534                                      'prior to install or upgrade');
1535                }
1536                $role = 'ext';
1537            } else {
1538                $role = 'src';
1539            }
1540
1541            $dest = $ext['dest'];
1542            $packagingroot = '';
1543            if (isset($this->_options['packagingroot'])) {
1544                $packagingroot = $this->_options['packagingroot'];
1545            }
1546
1547            $copyto = $this->_prependPath($dest, $packagingroot);
1548            $extra  = $copyto != $dest ? " as '$copyto'" : '';
1549            $this->log(1, "Installing '$dest'$extra");
1550
1551            $copydir = dirname($copyto);
1552            // pretty much nothing happens if we are only registering the install
1553            if (empty($this->_options['register-only'])) {
1554                if (!file_exists($copydir) || !is_dir($copydir)) {
1555                    if (!$this->mkDirHier($copydir)) {
1556                        return $this->raiseError("failed to mkdir $copydir",
1557                            PEAR_INSTALLER_FAILED);
1558                    }
1559
1560                    $this->log(3, "+ mkdir $copydir");
1561                }
1562
1563                if (!@copy($ext['file'], $copyto)) {
1564                    return $this->raiseError(
1565                        "failed to write $copyto (" . error_get_last()["message"] . ")",
1566                        PEAR_INSTALLER_FAILED);
1567                }
1568
1569                $this->log(3, "+ cp $ext[file] $copyto");
1570                $this->addFileOperation('rename', array($ext['file'], $copyto));
1571                if (!OS_WINDOWS) {
1572                    $mode = 0666 & ~(int)octdec($this->config->get('umask'));
1573                    $this->addFileOperation('chmod', array($mode, $copyto));
1574                    if (!@chmod($copyto, $mode)) {
1575                        $this->log(0, "failed to change mode of $copyto (" .
1576                                      error_get_last()["message"] . ")");
1577                    }
1578                }
1579            }
1580
1581
1582            $data = array(
1583                'role'         => $role,
1584                'name'         => $bn,
1585                'installed_as' => $dest,
1586                'php_api'      => $ext['php_api'],
1587                'zend_mod_api' => $ext['zend_mod_api'],
1588                'zend_ext_api' => $ext['zend_ext_api'],
1589            );
1590
1591            if ($filelist->getPackageXmlVersion() == '1.0') {
1592                $filelist->installedFile($bn, $data);
1593            } else {
1594                $filelist->installedFile($bn, array('attribs' => $data));
1595            }
1596        }
1597    }
1598
1599    // }}}
1600    function &getUninstallPackages()
1601    {
1602        return $this->_downloadedPackages;
1603    }
1604    // {{{ uninstall()
1605
1606    /**
1607     * Uninstall a package
1608     *
1609     * This method removes all files installed by the application, and then
1610     * removes any empty directories.
1611     * @param string package name
1612     * @param array Command-line options.  Possibilities include:
1613     *
1614     *              - installroot: base installation dir, if not the default
1615     *              - register-only : update registry but don't remove files
1616     *              - nodeps: do not process dependencies of other packages to ensure
1617     *                        uninstallation does not break things
1618     */
1619    function uninstall($package, $options = array())
1620    {
1621        $installRoot = isset($options['installroot']) ? $options['installroot'] : '';
1622        $this->config->setInstallRoot($installRoot);
1623
1624        $this->installroot = '';
1625        $this->_registry = &$this->config->getRegistry();
1626        if (is_object($package)) {
1627            $channel = $package->getChannel();
1628            $pkg     = $package;
1629            $package = $pkg->getPackage();
1630        } else {
1631            $pkg = false;
1632            $info = $this->_registry->parsePackageName($package,
1633                $this->config->get('default_channel'));
1634            $channel = $info['channel'];
1635            $package = $info['package'];
1636        }
1637
1638        $savechannel = $this->config->get('default_channel');
1639        $this->configSet('default_channel', $channel);
1640        if (!is_object($pkg)) {
1641            $pkg = $this->_registry->getPackage($package, $channel);
1642        }
1643
1644        if (!$pkg) {
1645            $this->configSet('default_channel', $savechannel);
1646            return $this->raiseError($this->_registry->parsedPackageNameToString(
1647                array(
1648                    'channel' => $channel,
1649                    'package' => $package
1650                ), true) . ' not installed');
1651        }
1652
1653        if ($pkg->getInstalledBinary()) {
1654            // this is just an alias for a binary package
1655            return $this->_registry->deletePackage($package, $channel);
1656        }
1657
1658        $filelist = $pkg->getFilelist();
1659        PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
1660        if (!class_exists('PEAR_Dependency2')) {
1661            require_once 'PEAR/Dependency2.php';
1662        }
1663
1664        $depchecker = new PEAR_Dependency2($this->config, $options,
1665            array('channel' => $channel, 'package' => $package),
1666            PEAR_VALIDATE_UNINSTALLING);
1667        $e = $depchecker->validatePackageUninstall($this);
1668        PEAR::staticPopErrorHandling();
1669        if (PEAR::isError($e)) {
1670            if (!isset($options['ignore-errors'])) {
1671                return $this->raiseError($e);
1672            }
1673
1674            if (!isset($options['soft'])) {
1675                $this->log(0, 'WARNING: ' . $e->getMessage());
1676            }
1677        } elseif (is_array($e)) {
1678            if (!isset($options['soft'])) {
1679                $this->log(0, $e[0]);
1680            }
1681        }
1682
1683        $this->pkginfo = &$pkg;
1684        // pretty much nothing happens if we are only registering the uninstall
1685        if (empty($options['register-only'])) {
1686            // {{{ Delete the files
1687            $this->startFileTransaction();
1688            PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
1689            if (PEAR::isError($err = $this->_deletePackageFiles($package, $channel))) {
1690                PEAR::popErrorHandling();
1691                $this->rollbackFileTransaction();
1692                $this->configSet('default_channel', $savechannel);
1693                if (!isset($options['ignore-errors'])) {
1694                    return $this->raiseError($err);
1695                }
1696
1697                if (!isset($options['soft'])) {
1698                    $this->log(0, 'WARNING: ' . $err->getMessage());
1699                }
1700            } else {
1701                PEAR::popErrorHandling();
1702            }
1703
1704            if (!$this->commitFileTransaction()) {
1705                $this->rollbackFileTransaction();
1706                if (!isset($options['ignore-errors'])) {
1707                    return $this->raiseError("uninstall failed");
1708                }
1709
1710                if (!isset($options['soft'])) {
1711                    $this->log(0, 'WARNING: uninstall failed');
1712                }
1713            } else {
1714                $this->startFileTransaction();
1715                $dirtree = $pkg->getDirTree();
1716                if ($dirtree === false) {
1717                    $this->configSet('default_channel', $savechannel);
1718                    return $this->_registry->deletePackage($package, $channel);
1719                }
1720
1721                // attempt to delete empty directories
1722                uksort($dirtree, array($this, '_sortDirs'));
1723                foreach($dirtree as $dir => $notused) {
1724                    $this->addFileOperation('rmdir', array($dir));
1725                }
1726
1727                if (!$this->commitFileTransaction()) {
1728                    $this->rollbackFileTransaction();
1729                    if (!isset($options['ignore-errors'])) {
1730                        return $this->raiseError("uninstall failed");
1731                    }
1732
1733                    if (!isset($options['soft'])) {
1734                        $this->log(0, 'WARNING: uninstall failed');
1735                    }
1736                }
1737            }
1738            // }}}
1739        }
1740
1741        $this->configSet('default_channel', $savechannel);
1742        // Register that the package is no longer installed
1743        return $this->_registry->deletePackage($package, $channel);
1744    }
1745
1746    /**
1747     * Sort a list of arrays of array(downloaded packagefilename) by dependency.
1748     *
1749     * It also removes duplicate dependencies
1750     * @param array an array of PEAR_PackageFile_v[1/2] objects
1751     * @return array|PEAR_Error array of array(packagefilename, package.xml contents)
1752     */
1753    function sortPackagesForUninstall(&$packages)
1754    {
1755        $this->_dependencyDB = &PEAR_DependencyDB::singleton($this->config);
1756        if (PEAR::isError($this->_dependencyDB)) {
1757            return $this->_dependencyDB;
1758        }
1759        usort($packages, array(&$this, '_sortUninstall'));
1760    }
1761
1762    function _sortUninstall($a, $b)
1763    {
1764        if (!$a->getDeps() && !$b->getDeps()) {
1765            return 0; // neither package has dependencies, order is insignificant
1766        }
1767        if ($a->getDeps() && !$b->getDeps()) {
1768            return -1; // $a must be installed after $b because $a has dependencies
1769        }
1770        if (!$a->getDeps() && $b->getDeps()) {
1771            return 1; // $b must be installed after $a because $b has dependencies
1772        }
1773        // both packages have dependencies
1774        if ($this->_dependencyDB->dependsOn($a, $b)) {
1775            return -1;
1776        }
1777        if ($this->_dependencyDB->dependsOn($b, $a)) {
1778            return 1;
1779        }
1780        return 0;
1781    }
1782
1783    // }}}
1784    // {{{ _sortDirs()
1785    function _sortDirs($a, $b)
1786    {
1787        if (strnatcmp($a, $b) == -1) return 1;
1788        if (strnatcmp($a, $b) == 1) return -1;
1789        return 0;
1790    }
1791
1792    // }}}
1793
1794    // {{{ _buildCallback()
1795
1796    function _buildCallback($what, $data)
1797    {
1798        if (($what == 'cmdoutput' && $this->debug > 1) ||
1799            ($what == 'output' && $this->debug > 0)) {
1800            $this->ui->outputData(rtrim($data), 'build');
1801        }
1802    }
1803
1804    // }}}
1805}
1806