* Jordi Boggiano | *=> *)(.*?)(?: |$)}m', $phpinfo, $match)) {
$configure = $match[1];
if (false !== strpos($configure, '--enable-sigchild')) {
$warnings['sigchild'] = array(
'PHP was compiled with --enable-sigchild which can cause issues on some platforms.',
'Recompile it without this flag if possible, see also:',
' https://bugs.php.net/bug.php?id=22999'
);
}
if (false !== strpos($configure, '--with-curlwrappers')) {
$warnings['curlwrappers'] = array(
'PHP was compiled with --with-curlwrappers which will cause issues with HTTP authentication and GitHub.',
'Recompile it without this flag if possible'
);
}
}
// Stringify the message arrays
foreach ($errors as $key => $value) {
$errors[$key] = PHP_EOL.implode(PHP_EOL, $value);
}
foreach ($warnings as $key => $value) {
$warnings[$key] = PHP_EOL.implode(PHP_EOL, $value);
}
return !empty($errors) || !empty($warnings);
}
/**
* Outputs an array of issues
*
* @param array $issues
*/
function outputIssues($issues)
{
foreach ($issues as $issue) {
out($issue, 'info');
}
out('');
}
/**
* Outputs any warnings found
*
* @param array $warnings
*/
function showWarnings($warnings)
{
if (!empty($warnings)) {
out('Some settings on your machine may cause stability issues with Composer.', 'error');
out('If you encounter issues, try to change the following:', 'error');
outputIssues($warnings);
}
}
/**
* Outputs an end of process warning if tls has been bypassed
*
* @param bool $disableTls Bypass tls
*/
function showSecurityWarning($disableTls)
{
if ($disableTls) {
out('You have instructed the Installer not to enforce SSL/TLS security on remote HTTPS requests.', 'info');
out('This will leave all downloads during installation vulnerable to Man-In-The-Middle (MITM) attacks', 'info');
}
}
/**
* colorize output
*/
function out($text, $color = null, $newLine = true)
{
$styles = array(
'success' => "\033[0;32m%s\033[0m",
'error' => "\033[31;31m%s\033[0m",
'info' => "\033[33;33m%s\033[0m"
);
$format = '%s';
if (is_string($color) && isset($styles[$color]) && USE_ANSI) {
$format = $styles[$color];
}
if ($newLine) {
$format .= PHP_EOL;
}
printf($format, $text);
}
/**
* Returns the system-dependent Composer home location, which may not exist
*
* @return string
*/
function getHomeDir()
{
$home = getenv('COMPOSER_HOME');
if ($home) {
return $home;
}
$userDir = getUserDir();
if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
return $userDir.'/Composer';
}
$dirs = array();
if (useXdg()) {
// XDG Base Directory Specifications
$xdgConfig = getenv('XDG_CONFIG_HOME');
if (!$xdgConfig) {
$xdgConfig = $userDir . '/.config';
}
$dirs[] = $xdgConfig . '/composer';
}
$dirs[] = $userDir . '/.composer';
// select first dir which exists of: $XDG_CONFIG_HOME/composer or ~/.composer
foreach ($dirs as $dir) {
if (is_dir($dir)) {
return $dir;
}
}
// if none exists, we default to first defined one (XDG one if system uses it, or ~/.composer otherwise)
return $dirs[0];
}
/**
* Returns the location of the user directory from the environment
* @throws RuntimeException If the environment value does not exists
*
* @return string
*/
function getUserDir()
{
$userEnv = defined('PHP_WINDOWS_VERSION_MAJOR') ? 'APPDATA' : 'HOME';
$userDir = getenv($userEnv);
if (!$userDir) {
throw new RuntimeException('The '.$userEnv.' or COMPOSER_HOME environment variable must be set for composer to run correctly');
}
return rtrim(strtr($userDir, '\\', '/'), '/');
}
/**
* @return bool
*/
function useXdg()
{
foreach (array_keys($_SERVER) as $key) {
if (strpos((string) $key, 'XDG_') === 0) {
return true;
}
}
if (is_dir('/etc/xdg')) {
return true;
}
return false;
}
function validateCaFile($contents)
{
// assume the CA is valid if php is vulnerable to
// https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html
if (
PHP_VERSION_ID <= 50327
|| (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50422)
|| (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50506)
) {
return !empty($contents);
}
return (bool) openssl_x509_parse($contents);
}
/**
* Returns php.ini location information
*
* @return string
*/
function getIniMessage()
{
$paths = array((string) php_ini_loaded_file());
$scanned = php_ini_scanned_files();
if ($scanned !== false) {
$paths = array_merge($paths, array_map('trim', explode(',', $scanned)));
}
// We will have at least one value, which may be empty
if ($paths[0] === '') {
array_shift($paths);
}
$ini = array_shift($paths);
if ($ini === null) {
return 'A php.ini file does not exist. You will have to create one.';
}
if (count($paths) > 1) {
return 'Your command-line PHP is using multiple ini files. Run `php --ini` to show them.';
}
return 'The php.ini used by your command-line PHP is: '.$ini;
}
class Installer
{
private $quiet;
private $disableTls;
private $cafile;
private $displayPath;
private $target;
private $tmpFile;
private $tmpCafile;
private $baseUrl;
private $algo;
private $errHandler;
private $httpClient;
private $pubKeys = array();
private $installs = array();
/**
* Constructor - must not do anything that throws an exception
*
* @param bool $quiet Quiet mode
* @param bool $disableTls Bypass tls
* @param mixed $cafile Path to CA bundle, or false
*/
public function __construct($quiet, $disableTls, $caFile)
{
if (($this->quiet = $quiet)) {
ob_start();
}
$this->disableTls = $disableTls;
$this->cafile = $caFile;
$this->errHandler = new ErrorHandler();
}
/**
* Runs the installer
*
* @param mixed $version Specific version to install, or false
* @param mixed $installDir Specific installation directory, or false
* @param string $filename Specific filename to save to, or composer.phar
* @param string $channel Specific version channel to use
* @throws Exception If anything other than a RuntimeException is caught
*
* @return bool If the installation succeeded
*/
public function run($version, $installDir, $filename, $channel)
{
try {
$this->initTargets($installDir, $filename);
$this->initTls();
$this->httpClient = new HttpClient($this->disableTls, $this->cafile);
$result = $this->install($version, $channel);
// in case --1 or --2 is passed, we leave the default channel for next self-update to stable
if (1 === preg_match('{^\d+$}D', $channel)) {
$channel = 'stable';
}
if ($result && $channel !== 'stable' && !$version && defined('PHP_BINARY')) {
$null = (defined('PHP_WINDOWS_VERSION_MAJOR') ? 'NUL' : '/dev/null');
@exec(escapeshellarg(PHP_BINARY) .' '.escapeshellarg($this->target).' self-update --'.$channel.' --set-channel-only -q > '.$null.' 2> '.$null, $output);
}
} catch (Exception $e) {
$result = false;
}
// Always clean up
$this->cleanUp($result);
if (isset($e)) {
// Rethrow anything that is not a RuntimeException
if (!$e instanceof RuntimeException) {
throw $e;
}
out($e->getMessage(), 'error');
}
return $result;
}
/**
* Initialization methods to set the required filenames and composer url
*
* @param mixed $installDir Specific installation directory, or false
* @param string $filename Specific filename to save to, or composer.phar
* @throws RuntimeException If the installation directory is not writable
*/
protected function initTargets($installDir, $filename)
{
$this->displayPath = ($installDir ? rtrim($installDir, '/').'/' : '').$filename;
$installDir = $installDir ? realpath($installDir) : getcwd();
if (!is_writeable($installDir)) {
throw new RuntimeException('The installation directory "'.$installDir.'" is not writable');
}
$this->target = $installDir.DIRECTORY_SEPARATOR.$filename;
$this->tmpFile = $installDir.DIRECTORY_SEPARATOR.basename($this->target, '.phar').'-temp.phar';
$uriScheme = $this->disableTls ? 'http' : 'https';
$this->baseUrl = $uriScheme.'://getcomposer.org';
}
/**
* A wrapper around methods to check tls and write public keys
* @throws RuntimeException If SHA384 is not supported
*/
protected function initTls()
{
if ($this->disableTls) {
return;
}
if (!in_array('sha384', array_map('strtolower', openssl_get_md_methods()))) {
throw new RuntimeException('SHA384 is not supported by your openssl extension');
}
$this->algo = defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'SHA384';
$home = $this->getComposerHome();
$this->pubKeys = array(
'dev' => $this->installKey(self::getPKDev(), $home, 'keys.dev.pub'),
'tags' => $this->installKey(self::getPKTags(), $home, 'keys.tags.pub')
);
if (empty($this->cafile) && !HttpClient::getSystemCaRootBundlePath()) {
$this->cafile = $this->tmpCafile = $this->installKey(HttpClient::getPackagedCaFile(), $home, 'cacert-temp.pem');
}
}
/**
* Returns the Composer home directory, creating it if required
* @throws RuntimeException If the directory cannot be created
*
* @return string
*/
protected function getComposerHome()
{
$home = getHomeDir();
if (!is_dir($home)) {
$this->errHandler->start();
if (!mkdir($home, 0777, true)) {
throw new RuntimeException(sprintf(
'Unable to create Composer home directory "%s": %s',
$home,
$this->errHandler->message
));
}
$this->installs[] = $home;
$this->errHandler->stop();
}
return $home;
}
/**
* Writes public key data to disc
*
* @param string $data The public key(s) in pem format
* @param string $path The directory to write to
* @param string $filename The name of the file
* @throws RuntimeException If the file cannot be written
*
* @return string The path to the saved data
*/
protected function installKey($data, $path, $filename)
{
$this->errHandler->start();
$target = $path.DIRECTORY_SEPARATOR.$filename;
$installed = file_exists($target);
$write = file_put_contents($target, $data, LOCK_EX);
@chmod($target, 0644);
$this->errHandler->stop();
if (!$write) {
throw new RuntimeException(sprintf('Unable to write %s to: %s', $filename, $path));
}
if (!$installed) {
$this->installs[] = $target;
}
return $target;
}
/**
* The main install function
*
* @param mixed $version Specific version to install, or false
* @param string $channel Version channel to use
*
* @return bool If the installation succeeded
*/
protected function install($version, $channel)
{
$retries = 3;
$result = false;
$infoMsg = 'Downloading...';
$infoType = 'info';
while ($retries--) {
if (!$this->quiet) {
out($infoMsg, $infoType);
$infoMsg = 'Retrying...';
$infoType = 'error';
}
if (!$this->getVersion($channel, $version, $url, $error)) {
out($error, 'error');
continue;
}
if (!$this->downloadToTmp($url, $signature, $error)) {
out($error, 'error');
continue;
}
if (!$this->verifyAndSave($version, $signature, $error)) {
out($error, 'error');
continue;
}
$result = true;
break;
}
if (!$this->quiet) {
if ($result) {
out(PHP_EOL."Composer (version {$version}) successfully installed to: {$this->target}", 'success');
out("Use it: php {$this->displayPath}", 'info');
out('');
} else {
out('The download failed repeatedly, aborting.', 'error');
}
}
return $result;
}
/**
* Sets the version url, downloading version data if required
*
* @param string $channel Version channel to use
* @param false|string $version Version to install, or set by method
* @param null|string $url The versioned url, set by method
* @param null|string $error Set by method on failure
*
* @return bool If the operation succeeded
*/
protected function getVersion($channel, &$version, &$url, &$error)
{
$error = '';
if ($version) {
if (empty($url)) {
$url = $this->baseUrl."/download/{$version}/composer.phar";
}
return true;
}
$this->errHandler->start();
if ($this->downloadVersionData($data, $error)) {
$this->parseVersionData($data, $channel, $version, $url);
}
$this->errHandler->stop();
return empty($error);
}
/**
* Downloads and json-decodes version data
*
* @param null|array $data Downloaded version data, set by method
* @param null|string $error Set by method on failure
*
* @return bool If the operation succeeded
*/
protected function downloadVersionData(&$data, &$error)
{
$url = $this->baseUrl.'/versions';
$errFmt = 'The "%s" file could not be %s: %s';
if (!$json = $this->httpClient->get($url)) {
$error = sprintf($errFmt, $url, 'downloaded', $this->errHandler->message);
return false;
}
if (!$data = json_decode($json, true)) {
$error = sprintf($errFmt, $url, 'json-decoded', $this->getJsonError());
return false;
}
return true;
}
/**
* A wrapper around the methods needed to download and save the phar
*
* @param string $url The versioned download url
* @param null|string $signature Set by method on successful download
* @param null|string $error Set by method on failure
*
* @return bool If the operation succeeded
*/
protected function downloadToTmp($url, &$signature, &$error)
{
$error = '';
$errFmt = 'The "%s" file could not be downloaded: %s';
$sigUrl = $url.'.sig';
$this->errHandler->start();
if (!$fh = fopen($this->tmpFile, 'w')) {
$error = sprintf('Could not create file "%s": %s', $this->tmpFile, $this->errHandler->message);
} elseif (!$this->getSignature($sigUrl, $signature)) {
$error = sprintf($errFmt, $sigUrl, $this->errHandler->message);
} elseif (!fwrite($fh, $this->httpClient->get($url))) {
$error = sprintf($errFmt, $url, $this->errHandler->message);
}
if (is_resource($fh)) {
fclose($fh);
}
$this->errHandler->stop();
return empty($error);
}
/**
* Verifies the downloaded file and saves it to the target location
*
* @param string $version The composer version downloaded
* @param string $signature The digital signature to check
* @param null|string $error Set by method on failure
*
* @return bool If the operation succeeded
*/
protected function verifyAndSave($version, $signature, &$error)
{
$error = '';
if (!$this->validatePhar($this->tmpFile, $pharError)) {
$error = 'The download is corrupt: '.$pharError;
} elseif (!$this->verifySignature($version, $signature, $this->tmpFile)) {
$error = 'Signature mismatch, could not verify the phar file integrity';
} else {
$this->errHandler->start();
if (!rename($this->tmpFile, $this->target)) {
$error = sprintf('Could not write to file "%s": %s', $this->target, $this->errHandler->message);
}
chmod($this->target, 0755);
$this->errHandler->stop();
}
return empty($error);
}
/**
* Parses an array of version data to match the required channel
*
* @param array $data Downloaded version data
* @param mixed $channel Version channel to use
* @param false|string $version Set by method
* @param mixed $url The versioned url, set by method
*/
protected function parseVersionData(array $data, $channel, &$version, &$url)
{
foreach ($data[$channel] as $candidate) {
if ($candidate['min-php'] <= PHP_VERSION_ID) {
$version = $candidate['version'];
$url = $this->baseUrl.$candidate['path'];
break;
}
}
if (!$version) {
$error = sprintf(
'None of the %d %s version(s) of Composer matches your PHP version (%s / ID: %d)',
count($data[$channel]),
$channel,
PHP_VERSION,
PHP_VERSION_ID
);
throw new RuntimeException($error);
}
}
/**
* Downloads the digital signature of required phar file
*
* @param string $url The signature url
* @param null|string $signature Set by method on success
*
* @return bool If the download succeeded
*/
protected function getSignature($url, &$signature)
{
if (!$result = $this->disableTls) {
$signature = $this->httpClient->get($url);
if ($signature) {
$signature = json_decode($signature, true);
$signature = base64_decode($signature['sha384']);
$result = true;
}
}
return $result;
}
/**
* Verifies the signature of the downloaded phar
*
* @param string $version The composer versione
* @param string $signature The downloaded digital signature
* @param string $file The temp phar file
*
* @return bool If the operation succeeded
*/
protected function verifySignature($version, $signature, $file)
{
if (!$result = $this->disableTls) {
$path = preg_match('{^[0-9a-f]{40}$}', $version) ? $this->pubKeys['dev'] : $this->pubKeys['tags'];
$pubkeyid = openssl_pkey_get_public('file://'.$path);
$result = 1 === openssl_verify(
file_get_contents($file),
$signature,
$pubkeyid,
$this->algo
);
// PHP 8 automatically frees the key instance and deprecates the function
if (PHP_VERSION_ID < 80000) {
openssl_free_key($pubkeyid);
}
}
return $result;
}
/**
* Validates the downloaded phar file
*
* @param string $pharFile The temp phar file
* @param null|string $error Set by method on failure
*
* @return bool If the operation succeeded
*/
protected function validatePhar($pharFile, &$error)
{
if (ini_get('phar.readonly')) {
return true;
}
try {
// Test the phar validity
$phar = new Phar($pharFile);
// Free the variable to unlock the file
unset($phar);
$result = true;
} catch (Exception $e) {
if (!$e instanceof UnexpectedValueException && !$e instanceof PharException) {
throw $e;
}
$error = $e->getMessage();
$result = false;
}
return $result;
}
/**
* Returns a string representation of the last json error
*
* @return string The error string or code
*/
protected function getJsonError()
{
if (function_exists('json_last_error_msg')) {
return json_last_error_msg();
} else {
return 'json_last_error = '.json_last_error();
}
}
/**
* Cleans up resources at the end of the installation
*
* @param bool $result If the installation succeeded
*/
protected function cleanUp($result)
{
if ($this->quiet) {
// Ensure output buffers are emptied
$errors = explode(PHP_EOL, (string) ob_get_clean());
}
if (!$result) {
// Output buffered errors
if ($this->quiet) {
$this->outputErrors($errors);
}
// Clean up stuff we created
$this->uninstall();
} elseif ($this->tmpCafile !== null) {
@unlink($this->tmpCafile);
}
}
/**
* Outputs unique errors when in quiet mode
*
*/
protected function outputErrors(array $errors)
{
$shown = array();
foreach ($errors as $error) {
if ($error && !in_array($error, $shown)) {
out($error, 'error');
$shown[] = $error;
}
}
}
/**
* Uninstalls newly-created files and directories on failure
*
*/
protected function uninstall()
{
foreach (array_reverse($this->installs) as $target) {
if (is_file($target)) {
@unlink($target);
} elseif (is_dir($target)) {
@rmdir($target);
}
}
if ($this->tmpFile !== null && file_exists($this->tmpFile)) {
@unlink($this->tmpFile);
}
}
public static function getPKDev()
{
return <<