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