server/inc/PEAR/Downloader.php
2010-07-16 11:36:03 +02:00

680 lines
24 KiB
PHP

<?php
//
// +----------------------------------------------------------------------+
// | PHP Version 5 |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2004 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 3.0 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available through the world-wide-web at the following url: |
// | http://www.php.net/license/3_0.txt. |
// | If you did not receive a copy of the PHP license and are unable to |
// | obtain it through the world-wide-web, please send a note to |
// | license@php.net so we can mail you a copy immediately. |
// +----------------------------------------------------------------------+
// | Authors: Stig Bakken <ssb@php.net> |
// | Tomas V.V.Cox <cox@idecnet.com> |
// | Martin Jansen <mj@php.net> |
// +----------------------------------------------------------------------+
//
// $Id: Downloader.php,v 1.17.2.1 2004/10/22 22:54:03 cellog Exp $
require_once 'PEAR/Common.php';
require_once 'PEAR/Registry.php';
require_once 'PEAR/Dependency.php';
require_once 'PEAR/Remote.php';
require_once 'System.php';
define('PEAR_INSTALLER_OK', 1);
define('PEAR_INSTALLER_FAILED', 0);
define('PEAR_INSTALLER_SKIPPED', -1);
define('PEAR_INSTALLER_ERROR_NO_PREF_STATE', 2);
/**
* Administration class used to download PEAR packages and maintain the
* installed package database.
*
* @since PEAR 1.4
* @author Greg Beaver <cellog@php.net>
*/
class PEAR_Downloader extends PEAR_Common
{
/**
* @var PEAR_Config
* @access private
*/
var $_config;
/**
* @var PEAR_Registry
* @access private
*/
var $_registry;
/**
* @var PEAR_Remote
* @access private
*/
var $_remote;
/**
* Preferred Installation State (snapshot, devel, alpha, beta, stable)
* @var string|null
* @access private
*/
var $_preferredState;
/**
* Options from command-line passed to Install.
*
* Recognized options:<br />
* - onlyreqdeps : install all required dependencies as well
* - alldeps : install all dependencies, including optional
* - installroot : base relative path to install files in
* - force : force a download even if warnings would prevent it
* @see PEAR_Command_Install
* @access private
* @var array
*/
var $_options;
/**
* Downloaded Packages after a call to download().
*
* Format of each entry:
*
* <code>
* array('pkg' => 'package_name', 'file' => '/path/to/local/file',
* 'info' => array() // parsed package.xml
* );
* </code>
* @access private
* @var array
*/
var $_downloadedPackages = array();
/**
* Packages slated for download.
*
* This is used to prevent downloading a package more than once should it be a dependency
* for two packages to be installed.
* Format of each entry:
*
* <pre>
* array('package_name1' => parsed package.xml, 'package_name2' => parsed package.xml,
* );
* </pre>
* @access private
* @var array
*/
var $_toDownload = array();
/**
* Array of every package installed, with names lower-cased.
*
* Format:
* <code>
* array('package1' => 0, 'package2' => 1, );
* </code>
* @var array
*/
var $_installed = array();
/**
* @var array
* @access private
*/
var $_errorStack = array();
// {{{ PEAR_Downloader()
function PEAR_Downloader(&$ui, $options, &$config)
{
$this->_options = $options;
$this->_config = &$config;
$this->_preferredState = $this->_config->get('preferred_state');
$this->ui = &$ui;
if (!$this->_preferredState) {
// don't inadvertantly use a non-set preferred_state
$this->_preferredState = null;
}
$php_dir = $this->_config->get('php_dir');
if (isset($this->_options['installroot'])) {
if (substr($this->_options['installroot'], -1) == DIRECTORY_SEPARATOR) {
$this->_options['installroot'] = substr($this->_options['installroot'], 0, -1);
}
$php_dir = $this->_prependPath($php_dir, $this->_options['installroot']);
}
$this->_registry = &new PEAR_Registry($php_dir);
$this->_remote = &new PEAR_Remote($config);
if (isset($this->_options['alldeps']) || isset($this->_options['onlyreqdeps'])) {
$this->_installed = $this->_registry->listPackages();
array_walk($this->_installed, create_function('&$v,$k','$v = strtolower($v);'));
$this->_installed = array_flip($this->_installed);
}
parent::PEAR_Common();
}
// }}}
// {{{ configSet()
function configSet($key, $value, $layer = 'user')
{
$this->_config->set($key, $value, $layer);
$this->_preferredState = $this->_config->get('preferred_state');
if (!$this->_preferredState) {
// don't inadvertantly use a non-set preferred_state
$this->_preferredState = null;
}
}
// }}}
// {{{ setOptions()
function setOptions($options)
{
$this->_options = $options;
}
// }}}
// {{{ _downloadFile()
/**
* @param string filename to download
* @param string version/state
* @param string original value passed to command-line
* @param string|null preferred state (snapshot/devel/alpha/beta/stable)
* Defaults to configuration preferred state
* @return null|PEAR_Error|string
* @access private
*/
function _downloadFile($pkgfile, $version, $origpkgfile, $state = null)
{
if (is_null($state)) {
$state = $this->_preferredState;
}
// {{{ check the package filename, and whether it's already installed
$need_download = false;
if (preg_match('#^(http|ftp)://#', $pkgfile)) {
$need_download = true;
} elseif (!@is_file($pkgfile)) {
if ($this->validPackageName($pkgfile)) {
if ($this->_registry->packageExists($pkgfile)) {
if (empty($this->_options['upgrade']) && empty($this->_options['force'])) {
$errors[] = "$pkgfile already installed";
return;
}
}
$pkgfile = $this->getPackageDownloadUrl($pkgfile, $version);
$need_download = true;
} else {
if (strlen($pkgfile)) {
$errors[] = "Could not open the package file: $pkgfile";
} else {
$errors[] = "No package file given";
}
return;
}
}
// }}}
// {{{ Download package -----------------------------------------------
if ($need_download) {
$downloaddir = $this->_config->get('download_dir');
if (empty($downloaddir)) {
if (PEAR::isError($downloaddir = System::mktemp('-d'))) {
return $downloaddir;
}
$this->log(3, '+ tmp dir created at ' . $downloaddir);
}
$callback = $this->ui ? array(&$this, '_downloadCallback') : null;
$this->pushErrorHandling(PEAR_ERROR_RETURN);
$file = $this->downloadHttp($pkgfile, $this->ui, $downloaddir, $callback);
$this->popErrorHandling();
if (PEAR::isError($file)) {
if ($this->validPackageName($origpkgfile)) {
if (!PEAR::isError($info = $this->_remote->call('package.info',
$origpkgfile))) {
if (!count($info['releases'])) {
return $this->raiseError('Package ' . $origpkgfile .
' has no releases');
} else {
return $this->raiseError('No releases of preferred state "'
. $state . '" exist for package ' . $origpkgfile .
'. Use ' . $origpkgfile . '-state to install another' .
' state (like ' . $origpkgfile .'-beta)',
PEAR_INSTALLER_ERROR_NO_PREF_STATE);
}
} else {
return $pkgfile;
}
} else {
return $this->raiseError($file);
}
}
$pkgfile = $file;
}
// }}}
return $pkgfile;
}
// }}}
// {{{ getPackageDownloadUrl()
function getPackageDownloadUrl($package, $version = null)
{
if ($version) {
$package .= "-$version";
}
if ($this === null || $this->_config === null) {
$package = "http://pear.php.net/get/$package";
} else {
$package = "http://" . $this->_config->get('master_server') .
"/get/$package";
}
if (!extension_loaded("zlib")) {
$package .= '?uncompress=yes';
}
return $package;
}
// }}}
// {{{ extractDownloadFileName($pkgfile, &$version)
function extractDownloadFileName($pkgfile, &$version)
{
if (@is_file($pkgfile)) {
return $pkgfile;
}
// regex defined in Common.php
if (preg_match(PEAR_COMMON_PACKAGE_DOWNLOAD_PREG, $pkgfile, $m)) {
$version = (isset($m[3])) ? $m[3] : null;
return $m[1];
}
$version = null;
return $pkgfile;
}
// }}}
// }}}
// {{{ getDownloadedPackages()
/**
* Retrieve a list of downloaded packages after a call to {@link download()}.
*
* Also resets the list of downloaded packages.
* @return array
*/
function getDownloadedPackages()
{
$ret = $this->_downloadedPackages;
$this->_downloadedPackages = array();
$this->_toDownload = array();
return $ret;
}
// }}}
// {{{ download()
/**
* Download any files and their dependencies, if necessary
*
* BC-compatible method name
* @param array a mixed list of package names, local files, or package.xml
*/
function download($packages)
{
return $this->doDownload($packages);
}
// }}}
// {{{ doDownload()
/**
* Download any files and their dependencies, if necessary
*
* @param array a mixed list of package names, local files, or package.xml
*/
function doDownload($packages)
{
$mywillinstall = array();
$state = $this->_preferredState;
// {{{ download files in this list if necessary
foreach($packages as $pkgfile) {
$need_download = false;
if (!is_file($pkgfile)) {
if (preg_match('#^(http|ftp)://#', $pkgfile)) {
$need_download = true;
}
$pkgfile = $this->_downloadNonFile($pkgfile);
if (PEAR::isError($pkgfile)) {
return $pkgfile;
}
if ($pkgfile === false) {
continue;
}
} // end is_file()
$tempinfo = $this->infoFromAny($pkgfile);
if ($need_download) {
$this->_toDownload[] = $tempinfo['package'];
}
if (isset($this->_options['alldeps']) || isset($this->_options['onlyreqdeps'])) {
// ignore dependencies if there are any errors
if (!PEAR::isError($tempinfo)) {
$mywillinstall[strtolower($tempinfo['package'])] = @$tempinfo['release_deps'];
}
}
$this->_downloadedPackages[] = array('pkg' => $tempinfo['package'],
'file' => $pkgfile, 'info' => $tempinfo);
} // end foreach($packages)
// }}}
// {{{ extract dependencies from downloaded files and then download
// them if necessary
if (isset($this->_options['alldeps']) || isset($this->_options['onlyreqdeps'])) {
$deppackages = array();
foreach ($mywillinstall as $package => $alldeps) {
if (!is_array($alldeps)) {
// there are no dependencies
continue;
}
$fail = false;
foreach ($alldeps as $info) {
if ($info['type'] != 'pkg') {
continue;
}
$ret = $this->_processDependency($package, $info, $mywillinstall);
if ($ret === false) {
continue;
}
if ($ret === 0) {
$fail = true;
continue;
}
if (PEAR::isError($ret)) {
return $ret;
}
$deppackages[] = $ret;
} // foreach($alldeps
if ($fail) {
$deppackages = array();
}
}
if (count($deppackages)) {
$this->doDownload($deppackages);
}
} // }}} if --alldeps or --onlyreqdeps
}
// }}}
// {{{ _downloadNonFile($pkgfile)
/**
* @return false|PEAR_Error|string false if loop should be broken out of,
* string if the file was downloaded,
* PEAR_Error on exception
* @access private
*/
function _downloadNonFile($pkgfile)
{
$origpkgfile = $pkgfile;
$state = null;
$pkgfile = $this->extractDownloadFileName($pkgfile, $version);
if (preg_match('#^(http|ftp)://#', $pkgfile)) {
return $this->_downloadFile($pkgfile, $version, $origpkgfile);
}
if (!$this->validPackageName($pkgfile)) {
return $this->raiseError("Package name '$pkgfile' not valid");
}
// ignore packages that are installed unless we are upgrading
$curinfo = $this->_registry->packageInfo($pkgfile);
if ($this->_registry->packageExists($pkgfile)
&& empty($this->_options['upgrade']) && empty($this->_options['force'])) {
$this->log(0, "Package '{$curinfo['package']}' already installed, skipping");
return false;
}
if (in_array($pkgfile, $this->_toDownload)) {
return false;
}
$releases = $this->_remote->call('package.info', $pkgfile, 'releases', true);
if (!count($releases)) {
return $this->raiseError("No releases found for package '$pkgfile'");
}
// Want a specific version/state
if ($version !== null) {
// Passed Foo-1.2
if ($this->validPackageVersion($version)) {
if (!isset($releases[$version])) {
return $this->raiseError("No release with version '$version' found for '$pkgfile'");
}
// Passed Foo-alpha
} elseif (in_array($version, $this->getReleaseStates())) {
$state = $version;
$version = 0;
foreach ($releases as $ver => $inf) {
if ($inf['state'] == $state && version_compare("$version", "$ver") < 0) {
$version = $ver;
break;
}
}
if ($version == 0) {
return $this->raiseError("No release with state '$state' found for '$pkgfile'");
}
// invalid suffix passed
} else {
return $this->raiseError("Invalid suffix '-$version', be sure to pass a valid PEAR ".
"version number or release state");
}
// Guess what to download
} else {
$states = $this->betterStates($this->_preferredState, true);
$possible = false;
$version = 0;
$higher_version = 0;
$prev_hi_ver = 0;
foreach ($releases as $ver => $inf) {
if (in_array($inf['state'], $states) && version_compare("$version", "$ver") < 0) {
$version = $ver;
break;
} else {
$ver = (string)$ver;
if (version_compare($prev_hi_ver, $ver) < 0) {
$prev_hi_ver = $higher_version = $ver;
}
}
}
if ($version === 0 && !isset($this->_options['force'])) {
return $this->raiseError('No release with state equal to: \'' . implode(', ', $states) .
"' found for '$pkgfile'");
} elseif ($version === 0) {
$this->log(0, "Warning: $pkgfile is state '" . $releases[$higher_version]['state'] . "' which is less stable " .
"than state '$this->_preferredState'");
}
}
// Check if we haven't already the version
if (empty($this->_options['force']) && !is_null($curinfo)) {
if ($curinfo['version'] == $version) {
$this->log(0, "Package '{$curinfo['package']}-{$curinfo['version']}' already installed, skipping");
return false;
} elseif (version_compare("$version", "{$curinfo['version']}") < 0) {
$this->log(0, "Package '{$curinfo['package']}' version '{$curinfo['version']}' " .
" is installed and {$curinfo['version']} is > requested '$version', skipping");
return false;
}
}
$this->_toDownload[] = $pkgfile;
return $this->_downloadFile($pkgfile, $version, $origpkgfile, $state);
}
// }}}
// {{{ _processDependency($package, $info, $mywillinstall)
/**
* Process a dependency, download if necessary
* @param array dependency information from PEAR_Remote call
* @param array packages that will be installed in this iteration
* @return false|string|PEAR_Error
* @access private
* @todo Add test for relation 'lt'/'le' -> make sure that the dependency requested is
* in fact lower than the required value. This will be very important for BC dependencies
*/
function _processDependency($package, $info, $mywillinstall)
{
$state = $this->_preferredState;
if (!isset($this->_options['alldeps']) && isset($info['optional']) &&
$info['optional'] == 'yes') {
// skip optional deps
$this->log(0, "skipping Package '$package' optional dependency '$info[name]'");
return false;
}
// {{{ get releases
$releases = $this->_remote->call('package.info', $info['name'], 'releases', true);
if (PEAR::isError($releases)) {
return $releases;
}
if (!count($releases)) {
if (!isset($this->_installed[strtolower($info['name'])])) {
$this->pushError("Package '$package' dependency '$info[name]' ".
"has no releases");
}
return false;
}
$found = false;
$save = $releases;
while(count($releases) && !$found) {
if (!empty($state) && $state != 'any') {
list($release_version, $release) = each($releases);
if ($state != $release['state'] &&
!in_array($release['state'], $this->betterStates($state)))
{
// drop this release - it ain't stable enough
array_shift($releases);
} else {
$found = true;
}
} else {
$found = true;
}
}
if (!count($releases) && !$found) {
$get = array();
foreach($save as $release) {
$get = array_merge($get,
$this->betterStates($release['state'], true));
}
$savestate = array_shift($get);
$this->pushError( "Release for '$package' dependency '$info[name]' " .
"has state '$savestate', requires '$state'");
return 0;
}
if (in_array(strtolower($info['name']), $this->_toDownload) ||
isset($mywillinstall[strtolower($info['name'])])) {
// skip upgrade check for packages we will install
return false;
}
if (!isset($this->_installed[strtolower($info['name'])])) {
// check to see if we can install the specific version required
if ($info['rel'] == 'eq') {
return $info['name'] . '-' . $info['version'];
}
// skip upgrade check for packages we don't have installed
return $info['name'];
}
// }}}
// {{{ see if a dependency must be upgraded
$inst_version = $this->_registry->packageInfo($info['name'], 'version');
if (!isset($info['version'])) {
// this is a rel='has' dependency, check against latest
if (version_compare($release_version, $inst_version, 'le')) {
return false;
} else {
return $info['name'];
}
}
if (version_compare($info['version'], $inst_version, 'le')) {
// installed version is up-to-date
return false;
}
return $info['name'];
}
// }}}
// {{{ _downloadCallback()
function _downloadCallback($msg, $params = null)
{
switch ($msg) {
case 'saveas':
$this->log(1, "downloading $params ...");
break;
case 'done':
$this->log(1, '...done: ' . number_format($params, 0, '', ',') . ' bytes');
break;
case 'bytesread':
static $bytes;
if (empty($bytes)) {
$bytes = 0;
}
if (!($bytes % 10240)) {
$this->log(1, '.', false);
}
$bytes += $params;
break;
case 'start':
$this->log(1, "Starting to download {$params[0]} (".number_format($params[1], 0, '', ',')." bytes)");
break;
}
if (method_exists($this->ui, '_downloadCallback'))
$this->ui->_downloadCallback($msg, $params);
}
// }}}
// {{{ _prependPath($path, $prepend)
function _prependPath($path, $prepend)
{
if (strlen($prepend) > 0) {
if (OS_WINDOWS && preg_match('/^[a-z]:/i', $path)) {
$path = $prepend . substr($path, 2);
} else {
$path = $prepend . $path;
}
}
return $path;
}
// }}}
// {{{ pushError($errmsg, $code)
/**
* @param string
* @param integer
*/
function pushError($errmsg, $code = -1)
{
array_push($this->_errorStack, array($errmsg, $code));
}
// }}}
// {{{ getErrorMsgs()
function getErrorMsgs()
{
$msgs = array();
$errs = $this->_errorStack;
foreach ($errs as $err) {
$msgs[] = $err[0];
}
$this->_errorStack = array();
return $msgs;
}
// }}}
}
// }}}
?>