Initial repo created
[timetracker.git] / WEB-INF / lib / pear / PEAR / Downloader.php
1 <?php
2 /**
3  * PEAR_Downloader, the PEAR Installer's download utility class
4  *
5  * PHP versions 4 and 5
6  *
7  * @category   pear
8  * @package    PEAR
9  * @author     Greg Beaver <cellog@php.net>
10  * @author     Stig Bakken <ssb@php.net>
11  * @author     Tomas V. V. Cox <cox@idecnet.com>
12  * @author     Martin Jansen <mj@php.net>
13  * @copyright  1997-2009 The Authors
14  * @license    http://opensource.org/licenses/bsd-license.php New BSD License
15  * @version    CVS: $Id: Downloader.php 313024 2011-07-06 19:51:24Z dufuz $
16  * @link       http://pear.php.net/package/PEAR
17  * @since      File available since Release 1.3.0
18  */
19
20 /**
21  * Needed for constants, extending
22  */
23 require_once 'PEAR/Common.php';
24
25 define('PEAR_INSTALLER_OK',       1);
26 define('PEAR_INSTALLER_FAILED',   0);
27 define('PEAR_INSTALLER_SKIPPED', -1);
28 define('PEAR_INSTALLER_ERROR_NO_PREF_STATE', 2);
29
30 /**
31  * Administration class used to download anything from the internet (PEAR Packages,
32  * static URLs, xml files)
33  *
34  * @category   pear
35  * @package    PEAR
36  * @author     Greg Beaver <cellog@php.net>
37  * @author     Stig Bakken <ssb@php.net>
38  * @author     Tomas V. V. Cox <cox@idecnet.com>
39  * @author     Martin Jansen <mj@php.net>
40  * @copyright  1997-2009 The Authors
41  * @license    http://opensource.org/licenses/bsd-license.php New BSD License
42  * @version    Release: 1.9.4
43  * @link       http://pear.php.net/package/PEAR
44  * @since      Class available since Release 1.3.0
45  */
46 class PEAR_Downloader extends PEAR_Common
47 {
48     /**
49      * @var PEAR_Registry
50      * @access private
51      */
52     var $_registry;
53
54     /**
55      * Preferred Installation State (snapshot, devel, alpha, beta, stable)
56      * @var string|null
57      * @access private
58      */
59     var $_preferredState;
60
61     /**
62      * Options from command-line passed to Install.
63      *
64      * Recognized options:<br />
65      *  - onlyreqdeps   : install all required dependencies as well
66      *  - alldeps       : install all dependencies, including optional
67      *  - installroot   : base relative path to install files in
68      *  - force         : force a download even if warnings would prevent it
69      *  - nocompress    : download uncompressed tarballs
70      * @see PEAR_Command_Install
71      * @access private
72      * @var array
73      */
74     var $_options;
75
76     /**
77      * Downloaded Packages after a call to download().
78      *
79      * Format of each entry:
80      *
81      * <code>
82      * array('pkg' => 'package_name', 'file' => '/path/to/local/file',
83      *    'info' => array() // parsed package.xml
84      * );
85      * </code>
86      * @access private
87      * @var array
88      */
89     var $_downloadedPackages = array();
90
91     /**
92      * Packages slated for download.
93      *
94      * This is used to prevent downloading a package more than once should it be a dependency
95      * for two packages to be installed.
96      * Format of each entry:
97      *
98      * <pre>
99      * array('package_name1' => parsed package.xml, 'package_name2' => parsed package.xml,
100      * );
101      * </pre>
102      * @access private
103      * @var array
104      */
105     var $_toDownload = array();
106
107     /**
108      * Array of every package installed, with names lower-cased.
109      *
110      * Format:
111      * <code>
112      * array('package1' => 0, 'package2' => 1, );
113      * </code>
114      * @var array
115      */
116     var $_installed = array();
117
118     /**
119      * @var array
120      * @access private
121      */
122     var $_errorStack = array();
123
124     /**
125      * @var boolean
126      * @access private
127      */
128     var $_internalDownload = false;
129
130     /**
131      * Temporary variable used in sorting packages by dependency in {@link sortPkgDeps()}
132      * @var array
133      * @access private
134      */
135     var $_packageSortTree;
136
137     /**
138      * Temporary directory, or configuration value where downloads will occur
139      * @var string
140      */
141     var $_downloadDir;
142
143     /**
144      * @param PEAR_Frontend_*
145      * @param array
146      * @param PEAR_Config
147      */
148     function PEAR_Downloader(&$ui, $options, &$config)
149     {
150         parent::PEAR_Common();
151         $this->_options = $options;
152         $this->config = &$config;
153         $this->_preferredState = $this->config->get('preferred_state');
154         $this->ui = &$ui;
155         if (!$this->_preferredState) {
156             // don't inadvertantly use a non-set preferred_state
157             $this->_preferredState = null;
158         }
159
160         if (isset($this->_options['installroot'])) {
161             $this->config->setInstallRoot($this->_options['installroot']);
162         }
163         $this->_registry = &$config->getRegistry();
164
165         if (isset($this->_options['alldeps']) || isset($this->_options['onlyreqdeps'])) {
166             $this->_installed = $this->_registry->listAllPackages();
167             foreach ($this->_installed as $key => $unused) {
168                 if (!count($unused)) {
169                     continue;
170                 }
171                 $strtolower = create_function('$a','return strtolower($a);');
172                 array_walk($this->_installed[$key], $strtolower);
173             }
174         }
175     }
176
177     /**
178      * Attempt to discover a channel's remote capabilities from
179      * its server name
180      * @param string
181      * @return boolean
182      */
183     function discover($channel)
184     {
185         $this->log(1, 'Attempting to discover channel "' . $channel . '"...');
186         PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
187         $callback = $this->ui ? array(&$this, '_downloadCallback') : null;
188         if (!class_exists('System')) {
189             require_once 'System.php';
190         }
191
192         $tmpdir = $this->config->get('temp_dir');
193         $tmp = System::mktemp('-d -t "' . $tmpdir . '"');
194         $a   = $this->downloadHttp('http://' . $channel . '/channel.xml', $this->ui, $tmp, $callback, false);
195         PEAR::popErrorHandling();
196         if (PEAR::isError($a)) {
197             // Attempt to fallback to https automatically.
198             PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
199             $this->log(1, 'Attempting fallback to https instead of http on channel "' . $channel . '"...');
200             $a = $this->downloadHttp('https://' . $channel . '/channel.xml', $this->ui, $tmp, $callback, false);
201             PEAR::popErrorHandling();
202             if (PEAR::isError($a)) {
203                 return false;
204             }
205         }
206
207         list($a, $lastmodified) = $a;
208         if (!class_exists('PEAR_ChannelFile')) {
209             require_once 'PEAR/ChannelFile.php';
210         }
211
212         $b = new PEAR_ChannelFile;
213         if ($b->fromXmlFile($a)) {
214             unlink($a);
215             if ($this->config->get('auto_discover')) {
216                 $this->_registry->addChannel($b, $lastmodified);
217                 $alias = $b->getName();
218                 if ($b->getName() == $this->_registry->channelName($b->getAlias())) {
219                     $alias = $b->getAlias();
220                 }
221
222                 $this->log(1, 'Auto-discovered channel "' . $channel .
223                     '", alias "' . $alias . '", adding to registry');
224             }
225
226             return true;
227         }
228
229         unlink($a);
230         return false;
231     }
232
233     /**
234      * For simpler unit-testing
235      * @param PEAR_Downloader
236      * @return PEAR_Downloader_Package
237      */
238     function &newDownloaderPackage(&$t)
239     {
240         if (!class_exists('PEAR_Downloader_Package')) {
241             require_once 'PEAR/Downloader/Package.php';
242         }
243         $a = &new PEAR_Downloader_Package($t);
244         return $a;
245     }
246
247     /**
248      * For simpler unit-testing
249      * @param PEAR_Config
250      * @param array
251      * @param array
252      * @param int
253      */
254     function &getDependency2Object(&$c, $i, $p, $s)
255     {
256         if (!class_exists('PEAR_Dependency2')) {
257             require_once 'PEAR/Dependency2.php';
258         }
259         $z = &new PEAR_Dependency2($c, $i, $p, $s);
260         return $z;
261     }
262
263     function &download($params)
264     {
265         if (!count($params)) {
266             $a = array();
267             return $a;
268         }
269
270         if (!isset($this->_registry)) {
271             $this->_registry = &$this->config->getRegistry();
272         }
273
274         $channelschecked = array();
275         // convert all parameters into PEAR_Downloader_Package objects
276         foreach ($params as $i => $param) {
277             $params[$i] = &$this->newDownloaderPackage($this);
278             PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
279             $err = $params[$i]->initialize($param);
280             PEAR::staticPopErrorHandling();
281             if (!$err) {
282                 // skip parameters that were missed by preferred_state
283                 continue;
284             }
285
286             if (PEAR::isError($err)) {
287                 if (!isset($this->_options['soft']) && $err->getMessage() !== '') {
288                     $this->log(0, $err->getMessage());
289                 }
290
291                 $params[$i] = false;
292                 if (is_object($param)) {
293                     $param = $param->getChannel() . '/' . $param->getPackage();
294                 }
295
296                 if (!isset($this->_options['soft'])) {
297                     $this->log(2, 'Package "' . $param . '" is not valid');
298                 }
299
300                 // Message logged above in a specific verbose mode, passing null to not show up on CLI
301                 $this->pushError(null, PEAR_INSTALLER_SKIPPED);
302             } else {
303                 do {
304                     if ($params[$i] && $params[$i]->getType() == 'local') {
305                         // bug #7090 skip channel.xml check for local packages
306                         break;
307                     }
308
309                     if ($params[$i] && !isset($channelschecked[$params[$i]->getChannel()]) &&
310                           !isset($this->_options['offline'])
311                     ) {
312                         $channelschecked[$params[$i]->getChannel()] = true;
313                         PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
314                         if (!class_exists('System')) {
315                             require_once 'System.php';
316                         }
317
318                         $curchannel = &$this->_registry->getChannel($params[$i]->getChannel());
319                         if (PEAR::isError($curchannel)) {
320                             PEAR::staticPopErrorHandling();
321                             return $this->raiseError($curchannel);
322                         }
323
324                         if (PEAR::isError($dir = $this->getDownloadDir())) {
325                             PEAR::staticPopErrorHandling();
326                             break;
327                         }
328
329                         $mirror = $this->config->get('preferred_mirror', null, $params[$i]->getChannel());
330                         $url    = 'http://' . $mirror . '/channel.xml';
331                         $a = $this->downloadHttp($url, $this->ui, $dir, null, $curchannel->lastModified());
332
333                         PEAR::staticPopErrorHandling();
334                         if (PEAR::isError($a) || !$a) {
335                             // Attempt fallback to https automatically
336                             PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
337                             $a = $this->downloadHttp('https://' . $mirror .
338                                 '/channel.xml', $this->ui, $dir, null, $curchannel->lastModified());
339
340                             PEAR::staticPopErrorHandling();
341                             if (PEAR::isError($a) || !$a) {
342                                 break;
343                             }
344                         }
345                         $this->log(0, 'WARNING: channel "' . $params[$i]->getChannel() . '" has ' .
346                             'updated its protocols, use "' . PEAR_RUNTYPE . ' channel-update ' . $params[$i]->getChannel() .
347                             '" to update');
348                     }
349                 } while (false);
350
351                 if ($params[$i] && !isset($this->_options['downloadonly'])) {
352                     if (isset($this->_options['packagingroot'])) {
353                         $checkdir = $this->_prependPath(
354                             $this->config->get('php_dir', null, $params[$i]->getChannel()),
355                             $this->_options['packagingroot']);
356                     } else {
357                         $checkdir = $this->config->get('php_dir',
358                             null, $params[$i]->getChannel());
359                     }
360
361                     while ($checkdir && $checkdir != '/' && !file_exists($checkdir)) {
362                         $checkdir = dirname($checkdir);
363                     }
364
365                     if ($checkdir == '.') {
366                         $checkdir = '/';
367                     }
368
369                     if (!is_writeable($checkdir)) {
370                         return PEAR::raiseError('Cannot install, php_dir for channel "' .
371                             $params[$i]->getChannel() . '" is not writeable by the current user');
372                     }
373                 }
374             }
375         }
376
377         unset($channelschecked);
378         PEAR_Downloader_Package::removeDuplicates($params);
379         if (!count($params)) {
380             $a = array();
381             return $a;
382         }
383
384         if (!isset($this->_options['nodeps']) && !isset($this->_options['offline'])) {
385             $reverify = true;
386             while ($reverify) {
387                 $reverify = false;
388                 foreach ($params as $i => $param) {
389                     //PHP Bug 40768 / PEAR Bug #10944
390                     //Nested foreaches fail in PHP 5.2.1
391                     key($params);
392                     $ret = $params[$i]->detectDependencies($params);
393                     if (PEAR::isError($ret)) {
394                         $reverify = true;
395                         $params[$i] = false;
396                         PEAR_Downloader_Package::removeDuplicates($params);
397                         if (!isset($this->_options['soft'])) {
398                             $this->log(0, $ret->getMessage());
399                         }
400                         continue 2;
401                     }
402                 }
403             }
404         }
405
406         if (isset($this->_options['offline'])) {
407             $this->log(3, 'Skipping dependency download check, --offline specified');
408         }
409
410         if (!count($params)) {
411             $a = array();
412             return $a;
413         }
414
415         while (PEAR_Downloader_Package::mergeDependencies($params));
416         PEAR_Downloader_Package::removeDuplicates($params, true);
417         $errorparams = array();
418         if (PEAR_Downloader_Package::detectStupidDuplicates($params, $errorparams)) {
419             if (count($errorparams)) {
420                 foreach ($errorparams as $param) {
421                     $name = $this->_registry->parsedPackageNameToString($param->getParsedPackage());
422                     $this->pushError('Duplicate package ' . $name . ' found', PEAR_INSTALLER_FAILED);
423                 }
424                 $a = array();
425                 return $a;
426             }
427         }
428
429         PEAR_Downloader_Package::removeInstalled($params);
430         if (!count($params)) {
431             $this->pushError('No valid packages found', PEAR_INSTALLER_FAILED);
432             $a = array();
433             return $a;
434         }
435
436         PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
437         $err = $this->analyzeDependencies($params);
438         PEAR::popErrorHandling();
439         if (!count($params)) {
440             $this->pushError('No valid packages found', PEAR_INSTALLER_FAILED);
441             $a = array();
442             return $a;
443         }
444
445         $ret = array();
446         $newparams = array();
447         if (isset($this->_options['pretend'])) {
448             return $params;
449         }
450
451         $somefailed = false;
452         foreach ($params as $i => $package) {
453             PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
454             $pf = &$params[$i]->download();
455             PEAR::staticPopErrorHandling();
456             if (PEAR::isError($pf)) {
457                 if (!isset($this->_options['soft'])) {
458                     $this->log(1, $pf->getMessage());
459                     $this->log(0, 'Error: cannot download "' .
460                         $this->_registry->parsedPackageNameToString($package->getParsedPackage(),
461                             true) .
462                         '"');
463                 }
464                 $somefailed = true;
465                 continue;
466             }
467
468             $newparams[] = &$params[$i];
469             $ret[] = array(
470                 'file' => $pf->getArchiveFile(),
471                 'info' => &$pf,
472                 'pkg'  => $pf->getPackage()
473             );
474         }
475
476         if ($somefailed) {
477             // remove params that did not download successfully
478             PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
479             $err = $this->analyzeDependencies($newparams, true);
480             PEAR::popErrorHandling();
481             if (!count($newparams)) {
482                 $this->pushError('Download failed', PEAR_INSTALLER_FAILED);
483                 $a = array();
484                 return $a;
485             }
486         }
487
488         $this->_downloadedPackages = $ret;
489         return $newparams;
490     }
491
492     /**
493      * @param array all packages to be installed
494      */
495     function analyzeDependencies(&$params, $force = false)
496     {
497         if (isset($this->_options['downloadonly'])) {
498             return;
499         }
500
501         PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
502         $redo  = true;
503         $reset = $hasfailed = $failed = false;
504         while ($redo) {
505             $redo = false;
506             foreach ($params as $i => $param) {
507                 $deps = $param->getDeps();
508                 if (!$deps) {
509                     $depchecker = &$this->getDependency2Object($this->config, $this->getOptions(),
510                         $param->getParsedPackage(), PEAR_VALIDATE_DOWNLOADING);
511                     $send = $param->getPackageFile();
512
513                     $installcheck = $depchecker->validatePackage($send, $this, $params);
514                     if (PEAR::isError($installcheck)) {
515                         if (!isset($this->_options['soft'])) {
516                             $this->log(0, $installcheck->getMessage());
517                         }
518                         $hasfailed  = true;
519                         $params[$i] = false;
520                         $reset      = true;
521                         $redo       = true;
522                         $failed     = false;
523                         PEAR_Downloader_Package::removeDuplicates($params);
524                         continue 2;
525                     }
526                     continue;
527                 }
528
529                 if (!$reset && $param->alreadyValidated() && !$force) {
530                     continue;
531                 }
532
533                 if (count($deps)) {
534                     $depchecker = &$this->getDependency2Object($this->config, $this->getOptions(),
535                         $param->getParsedPackage(), PEAR_VALIDATE_DOWNLOADING);
536                     $send = $param->getPackageFile();
537                     if ($send === null) {
538                         $send = $param->getDownloadURL();
539                     }
540
541                     $installcheck = $depchecker->validatePackage($send, $this, $params);
542                     if (PEAR::isError($installcheck)) {
543                         if (!isset($this->_options['soft'])) {
544                             $this->log(0, $installcheck->getMessage());
545                         }
546                         $hasfailed  = true;
547                         $params[$i] = false;
548                         $reset      = true;
549                         $redo       = true;
550                         $failed     = false;
551                         PEAR_Downloader_Package::removeDuplicates($params);
552                         continue 2;
553                     }
554
555                     $failed = false;
556                     if (isset($deps['required']) && is_array($deps['required'])) {
557                         foreach ($deps['required'] as $type => $dep) {
558                             // note: Dependency2 will never return a PEAR_Error if ignore-errors
559                             // is specified, so soft is needed to turn off logging
560                             if (!isset($dep[0])) {
561                                 if (PEAR::isError($e = $depchecker->{"validate{$type}Dependency"}($dep,
562                                       true, $params))) {
563                                     $failed = true;
564                                     if (!isset($this->_options['soft'])) {
565                                         $this->log(0, $e->getMessage());
566                                     }
567                                 } elseif (is_array($e) && !$param->alreadyValidated()) {
568                                     if (!isset($this->_options['soft'])) {
569                                         $this->log(0, $e[0]);
570                                     }
571                                 }
572                             } else {
573                                 foreach ($dep as $d) {
574                                     if (PEAR::isError($e =
575                                           $depchecker->{"validate{$type}Dependency"}($d,
576                                           true, $params))) {
577                                         $failed = true;
578                                         if (!isset($this->_options['soft'])) {
579                                             $this->log(0, $e->getMessage());
580                                         }
581                                     } elseif (is_array($e) && !$param->alreadyValidated()) {
582                                         if (!isset($this->_options['soft'])) {
583                                             $this->log(0, $e[0]);
584                                         }
585                                     }
586                                 }
587                             }
588                         }
589
590                         if (isset($deps['optional']) && is_array($deps['optional'])) {
591                             foreach ($deps['optional'] as $type => $dep) {
592                                 if (!isset($dep[0])) {
593                                     if (PEAR::isError($e =
594                                           $depchecker->{"validate{$type}Dependency"}($dep,
595                                           false, $params))) {
596                                         $failed = true;
597                                         if (!isset($this->_options['soft'])) {
598                                             $this->log(0, $e->getMessage());
599                                         }
600                                     } elseif (is_array($e) && !$param->alreadyValidated()) {
601                                         if (!isset($this->_options['soft'])) {
602                                             $this->log(0, $e[0]);
603                                         }
604                                     }
605                                 } else {
606                                     foreach ($dep as $d) {
607                                         if (PEAR::isError($e =
608                                               $depchecker->{"validate{$type}Dependency"}($d,
609                                               false, $params))) {
610                                             $failed = true;
611                                             if (!isset($this->_options['soft'])) {
612                                                 $this->log(0, $e->getMessage());
613                                             }
614                                         } elseif (is_array($e) && !$param->alreadyValidated()) {
615                                             if (!isset($this->_options['soft'])) {
616                                                 $this->log(0, $e[0]);
617                                             }
618                                         }
619                                     }
620                                 }
621                             }
622                         }
623
624                         $groupname = $param->getGroup();
625                         if (isset($deps['group']) && $groupname) {
626                             if (!isset($deps['group'][0])) {
627                                 $deps['group'] = array($deps['group']);
628                             }
629
630                             $found = false;
631                             foreach ($deps['group'] as $group) {
632                                 if ($group['attribs']['name'] == $groupname) {
633                                     $found = true;
634                                     break;
635                                 }
636                             }
637
638                             if ($found) {
639                                 unset($group['attribs']);
640                                 foreach ($group as $type => $dep) {
641                                     if (!isset($dep[0])) {
642                                         if (PEAR::isError($e =
643                                               $depchecker->{"validate{$type}Dependency"}($dep,
644                                               false, $params))) {
645                                             $failed = true;
646                                             if (!isset($this->_options['soft'])) {
647                                                 $this->log(0, $e->getMessage());
648                                             }
649                                         } elseif (is_array($e) && !$param->alreadyValidated()) {
650                                             if (!isset($this->_options['soft'])) {
651                                                 $this->log(0, $e[0]);
652                                             }
653                                         }
654                                     } else {
655                                         foreach ($dep as $d) {
656                                             if (PEAR::isError($e =
657                                                   $depchecker->{"validate{$type}Dependency"}($d,
658                                                   false, $params))) {
659                                                 $failed = true;
660                                                 if (!isset($this->_options['soft'])) {
661                                                     $this->log(0, $e->getMessage());
662                                                 }
663                                             } elseif (is_array($e) && !$param->alreadyValidated()) {
664                                                 if (!isset($this->_options['soft'])) {
665                                                     $this->log(0, $e[0]);
666                                                 }
667                                             }
668                                         }
669                                     }
670                                 }
671                             }
672                         }
673                     } else {
674                         foreach ($deps as $dep) {
675                             if (PEAR::isError($e = $depchecker->validateDependency1($dep, $params))) {
676                                 $failed = true;
677                                 if (!isset($this->_options['soft'])) {
678                                     $this->log(0, $e->getMessage());
679                                 }
680                             } elseif (is_array($e) && !$param->alreadyValidated()) {
681                                 if (!isset($this->_options['soft'])) {
682                                     $this->log(0, $e[0]);
683                                 }
684                             }
685                         }
686                     }
687                     $params[$i]->setValidated();
688                 }
689
690                 if ($failed) {
691                     $hasfailed  = true;
692                     $params[$i] = false;
693                     $reset      = true;
694                     $redo       = true;
695                     $failed     = false;
696                     PEAR_Downloader_Package::removeDuplicates($params);
697                     continue 2;
698                 }
699             }
700         }
701
702         PEAR::staticPopErrorHandling();
703         if ($hasfailed && (isset($this->_options['ignore-errors']) ||
704               isset($this->_options['nodeps']))) {
705             // this is probably not needed, but just in case
706             if (!isset($this->_options['soft'])) {
707                 $this->log(0, 'WARNING: dependencies failed');
708             }
709         }
710     }
711
712     /**
713      * Retrieve the directory that downloads will happen in
714      * @access private
715      * @return string
716      */
717     function getDownloadDir()
718     {
719         if (isset($this->_downloadDir)) {
720             return $this->_downloadDir;
721         }
722
723         $downloaddir = $this->config->get('download_dir');
724         if (empty($downloaddir) || (is_dir($downloaddir) && !is_writable($downloaddir))) {
725             if  (is_dir($downloaddir) && !is_writable($downloaddir)) {
726                 $this->log(0, 'WARNING: configuration download directory "' . $downloaddir .
727                     '" is not writeable.  Change download_dir config variable to ' .
728                     'a writeable dir to avoid this warning');
729             }
730
731             if (!class_exists('System')) {
732                 require_once 'System.php';
733             }
734
735             if (PEAR::isError($downloaddir = System::mktemp('-d'))) {
736                 return $downloaddir;
737             }
738             $this->log(3, '+ tmp dir created at ' . $downloaddir);
739         }
740
741         if (!is_writable($downloaddir)) {
742             if (PEAR::isError(System::mkdir(array('-p', $downloaddir))) ||
743                   !is_writable($downloaddir)) {
744                 return PEAR::raiseError('download directory "' . $downloaddir .
745                     '" is not writeable.  Change download_dir config variable to ' .
746                     'a writeable dir');
747             }
748         }
749
750         return $this->_downloadDir = $downloaddir;
751     }
752
753     function setDownloadDir($dir)
754     {
755         if (!@is_writable($dir)) {
756             if (PEAR::isError(System::mkdir(array('-p', $dir)))) {
757                 return PEAR::raiseError('download directory "' . $dir .
758                     '" is not writeable.  Change download_dir config variable to ' .
759                     'a writeable dir');
760             }
761         }
762         $this->_downloadDir = $dir;
763     }
764
765     function configSet($key, $value, $layer = 'user', $channel = false)
766     {
767         $this->config->set($key, $value, $layer, $channel);
768         $this->_preferredState = $this->config->get('preferred_state', null, $channel);
769         if (!$this->_preferredState) {
770             // don't inadvertantly use a non-set preferred_state
771             $this->_preferredState = null;
772         }
773     }
774
775     function setOptions($options)
776     {
777         $this->_options = $options;
778     }
779
780     function getOptions()
781     {
782         return $this->_options;
783     }
784
785
786     /**
787      * @param array output of {@link parsePackageName()}
788      * @access private
789      */
790     function _getPackageDownloadUrl($parr)
791     {
792         $curchannel = $this->config->get('default_channel');
793         $this->configSet('default_channel', $parr['channel']);
794         // getDownloadURL returns an array.  On error, it only contains information
795         // on the latest release as array(version, info).  On success it contains
796         // array(version, info, download url string)
797         $state = isset($parr['state']) ? $parr['state'] : $this->config->get('preferred_state');
798         if (!$this->_registry->channelExists($parr['channel'])) {
799             do {
800                 if ($this->config->get('auto_discover') && $this->discover($parr['channel'])) {
801                     break;
802                 }
803
804                 $this->configSet('default_channel', $curchannel);
805                 return PEAR::raiseError('Unknown remote channel: ' . $parr['channel']);
806             } while (false);
807         }
808
809         $chan = &$this->_registry->getChannel($parr['channel']);
810         if (PEAR::isError($chan)) {
811             return $chan;
812         }
813
814         PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
815         $version   = $this->_registry->packageInfo($parr['package'], 'version', $parr['channel']);
816         $stability = $this->_registry->packageInfo($parr['package'], 'stability', $parr['channel']);
817         // package is installed - use the installed release stability level
818         if (!isset($parr['state']) && $stability !== null) {
819             $state = $stability['release'];
820         }
821         PEAR::staticPopErrorHandling();
822         $base2 = false;
823
824         $preferred_mirror = $this->config->get('preferred_mirror');
825         if (!$chan->supportsREST($preferred_mirror) ||
826               (
827                !($base2 = $chan->getBaseURL('REST1.3', $preferred_mirror))
828                &&
829                !($base = $chan->getBaseURL('REST1.0', $preferred_mirror))
830               )
831         ) {
832             return $this->raiseError($parr['channel'] . ' is using a unsupported protocol - This should never happen.');
833         }
834
835         if ($base2) {
836             $rest = &$this->config->getREST('1.3', $this->_options);
837             $base = $base2;
838         } else {
839             $rest = &$this->config->getREST('1.0', $this->_options);
840         }
841
842         $downloadVersion = false;
843         if (!isset($parr['version']) && !isset($parr['state']) && $version
844               && !PEAR::isError($version)
845               && !isset($this->_options['downloadonly'])
846         ) {
847             $downloadVersion = $version;
848         }
849
850         $url = $rest->getDownloadURL($base, $parr, $state, $downloadVersion, $chan->getName());
851         if (PEAR::isError($url)) {
852             $this->configSet('default_channel', $curchannel);
853             return $url;
854         }
855
856         if ($parr['channel'] != $curchannel) {
857             $this->configSet('default_channel', $curchannel);
858         }
859
860         if (!is_array($url)) {
861             return $url;
862         }
863
864         $url['raw'] = false; // no checking is necessary for REST
865         if (!is_array($url['info'])) {
866             return PEAR::raiseError('Invalid remote dependencies retrieved from REST - ' .
867                 'this should never happen');
868         }
869
870         if (!isset($this->_options['force']) &&
871               !isset($this->_options['downloadonly']) &&
872               $version &&
873               !PEAR::isError($version) &&
874               !isset($parr['group'])
875         ) {
876             if (version_compare($version, $url['version'], '=')) {
877                 return PEAR::raiseError($this->_registry->parsedPackageNameToString(
878                     $parr, true) . ' is already installed and is the same as the ' .
879                     'released version ' . $url['version'], -976);
880             }
881
882             if (version_compare($version, $url['version'], '>')) {
883                 return PEAR::raiseError($this->_registry->parsedPackageNameToString(
884                     $parr, true) . ' is already installed and is newer than detected ' .
885                     'released version ' . $url['version'], -976);
886             }
887         }
888
889         if (isset($url['info']['required']) || $url['compatible']) {
890             require_once 'PEAR/PackageFile/v2.php';
891             $pf = new PEAR_PackageFile_v2;
892             $pf->setRawChannel($parr['channel']);
893             if ($url['compatible']) {
894                 $pf->setRawCompatible($url['compatible']);
895             }
896         } else {
897             require_once 'PEAR/PackageFile/v1.php';
898             $pf = new PEAR_PackageFile_v1;
899         }
900
901         $pf->setRawPackage($url['package']);
902         $pf->setDeps($url['info']);
903         if ($url['compatible']) {
904             $pf->setCompatible($url['compatible']);
905         }
906
907         $pf->setRawState($url['stability']);
908         $url['info'] = &$pf;
909         if (!extension_loaded("zlib") || isset($this->_options['nocompress'])) {
910             $ext = '.tar';
911         } else {
912             $ext = '.tgz';
913         }
914
915         if (is_array($url) && isset($url['url'])) {
916             $url['url'] .= $ext;
917         }
918
919         return $url;
920     }
921
922     /**
923      * @param array dependency array
924      * @access private
925      */
926     function _getDepPackageDownloadUrl($dep, $parr)
927     {
928         $xsdversion = isset($dep['rel']) ? '1.0' : '2.0';
929         $curchannel = $this->config->get('default_channel');
930         if (isset($dep['uri'])) {
931             $xsdversion = '2.0';
932             $chan = &$this->_registry->getChannel('__uri');
933             if (PEAR::isError($chan)) {
934                 return $chan;
935             }
936
937             $version = $this->_registry->packageInfo($dep['name'], 'version', '__uri');
938             $this->configSet('default_channel', '__uri');
939         } else {
940             if (isset($dep['channel'])) {
941                 $remotechannel = $dep['channel'];
942             } else {
943                 $remotechannel = 'pear.php.net';
944             }
945
946             if (!$this->_registry->channelExists($remotechannel)) {
947                 do {
948                     if ($this->config->get('auto_discover')) {
949                         if ($this->discover($remotechannel)) {
950                             break;
951                         }
952                     }
953                     return PEAR::raiseError('Unknown remote channel: ' . $remotechannel);
954                 } while (false);
955             }
956
957             $chan = &$this->_registry->getChannel($remotechannel);
958             if (PEAR::isError($chan)) {
959                 return $chan;
960             }
961
962             $version = $this->_registry->packageInfo($dep['name'], 'version', $remotechannel);
963             $this->configSet('default_channel', $remotechannel);
964         }
965
966         $state = isset($parr['state']) ? $parr['state'] : $this->config->get('preferred_state');
967         if (isset($parr['state']) && isset($parr['version'])) {
968             unset($parr['state']);
969         }
970
971         if (isset($dep['uri'])) {
972             $info = &$this->newDownloaderPackage($this);
973             PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
974             $err = $info->initialize($dep);
975             PEAR::staticPopErrorHandling();
976             if (!$err) {
977                 // skip parameters that were missed by preferred_state
978                 return PEAR::raiseError('Cannot initialize dependency');
979             }
980
981             if (PEAR::isError($err)) {
982                 if (!isset($this->_options['soft'])) {
983                     $this->log(0, $err->getMessage());
984                 }
985
986                 if (is_object($info)) {
987                     $param = $info->getChannel() . '/' . $info->getPackage();
988                 }
989                 return PEAR::raiseError('Package "' . $param . '" is not valid');
990             }
991             return $info;
992         } elseif ($chan->supportsREST($this->config->get('preferred_mirror'))
993               &&
994                 (
995                   ($base2 = $chan->getBaseURL('REST1.3', $this->config->get('preferred_mirror')))
996                     ||
997                   ($base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror')))
998                 )
999         ) {
1000             if ($base2) {
1001                 $base = $base2;
1002                 $rest = &$this->config->getREST('1.3', $this->_options);
1003             } else {
1004                 $rest = &$this->config->getREST('1.0', $this->_options);
1005             }
1006
1007             $url = $rest->getDepDownloadURL($base, $xsdversion, $dep, $parr,
1008                     $state, $version, $chan->getName());
1009             if (PEAR::isError($url)) {
1010                 return $url;
1011             }
1012
1013             if ($parr['channel'] != $curchannel) {
1014                 $this->configSet('default_channel', $curchannel);
1015             }
1016
1017             if (!is_array($url)) {
1018                 return $url;
1019             }
1020
1021             $url['raw'] = false; // no checking is necessary for REST
1022             if (!is_array($url['info'])) {
1023                 return PEAR::raiseError('Invalid remote dependencies retrieved from REST - ' .
1024                     'this should never happen');
1025             }
1026
1027             if (isset($url['info']['required'])) {
1028                 if (!class_exists('PEAR_PackageFile_v2')) {
1029                     require_once 'PEAR/PackageFile/v2.php';
1030                 }
1031                 $pf = new PEAR_PackageFile_v2;
1032                 $pf->setRawChannel($remotechannel);
1033             } else {
1034                 if (!class_exists('PEAR_PackageFile_v1')) {
1035                     require_once 'PEAR/PackageFile/v1.php';
1036                 }
1037                 $pf = new PEAR_PackageFile_v1;
1038
1039             }
1040             $pf->setRawPackage($url['package']);
1041             $pf->setDeps($url['info']);
1042             if ($url['compatible']) {
1043                 $pf->setCompatible($url['compatible']);
1044             }
1045
1046             $pf->setRawState($url['stability']);
1047             $url['info'] = &$pf;
1048             if (!extension_loaded("zlib") || isset($this->_options['nocompress'])) {
1049                 $ext = '.tar';
1050             } else {
1051                 $ext = '.tgz';
1052             }
1053
1054             if (is_array($url) && isset($url['url'])) {
1055                 $url['url'] .= $ext;
1056             }
1057
1058             return $url;
1059         }
1060
1061         return $this->raiseError($parr['channel'] . ' is using a unsupported protocol - This should never happen.');
1062     }
1063
1064     /**
1065      * @deprecated in favor of _getPackageDownloadUrl
1066      */
1067     function getPackageDownloadUrl($package, $version = null, $channel = false)
1068     {
1069         if ($version) {
1070             $package .= "-$version";
1071         }
1072         if ($this === null || $this->_registry === null) {
1073             $package = "http://pear.php.net/get/$package";
1074         } else {
1075             $chan = $this->_registry->getChannel($channel);
1076             if (PEAR::isError($chan)) {
1077                 return '';
1078             }
1079             $package = "http://" . $chan->getServer() . "/get/$package";
1080         }
1081         if (!extension_loaded("zlib")) {
1082             $package .= '?uncompress=yes';
1083         }
1084         return $package;
1085     }
1086
1087     /**
1088      * Retrieve a list of downloaded packages after a call to {@link download()}.
1089      *
1090      * Also resets the list of downloaded packages.
1091      * @return array
1092      */
1093     function getDownloadedPackages()
1094     {
1095         $ret = $this->_downloadedPackages;
1096         $this->_downloadedPackages = array();
1097         $this->_toDownload = array();
1098         return $ret;
1099     }
1100
1101     function _downloadCallback($msg, $params = null)
1102     {
1103         switch ($msg) {
1104             case 'saveas':
1105                 $this->log(1, "downloading $params ...");
1106                 break;
1107             case 'done':
1108                 $this->log(1, '...done: ' . number_format($params, 0, '', ',') . ' bytes');
1109                 break;
1110             case 'bytesread':
1111                 static $bytes;
1112                 if (empty($bytes)) {
1113                     $bytes = 0;
1114                 }
1115                 if (!($bytes % 10240)) {
1116                     $this->log(1, '.', false);
1117                 }
1118                 $bytes += $params;
1119                 break;
1120             case 'start':
1121                 if($params[1] == -1) {
1122                     $length = "Unknown size";
1123                 } else {
1124                     $length = number_format($params[1], 0, '', ',')." bytes";
1125                 }
1126                 $this->log(1, "Starting to download {$params[0]} ($length)");
1127                 break;
1128         }
1129         if (method_exists($this->ui, '_downloadCallback'))
1130             $this->ui->_downloadCallback($msg, $params);
1131     }
1132
1133     function _prependPath($path, $prepend)
1134     {
1135         if (strlen($prepend) > 0) {
1136             if (OS_WINDOWS && preg_match('/^[a-z]:/i', $path)) {
1137                 if (preg_match('/^[a-z]:/i', $prepend)) {
1138                     $prepend = substr($prepend, 2);
1139                 } elseif ($prepend{0} != '\\') {
1140                     $prepend = "\\$prepend";
1141                 }
1142                 $path = substr($path, 0, 2) . $prepend . substr($path, 2);
1143             } else {
1144                 $path = $prepend . $path;
1145             }
1146         }
1147         return $path;
1148     }
1149
1150     /**
1151      * @param string
1152      * @param integer
1153      */
1154     function pushError($errmsg, $code = -1)
1155     {
1156         array_push($this->_errorStack, array($errmsg, $code));
1157     }
1158
1159     function getErrorMsgs()
1160     {
1161         $msgs = array();
1162         $errs = $this->_errorStack;
1163         foreach ($errs as $err) {
1164             $msgs[] = $err[0];
1165         }
1166         $this->_errorStack = array();
1167         return $msgs;
1168     }
1169
1170     /**
1171      * for BC
1172      *
1173      * @deprecated
1174      */
1175     function sortPkgDeps(&$packages, $uninstall = false)
1176     {
1177         $uninstall ?
1178             $this->sortPackagesForUninstall($packages) :
1179             $this->sortPackagesForInstall($packages);
1180     }
1181
1182     /**
1183      * Sort a list of arrays of array(downloaded packagefilename) by dependency.
1184      *
1185      * This uses the topological sort method from graph theory, and the
1186      * Structures_Graph package to properly sort dependencies for installation.
1187      * @param array an array of downloaded PEAR_Downloader_Packages
1188      * @return array array of array(packagefilename, package.xml contents)
1189      */
1190     function sortPackagesForInstall(&$packages)
1191     {
1192         require_once 'Structures/Graph.php';
1193         require_once 'Structures/Graph/Node.php';
1194         require_once 'Structures/Graph/Manipulator/TopologicalSorter.php';
1195         $depgraph = new Structures_Graph(true);
1196         $nodes = array();
1197         $reg = &$this->config->getRegistry();
1198         foreach ($packages as $i => $package) {
1199             $pname = $reg->parsedPackageNameToString(
1200                 array(
1201                     'channel' => $package->getChannel(),
1202                     'package' => strtolower($package->getPackage()),
1203                 ));
1204             $nodes[$pname] = new Structures_Graph_Node;
1205             $nodes[$pname]->setData($packages[$i]);
1206             $depgraph->addNode($nodes[$pname]);
1207         }
1208
1209         $deplinks = array();
1210         foreach ($nodes as $package => $node) {
1211             $pf = &$node->getData();
1212             $pdeps = $pf->getDeps(true);
1213             if (!$pdeps) {
1214                 continue;
1215             }
1216
1217             if ($pf->getPackagexmlVersion() == '1.0') {
1218                 foreach ($pdeps as $dep) {
1219                     if ($dep['type'] != 'pkg' ||
1220                           (isset($dep['optional']) && $dep['optional'] == 'yes')) {
1221                         continue;
1222                     }
1223
1224                     $dname = $reg->parsedPackageNameToString(
1225                           array(
1226                               'channel' => 'pear.php.net',
1227                               'package' => strtolower($dep['name']),
1228                           ));
1229
1230                     if (isset($nodes[$dname])) {
1231                         if (!isset($deplinks[$dname])) {
1232                             $deplinks[$dname] = array();
1233                         }
1234
1235                         $deplinks[$dname][$package] = 1;
1236                         // dependency is in installed packages
1237                         continue;
1238                     }
1239
1240                     $dname = $reg->parsedPackageNameToString(
1241                           array(
1242                               'channel' => 'pecl.php.net',
1243                               'package' => strtolower($dep['name']),
1244                           ));
1245
1246                     if (isset($nodes[$dname])) {
1247                         if (!isset($deplinks[$dname])) {
1248                             $deplinks[$dname] = array();
1249                         }
1250
1251                         $deplinks[$dname][$package] = 1;
1252                         // dependency is in installed packages
1253                         continue;
1254                     }
1255                 }
1256             } else {
1257                 // the only ordering we care about is:
1258                 // 1) subpackages must be installed before packages that depend on them
1259                 // 2) required deps must be installed before packages that depend on them
1260                 if (isset($pdeps['required']['subpackage'])) {
1261                     $t = $pdeps['required']['subpackage'];
1262                     if (!isset($t[0])) {
1263                         $t = array($t);
1264                     }
1265
1266                     $this->_setupGraph($t, $reg, $deplinks, $nodes, $package);
1267                 }
1268
1269                 if (isset($pdeps['group'])) {
1270                     if (!isset($pdeps['group'][0])) {
1271                         $pdeps['group'] = array($pdeps['group']);
1272                     }
1273
1274                     foreach ($pdeps['group'] as $group) {
1275                         if (isset($group['subpackage'])) {
1276                             $t = $group['subpackage'];
1277                             if (!isset($t[0])) {
1278                                 $t = array($t);
1279                             }
1280
1281                             $this->_setupGraph($t, $reg, $deplinks, $nodes, $package);
1282                         }
1283                     }
1284                 }
1285
1286                 if (isset($pdeps['optional']['subpackage'])) {
1287                     $t = $pdeps['optional']['subpackage'];
1288                     if (!isset($t[0])) {
1289                         $t = array($t);
1290                     }
1291
1292                     $this->_setupGraph($t, $reg, $deplinks, $nodes, $package);
1293                 }
1294
1295                 if (isset($pdeps['required']['package'])) {
1296                     $t = $pdeps['required']['package'];
1297                     if (!isset($t[0])) {
1298                         $t = array($t);
1299                     }
1300
1301                     $this->_setupGraph($t, $reg, $deplinks, $nodes, $package);
1302                 }
1303
1304                 if (isset($pdeps['group'])) {
1305                     if (!isset($pdeps['group'][0])) {
1306                         $pdeps['group'] = array($pdeps['group']);
1307                     }
1308
1309                     foreach ($pdeps['group'] as $group) {
1310                         if (isset($group['package'])) {
1311                             $t = $group['package'];
1312                             if (!isset($t[0])) {
1313                                 $t = array($t);
1314                             }
1315
1316                             $this->_setupGraph($t, $reg, $deplinks, $nodes, $package);
1317                         }
1318                     }
1319                 }
1320             }
1321         }
1322
1323         $this->_detectDepCycle($deplinks);
1324         foreach ($deplinks as $dependent => $parents) {
1325             foreach ($parents as $parent => $unused) {
1326                 $nodes[$dependent]->connectTo($nodes[$parent]);
1327             }
1328         }
1329
1330         $installOrder = Structures_Graph_Manipulator_TopologicalSorter::sort($depgraph);
1331         $ret = array();
1332         for ($i = 0, $count = count($installOrder); $i < $count; $i++) {
1333             foreach ($installOrder[$i] as $index => $sortedpackage) {
1334                 $data = &$installOrder[$i][$index]->getData();
1335                 $ret[] = &$nodes[$reg->parsedPackageNameToString(
1336                           array(
1337                               'channel' => $data->getChannel(),
1338                               'package' => strtolower($data->getPackage()),
1339                           ))]->getData();
1340             }
1341         }
1342
1343         $packages = $ret;
1344         return;
1345     }
1346
1347     /**
1348      * Detect recursive links between dependencies and break the cycles
1349      *
1350      * @param array
1351      * @access private
1352      */
1353     function _detectDepCycle(&$deplinks)
1354     {
1355         do {
1356             $keepgoing = false;
1357             foreach ($deplinks as $dep => $parents) {
1358                 foreach ($parents as $parent => $unused) {
1359                     // reset the parent cycle detector
1360                     $this->_testCycle(null, null, null);
1361                     if ($this->_testCycle($dep, $deplinks, $parent)) {
1362                         $keepgoing = true;
1363                         unset($deplinks[$dep][$parent]);
1364                         if (count($deplinks[$dep]) == 0) {
1365                             unset($deplinks[$dep]);
1366                         }
1367
1368                         continue 3;
1369                     }
1370                 }
1371             }
1372         } while ($keepgoing);
1373     }
1374
1375     function _testCycle($test, $deplinks, $dep)
1376     {
1377         static $visited = array();
1378         if ($test === null) {
1379             $visited = array();
1380             return;
1381         }
1382
1383         // this happens when a parent has a dep cycle on another dependency
1384         // but the child is not part of the cycle
1385         if (isset($visited[$dep])) {
1386             return false;
1387         }
1388
1389         $visited[$dep] = 1;
1390         if ($test == $dep) {
1391             return true;
1392         }
1393
1394         if (isset($deplinks[$dep])) {
1395             if (in_array($test, array_keys($deplinks[$dep]), true)) {
1396                 return true;
1397             }
1398
1399             foreach ($deplinks[$dep] as $parent => $unused) {
1400                 if ($this->_testCycle($test, $deplinks, $parent)) {
1401                     return true;
1402                 }
1403             }
1404         }
1405
1406         return false;
1407     }
1408
1409     /**
1410      * Set up the dependency for installation parsing
1411      *
1412      * @param array $t dependency information
1413      * @param PEAR_Registry $reg
1414      * @param array $deplinks list of dependency links already established
1415      * @param array $nodes all existing package nodes
1416      * @param string $package parent package name
1417      * @access private
1418      */
1419     function _setupGraph($t, $reg, &$deplinks, &$nodes, $package)
1420     {
1421         foreach ($t as $dep) {
1422             $depchannel = !isset($dep['channel']) ? '__uri': $dep['channel'];
1423             $dname = $reg->parsedPackageNameToString(
1424                   array(
1425                       'channel' => $depchannel,
1426                       'package' => strtolower($dep['name']),
1427                   ));
1428
1429             if (isset($nodes[$dname])) {
1430                 if (!isset($deplinks[$dname])) {
1431                     $deplinks[$dname] = array();
1432                 }
1433                 $deplinks[$dname][$package] = 1;
1434             }
1435         }
1436     }
1437
1438     function _dependsOn($a, $b)
1439     {
1440         return $this->_checkDepTree(strtolower($a->getChannel()), strtolower($a->getPackage()), $b);
1441     }
1442
1443     function _checkDepTree($channel, $package, $b, $checked = array())
1444     {
1445         $checked[$channel][$package] = true;
1446         if (!isset($this->_depTree[$channel][$package])) {
1447             return false;
1448         }
1449
1450         if (isset($this->_depTree[$channel][$package][strtolower($b->getChannel())]
1451               [strtolower($b->getPackage())])) {
1452             return true;
1453         }
1454
1455         foreach ($this->_depTree[$channel][$package] as $ch => $packages) {
1456             foreach ($packages as $pa => $true) {
1457                 if ($this->_checkDepTree($ch, $pa, $b, $checked)) {
1458                     return true;
1459                 }
1460             }
1461         }
1462
1463         return false;
1464     }
1465
1466     function _sortInstall($a, $b)
1467     {
1468         if (!$a->getDeps() && !$b->getDeps()) {
1469             return 0; // neither package has dependencies, order is insignificant
1470         }
1471         if ($a->getDeps() && !$b->getDeps()) {
1472             return 1; // $a must be installed after $b because $a has dependencies
1473         }
1474         if (!$a->getDeps() && $b->getDeps()) {
1475             return -1; // $b must be installed after $a because $b has dependencies
1476         }
1477         // both packages have dependencies
1478         if ($this->_dependsOn($a, $b)) {
1479             return 1;
1480         }
1481         if ($this->_dependsOn($b, $a)) {
1482             return -1;
1483         }
1484         return 0;
1485     }
1486
1487     /**
1488      * Download a file through HTTP.  Considers suggested file name in
1489      * Content-disposition: header and can run a callback function for
1490      * different events.  The callback will be called with two
1491      * parameters: the callback type, and parameters.  The implemented
1492      * callback types are:
1493      *
1494      *  'setup'       called at the very beginning, parameter is a UI object
1495      *                that should be used for all output
1496      *  'message'     the parameter is a string with an informational message
1497      *  'saveas'      may be used to save with a different file name, the
1498      *                parameter is the filename that is about to be used.
1499      *                If a 'saveas' callback returns a non-empty string,
1500      *                that file name will be used as the filename instead.
1501      *                Note that $save_dir will not be affected by this, only
1502      *                the basename of the file.
1503      *  'start'       download is starting, parameter is number of bytes
1504      *                that are expected, or -1 if unknown
1505      *  'bytesread'   parameter is the number of bytes read so far
1506      *  'done'        download is complete, parameter is the total number
1507      *                of bytes read
1508      *  'connfailed'  if the TCP/SSL connection fails, this callback is called
1509      *                with array(host,port,errno,errmsg)
1510      *  'writefailed' if writing to disk fails, this callback is called
1511      *                with array(destfile,errmsg)
1512      *
1513      * If an HTTP proxy has been configured (http_proxy PEAR_Config
1514      * setting), the proxy will be used.
1515      *
1516      * @param string  $url       the URL to download
1517      * @param object  $ui        PEAR_Frontend_* instance
1518      * @param object  $config    PEAR_Config instance
1519      * @param string  $save_dir  directory to save file in
1520      * @param mixed   $callback  function/method to call for status
1521      *                           updates
1522      * @param false|string|array $lastmodified header values to check against for caching
1523      *                           use false to return the header values from this download
1524      * @param false|array $accept Accept headers to send
1525      * @param false|string $channel Channel to use for retrieving authentication
1526      * @return string|array  Returns the full path of the downloaded file or a PEAR
1527      *                       error on failure.  If the error is caused by
1528      *                       socket-related errors, the error object will
1529      *                       have the fsockopen error code available through
1530      *                       getCode().  If caching is requested, then return the header
1531      *                       values.
1532      *
1533      * @access public
1534      */
1535     function downloadHttp($url, &$ui, $save_dir = '.', $callback = null, $lastmodified = null,
1536                           $accept = false, $channel = false)
1537     {
1538         static $redirect = 0;
1539         // always reset , so we are clean case of error
1540         $wasredirect = $redirect;
1541         $redirect = 0;
1542         if ($callback) {
1543             call_user_func($callback, 'setup', array(&$ui));
1544         }
1545
1546         $info = parse_url($url);
1547         if (!isset($info['scheme']) || !in_array($info['scheme'], array('http', 'https'))) {
1548             return PEAR::raiseError('Cannot download non-http URL "' . $url . '"');
1549         }
1550
1551         if (!isset($info['host'])) {
1552             return PEAR::raiseError('Cannot download from non-URL "' . $url . '"');
1553         }
1554
1555         $host = isset($info['host']) ? $info['host'] : null;
1556         $port = isset($info['port']) ? $info['port'] : null;
1557         $path = isset($info['path']) ? $info['path'] : null;
1558
1559         if (isset($this)) {
1560             $config = &$this->config;
1561         } else {
1562             $config = &PEAR_Config::singleton();
1563         }
1564
1565         $proxy_host = $proxy_port = $proxy_user = $proxy_pass = '';
1566         if ($config->get('http_proxy') &&
1567               $proxy = parse_url($config->get('http_proxy'))) {
1568             $proxy_host = isset($proxy['host']) ? $proxy['host'] : null;
1569             if (isset($proxy['scheme']) && $proxy['scheme'] == 'https') {
1570                 $proxy_host = 'ssl://' . $proxy_host;
1571             }
1572             $proxy_port = isset($proxy['port']) ? $proxy['port'] : 8080;
1573             $proxy_user = isset($proxy['user']) ? urldecode($proxy['user']) : null;
1574             $proxy_pass = isset($proxy['pass']) ? urldecode($proxy['pass']) : null;
1575
1576             if ($callback) {
1577                 call_user_func($callback, 'message', "Using HTTP proxy $host:$port");
1578             }
1579         }
1580
1581         if (empty($port)) {
1582             $port = (isset($info['scheme']) && $info['scheme'] == 'https') ? 443 : 80;
1583         }
1584
1585         $scheme = (isset($info['scheme']) && $info['scheme'] == 'https') ? 'https' : 'http';
1586
1587         if ($proxy_host != '') {
1588             $fp = @fsockopen($proxy_host, $proxy_port, $errno, $errstr);
1589             if (!$fp) {
1590                 if ($callback) {
1591                     call_user_func($callback, 'connfailed', array($proxy_host, $proxy_port,
1592                                                                   $errno, $errstr));
1593                 }
1594                 return PEAR::raiseError("Connection to `$proxy_host:$proxy_port' failed: $errstr", $errno);
1595             }
1596
1597             if ($lastmodified === false || $lastmodified) {
1598                 $request  = "GET $url HTTP/1.1\r\n";
1599                 $request .= "Host: $host\r\n";
1600             } else {
1601                 $request  = "GET $url HTTP/1.0\r\n";
1602                 $request .= "Host: $host\r\n";
1603             }
1604         } else {
1605             $network_host = $host;
1606             if (isset($info['scheme']) && $info['scheme'] == 'https') {
1607                 $network_host = 'ssl://' . $host;
1608             }
1609
1610             $fp = @fsockopen($network_host, $port, $errno, $errstr);
1611             if (!$fp) {
1612                 if ($callback) {
1613                     call_user_func($callback, 'connfailed', array($host, $port,
1614                                                                   $errno, $errstr));
1615                 }
1616                 return PEAR::raiseError("Connection to `$host:$port' failed: $errstr", $errno);
1617             }
1618
1619             if ($lastmodified === false || $lastmodified) {
1620                 $request = "GET $path HTTP/1.1\r\n";
1621                 $request .= "Host: $host\r\n";
1622             } else {
1623                 $request = "GET $path HTTP/1.0\r\n";
1624                 $request .= "Host: $host\r\n";
1625             }
1626         }
1627
1628         $ifmodifiedsince = '';
1629         if (is_array($lastmodified)) {
1630             if (isset($lastmodified['Last-Modified'])) {
1631                 $ifmodifiedsince = 'If-Modified-Since: ' . $lastmodified['Last-Modified'] . "\r\n";
1632             }
1633
1634             if (isset($lastmodified['ETag'])) {
1635                 $ifmodifiedsince .= "If-None-Match: $lastmodified[ETag]\r\n";
1636             }
1637         } else {
1638             $ifmodifiedsince = ($lastmodified ? "If-Modified-Since: $lastmodified\r\n" : '');
1639         }
1640
1641         $request .= $ifmodifiedsince .
1642             "User-Agent: PEAR/1.9.4/PHP/" . PHP_VERSION . "\r\n";
1643
1644         if (isset($this)) { // only pass in authentication for non-static calls
1645             $username = $config->get('username', null, $channel);
1646             $password = $config->get('password', null, $channel);
1647             if ($username && $password) {
1648                 $tmp = base64_encode("$username:$password");
1649                 $request .= "Authorization: Basic $tmp\r\n";
1650             }
1651         }
1652
1653         if ($proxy_host != '' && $proxy_user != '') {
1654             $request .= 'Proxy-Authorization: Basic ' .
1655                 base64_encode($proxy_user . ':' . $proxy_pass) . "\r\n";
1656         }
1657
1658         if ($accept) {
1659             $request .= 'Accept: ' . implode(', ', $accept) . "\r\n";
1660         }
1661
1662         $request .= "Connection: close\r\n";
1663         $request .= "\r\n";
1664         fwrite($fp, $request);
1665         $headers = array();
1666         $reply = 0;
1667         while (trim($line = fgets($fp, 1024))) {
1668             if (preg_match('/^([^:]+):\s+(.*)\s*\\z/', $line, $matches)) {
1669                 $headers[strtolower($matches[1])] = trim($matches[2]);
1670             } elseif (preg_match('|^HTTP/1.[01] ([0-9]{3}) |', $line, $matches)) {
1671                 $reply = (int)$matches[1];
1672                 if ($reply == 304 && ($lastmodified || ($lastmodified === false))) {
1673                     return false;
1674                 }
1675
1676                 if (!in_array($reply, array(200, 301, 302, 303, 305, 307))) {
1677                     return PEAR::raiseError("File $scheme://$host:$port$path not valid (received: $line)");
1678                 }
1679             }
1680         }
1681
1682         if ($reply != 200) {
1683             if (!isset($headers['location'])) {
1684                 return PEAR::raiseError("File $scheme://$host:$port$path not valid (redirected but no location)");
1685             }
1686
1687             if ($wasredirect > 4) {
1688                 return PEAR::raiseError("File $scheme://$host:$port$path not valid (redirection looped more than 5 times)");
1689             }
1690
1691             $redirect = $wasredirect + 1;
1692             return $this->downloadHttp($headers['location'],
1693                     $ui, $save_dir, $callback, $lastmodified, $accept);
1694         }
1695
1696         if (isset($headers['content-disposition']) &&
1697             preg_match('/\sfilename=\"([^;]*\S)\"\s*(;|\\z)/', $headers['content-disposition'], $matches)) {
1698             $save_as = basename($matches[1]);
1699         } else {
1700             $save_as = basename($url);
1701         }
1702
1703         if ($callback) {
1704             $tmp = call_user_func($callback, 'saveas', $save_as);
1705             if ($tmp) {
1706                 $save_as = $tmp;
1707             }
1708         }
1709
1710         $dest_file = $save_dir . DIRECTORY_SEPARATOR . $save_as;
1711         if (is_link($dest_file)) {
1712             return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $dest_file . ' as it is symlinked to ' . readlink($dest_file) . ' - Possible symlink attack');
1713         }
1714
1715         if (!$wp = @fopen($dest_file, 'wb')) {
1716             fclose($fp);
1717             if ($callback) {
1718                 call_user_func($callback, 'writefailed', array($dest_file, $php_errormsg));
1719             }
1720             return PEAR::raiseError("could not open $dest_file for writing");
1721         }
1722
1723         $length = isset($headers['content-length']) ? $headers['content-length'] : -1;
1724
1725         $bytes = 0;
1726         if ($callback) {
1727             call_user_func($callback, 'start', array(basename($dest_file), $length));
1728         }
1729
1730         while ($data = fread($fp, 1024)) {
1731             $bytes += strlen($data);
1732             if ($callback) {
1733                 call_user_func($callback, 'bytesread', $bytes);
1734             }
1735             if (!@fwrite($wp, $data)) {
1736                 fclose($fp);
1737                 if ($callback) {
1738                     call_user_func($callback, 'writefailed', array($dest_file, $php_errormsg));
1739                 }
1740                 return PEAR::raiseError("$dest_file: write failed ($php_errormsg)");
1741             }
1742         }
1743
1744         fclose($fp);
1745         fclose($wp);
1746         if ($callback) {
1747             call_user_func($callback, 'done', $bytes);
1748         }
1749
1750         if ($lastmodified === false || $lastmodified) {
1751             if (isset($headers['etag'])) {
1752                 $lastmodified = array('ETag' => $headers['etag']);
1753             }
1754
1755             if (isset($headers['last-modified'])) {
1756                 if (is_array($lastmodified)) {
1757                     $lastmodified['Last-Modified'] = $headers['last-modified'];
1758                 } else {
1759                     $lastmodified = $headers['last-modified'];
1760                 }
1761             }
1762             return array($dest_file, $lastmodified, $headers);
1763         }
1764         return $dest_file;
1765     }
1766 }