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