From 379fb70ad96285cd945f0dd974677072b5409282 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Sat, 19 Nov 2016 20:50:15 +0000 Subject: [PATCH 1/2] Use random name for tmp ini and delete after use Thanks to Patrick Rose for reporting this issue. --- src/Composer/Command/DiagnoseCommand.php | 10 +- .../SolverProblemsException.php | 16 +- src/Composer/Downloader/RarDownloader.php | 9 +- src/Composer/Downloader/ZipDownloader.php | 10 +- src/Composer/Util/IniHelper.php | 64 ++++++++ src/Composer/XdebugHandler.php | 138 ++++++++---------- tests/Composer/Test/Util/IniHelperTest.php | 68 +++++++++ tests/Composer/Test/XdebugHandlerTest.php | 37 +++-- 8 files changed, 227 insertions(+), 125 deletions(-) create mode 100644 src/Composer/Util/IniHelper.php create mode 100644 tests/Composer/Test/Util/IniHelperTest.php diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 551239376..2cda68689 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -19,6 +19,7 @@ use Composer\Downloader\TransportException; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Util\ConfigValidator; +use Composer\Util\IniHelper; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; use Composer\Util\StreamContextFactory; @@ -436,14 +437,9 @@ EOT // code below taken from getcomposer.org/installer, any changes should be made there and replicated here $errors = array(); $warnings = array(); - - $iniPath = php_ini_loaded_file(); $displayIniMessage = false; - if ($iniPath) { - $iniMessage = PHP_EOL.PHP_EOL.'The php.ini used by your command-line PHP is: ' . $iniPath; - } else { - $iniMessage = PHP_EOL.PHP_EOL.'A php.ini file does not exist. You will have to create one.'; - } + + $iniMessage = PHP_EOL.PHP_EOL.IniHelper::getMessage(); $iniMessage .= PHP_EOL.'If you can not modify the ini file, you can also run `php -d option=value` to modify ini values on the fly. You can use -d multiple times.'; if (!function_exists('json_decode')) { diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index c6092c28c..6014012a4 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -12,6 +12,8 @@ namespace Composer\DependencyResolver; +use Composer\Util\IniHelper; + /** * @author Nils Adermann */ @@ -58,21 +60,13 @@ class SolverProblemsException extends \RuntimeException private function createExtensionHint() { - $paths = array(); - - if (($iniPath = php_ini_loaded_file()) !== false) { - $paths[] = $iniPath; - } - - if (!defined('HHVM_VERSION') && $additionalIniPaths = php_ini_scanned_files()) { - $paths = array_merge($paths, array_map("trim", explode(",", $additionalIniPaths))); - } + $paths = IniHelper::getAll(); - if (count($paths) === 0) { + if (count($paths) === 1 && empty($paths[0])) { return ''; } - $text = "\n To enable extensions, verify that they are enabled in those .ini files:\n - "; + $text = "\n To enable extensions, verify that they are enabled in your .ini files:\n - "; $text .= implode("\n - ", $paths); $text .= "\n You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode."; diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index eb6355a3e..40cd09896 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -15,6 +15,7 @@ namespace Composer\Downloader; use Composer\Config; use Composer\Cache; use Composer\EventDispatcher\EventDispatcher; +use Composer\Util\IniHelper; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; @@ -55,13 +56,7 @@ class RarDownloader extends ArchiveDownloader if (!class_exists('RarArchive')) { // php.ini path is added to the error message to help users find the correct file - $iniPath = php_ini_loaded_file(); - - if ($iniPath) { - $iniMessage = 'The php.ini used by your command-line PHP is: ' . $iniPath; - } else { - $iniMessage = 'A php.ini file does not exist. You will have to create one.'; - } + $iniMessage = IniHelper::getMessage(); $error = "Could not decompress the archive, enable the PHP rar extension or install unrar.\n" . $iniMessage . "\n" . $processError; diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index fb3f336bc..99fecdf0e 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -16,6 +16,7 @@ use Composer\Config; use Composer\Cache; use Composer\EventDispatcher\EventDispatcher; use Composer\Package\PackageInterface; +use Composer\Util\IniHelper; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\RemoteFilesystem; @@ -49,14 +50,7 @@ class ZipDownloader extends ArchiveDownloader if (!class_exists('ZipArchive') && !self::$hasSystemUnzip) { // php.ini path is added to the error message to help users find the correct file - $iniPath = php_ini_loaded_file(); - - if ($iniPath) { - $iniMessage = 'The php.ini used by your command-line PHP is: ' . $iniPath; - } else { - $iniMessage = 'A php.ini file does not exist. You will have to create one.'; - } - + $iniMessage = IniHelper::getMessage(); $error = "The zip extension and unzip command are both missing, skipping.\n" . $iniMessage; throw new \RuntimeException($error); diff --git a/src/Composer/Util/IniHelper.php b/src/Composer/Util/IniHelper.php new file mode 100644 index 000000000..f4eaa26f0 --- /dev/null +++ b/src/Composer/Util/IniHelper.php @@ -0,0 +1,64 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +/** + * Provides ini file location functions that work with and without a restart. + * When the process has restarted it uses a tmp ini and stores the original + * ini locations in an environment variable. + * + * @author John Stevenson + */ +class IniHelper +{ + const ENV_ORIGINAL = 'COMPOSER_ORIGINAL_INIS'; + + /** + * Returns an array of php.ini locations with at least one entry + * + * The equivalent of calling php_ini_loaded_file then php_ini_scanned_files. + * The loaded ini location is the first entry and may be empty. + + * @return array + */ + public static function getAll() + { + if ($env = strval(getenv(self::ENV_ORIGINAL))) { + return explode(PATH_SEPARATOR, $env); + } + + $paths = array(strval(php_ini_loaded_file())); + + if ($scanned = php_ini_scanned_files()) { + $paths = array_merge($paths, array_map('trim', explode(',', $scanned))); + } + + return $paths; + } + + /** + * Describes the location of the loaded php.ini file + * + * @return string + */ + public static function getMessage() + { + $paths = self::getAll(); + + if (empty($paths[0])) { + return 'A php.ini file does not exist. You will have to create one.'; + } + + return 'The php.ini used by your command-line PHP is: '.$paths[0]; + } +} diff --git a/src/Composer/XdebugHandler.php b/src/Composer/XdebugHandler.php index a76d243f5..658d797f0 100644 --- a/src/Composer/XdebugHandler.php +++ b/src/Composer/XdebugHandler.php @@ -12,6 +12,7 @@ namespace Composer; +use Composer\Util\IniHelper; use Symfony\Component\Console\Output\OutputInterface; /** @@ -25,6 +26,7 @@ class XdebugHandler private $output; private $loaded; private $envScanDir; + private $tmpIni; /** * Constructor @@ -41,7 +43,8 @@ class XdebugHandler * * If so, then a tmp ini is created with the xdebug ini entry commented out. * If additional inis have been loaded, these are combined into the tmp ini - * and PHP_INI_SCAN_DIR is set to an empty value. + * and PHP_INI_SCAN_DIR is set to an empty value. Current ini locations are + * are stored in COMPOSER_ORIGINAL_INIS, for use in the restarted process. * * This behaviour can be disabled by setting the COMPOSER_ALLOW_XDEBUG * environment variable to 1. This variable is used internally so that the @@ -73,13 +76,18 @@ class XdebugHandler } /** - * Executes the restarted command + * Executes the restarted command then deletes the tmp ini * * @param string $command */ protected function restart($command) { passthru($command, $exitCode); + + if (!empty($this->tmpIni)) { + @unlink($this->tmpIni); + } + exit($exitCode); } @@ -113,82 +121,75 @@ class XdebugHandler */ private function prepareRestart(&$command) { - $iniFiles = array(); - if ($loadedIni = php_ini_loaded_file()) { - $iniFiles[] = $loadedIni; - } - - $additional = $this->getAdditionalInis($iniFiles, $replace); - $tmpIni = $this->writeTmpIni($iniFiles, $replace); + $this->tmpIni = ''; + $iniPaths = IniHelper::getAll(); + $files = $this->getWorkingSet($iniPaths, $replace); - if (false !== $tmpIni) { - $command = $this->getCommand($tmpIni); - return $this->setEnvironment($additional); + if ($this->writeTmpIni($files, $replace)) { + $command = $this->getCommand(); + return $this->setEnvironment($iniPaths); } return false; } /** - * Writes the tmp ini file and returns its filename + * Returns true if the tmp ini file was written * - * The filename is passed as the -c option when the process restarts. On - * non-Windows platforms the filename is prefixed with the username to - * avoid any multi-user conflict. Windows always uses the user temp dir. + * The filename is passed as the -c option when the process restarts. * * @param array $iniFiles The php.ini locations - * @param bool $replace Whether we need to modify the files + * @param bool $replace Whether the files need modifying * - * @return bool|string False if the tmp ini could not be created + * @return bool */ private function writeTmpIni(array $iniFiles, $replace) { if (empty($iniFiles)) { // Unlikely, maybe xdebug was loaded through a command line option. - return ''; + return true; } - if (function_exists('posix_getpwuid')) { - $user = posix_getpwuid(posix_getuid()); + if (!$this->tmpIni = tempnam(sys_get_temp_dir(), '')) { + return false; } - $prefix = !empty($user) ? $user['name'].'-' : ''; - $tmpIni = sys_get_temp_dir().'/'.$prefix.'composer-php.ini'; - $content = $this->getIniHeader($iniFiles); + $content = ''; foreach ($iniFiles as $file) { $content .= $this->getIniData($file, $replace); } - return @file_put_contents($tmpIni, $content) ? $tmpIni : false; + return @file_put_contents($this->tmpIni, $content); } /** - * Returns true if additional inis were loaded + * Returns an array of ini files to use * - * @param array $iniFiles Populated by method - * @param bool $replace Whether we need to modify the files + * @param array $iniPaths Locations used by the current prcoess + * @param null|bool $replace Whether the files need modifying, set by method * - * @return bool + * @return array */ - private function getAdditionalInis(array &$iniFiles, &$replace) + private function getWorkingSet(array $iniPaths, &$replace) { $replace = true; + $result = array(); - if ($scanned = php_ini_scanned_files()) { - $list = explode(',', $scanned); + if (empty($iniPaths[0])) { + // There is no loaded ini + array_shift($iniPaths); + } - foreach ($list as $file) { - $file = trim($file); - if (preg_match('/xdebug.ini$/', $file)) { - // Skip the file, no need for regex replacing - $replace = false; - } else { - $iniFiles[] = $file; - } + foreach ($iniPaths as $file) { + if (preg_match('/xdebug.ini$/', $file)) { + // Skip the file, no need for regex replacing + $replace = false; + } else { + $result[] = $file; } } - return !empty($scanned); + return $result; } /** @@ -201,9 +202,8 @@ class XdebugHandler */ private function getIniData($iniFile, $replace) { - $data = str_repeat(PHP_EOL, 3); - $data .= sprintf('; %s%s', $iniFile, PHP_EOL); $contents = file_get_contents($iniFile); + $data = PHP_EOL; if ($replace) { // Comment out xdebug config @@ -219,13 +219,11 @@ class XdebugHandler /** * Returns the restart command line * - * @param string $tmpIni The temporary ini file location - * * @return string */ - private function getCommand($tmpIni) + private function getCommand() { - $phpArgs = array(PHP_BINARY, '-c', $tmpIni); + $phpArgs = array(PHP_BINARY, '-c', $this->tmpIni); $params = array_merge($phpArgs, $this->getScriptArgs($_SERVER['argv'])); return implode(' ', array_map(array($this, 'escape'), $params)); @@ -234,12 +232,25 @@ class XdebugHandler /** * Returns true if the restart environment variables were set * - * @param bool $additional Whether additional inis were loaded + * @param array $iniPaths Locations used by the current prcoess * * @return bool */ - private function setEnvironment($additional) + private function setEnvironment(array $iniPaths) { + // Set scan dir to an empty value if additional ini files were used + $additional = count($iniPaths) > 1; + + if ($additional && !putenv('PHP_INI_SCAN_DIR=')) { + return false; + } + + // Make original inis available to restarted process + if (!putenv(IniHelper::ENV_ORIGINAL.'='.implode(PATH_SEPARATOR, $iniPaths))) { + return false; + } + + // Flag restarted process and save env scan dir state $args = array(self::RESTART_ID); if (false !== $this->envScanDir) { @@ -247,10 +258,6 @@ class XdebugHandler $args[] = $this->envScanDir; } - if ($additional && !putenv('PHP_INI_SCAN_DIR=')) { - return false; - } - return putenv(self::ENV_ALLOW.'='.implode('|', $args)); } @@ -317,29 +324,4 @@ class XdebugHandler return $arg; } - - /** - * Returns the location of the original ini data used. - * - * @param array $iniFiles loaded php.ini locations - * - * @return string - */ - private function getIniHeader($iniFiles) - { - $ini = implode(PHP_EOL.'; ', $iniFiles); - $header = << + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util; + +use Composer\Util\IniHelper; + +/** + * @author John Stevenson + */ +class IniHelperTest extends \PHPUnit_Framework_TestCase +{ + public static $envOriginal; + + public function testWithLoadedIni() + { + $paths = array( + 'loaded.ini', + ); + + $this->setEnv($paths); + $this->assertContains('loaded.ini', IniHelper::getMessage()); + $this->assertEquals($paths, IniHelper::getAll()); + } + + public function testWithoutLoadedIni() + { + $paths = array( + '', + 'one.ini', + 'two.ini', + ); + + $this->setEnv($paths); + $this->assertContains('does not exist', IniHelper::getMessage()); + $this->assertEquals($paths, IniHelper::getAll()); + } + + public static function setUpBeforeClass() + { + // Save current state + self::$envOriginal = getenv(IniHelper::ENV_ORIGINAL); + } + + public static function tearDownAfterClass() + { + // Restore original state + if (false !== self::$envOriginal) { + putenv(IniHelper::ENV_ORIGINAL.'='.self::$envOriginal); + } else { + putenv(IniHelper::ENV_ORIGINAL); + } + } + + protected function setEnv(array $paths) + { + putenv(IniHelper::ENV_ORIGINAL.'='.implode(PATH_SEPARATOR, $paths)); + } +} diff --git a/tests/Composer/Test/XdebugHandlerTest.php b/tests/Composer/Test/XdebugHandlerTest.php index a130b4a1e..35d53a317 100644 --- a/tests/Composer/Test/XdebugHandlerTest.php +++ b/tests/Composer/Test/XdebugHandlerTest.php @@ -13,6 +13,7 @@ namespace Composer\Test; use Composer\Test\Mock\XdebugHandlerMock; +use Composer\Util\IniHelper; /** * @author John Stevenson @@ -22,8 +23,7 @@ use Composer\Test\Mock\XdebugHandlerMock; */ class XdebugHandlerTest extends \PHPUnit_Framework_TestCase { - public static $envAllow; - public static $envIniScanDir; + public static $env = array(); public function testRestartWhenLoaded() { @@ -32,6 +32,7 @@ class XdebugHandlerTest extends \PHPUnit_Framework_TestCase $xdebug = new XdebugHandlerMock($loaded); $xdebug->check(); $this->assertTrue($xdebug->restarted); + $this->assertNotEquals(false, getenv(IniHelper::ENV_ORIGINAL)); } public function testNoRestartWhenNotLoaded() @@ -41,6 +42,7 @@ class XdebugHandlerTest extends \PHPUnit_Framework_TestCase $xdebug = new XdebugHandlerMock($loaded); $xdebug->check(); $this->assertFalse($xdebug->restarted); + $this->assertEquals(false, getenv(IniHelper::ENV_ORIGINAL)); } public function testNoRestartWhenLoadedAndAllowed() @@ -106,28 +108,35 @@ class XdebugHandlerTest extends \PHPUnit_Framework_TestCase public static function setUpBeforeClass() { - self::$envAllow = getenv(XdebugHandlerMock::ENV_ALLOW); - self::$envIniScanDir = getenv('PHP_INI_SCAN_DIR'); + // Save current state + $names = array( + XdebugHandlerMock::ENV_ALLOW, + 'PHP_INI_SCAN_DIR', + IniHelper::ENV_ORIGINAL, + ); + + foreach ($names as $name) { + self::$env[$name] = getenv($name); + } } public static function tearDownAfterClass() { - if (false !== self::$envAllow) { - putenv(XdebugHandlerMock::ENV_ALLOW.'='.self::$envAllow); - } else { - putenv(XdebugHandlerMock::ENV_ALLOW); - } - - if (false !== self::$envIniScanDir) { - putenv('PHP_INI_SCAN_DIR='.self::$envIniScanDir); - } else { - putenv('PHP_INI_SCAN_DIR'); + // Restore original state + foreach (self::$env as $name => $value) { + if (false !== $value) { + putenv($name.'='.$value); + } else { + putenv($name); + } } } protected function setUp() { + // Ensure env is unset putenv(XdebugHandlerMock::ENV_ALLOW); putenv('PHP_INI_SCAN_DIR'); + putenv(IniHelper::ENV_ORIGINAL); } } From c1058cf37c2ffcd7d8482347796fc5367e4784e6 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Mon, 21 Nov 2016 11:52:56 +0000 Subject: [PATCH 2/2] Fix XdebugHandler test --- tests/Composer/Test/XdebugHandlerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Composer/Test/XdebugHandlerTest.php b/tests/Composer/Test/XdebugHandlerTest.php index 35d53a317..76340fbe5 100644 --- a/tests/Composer/Test/XdebugHandlerTest.php +++ b/tests/Composer/Test/XdebugHandlerTest.php @@ -32,7 +32,7 @@ class XdebugHandlerTest extends \PHPUnit_Framework_TestCase $xdebug = new XdebugHandlerMock($loaded); $xdebug->check(); $this->assertTrue($xdebug->restarted); - $this->assertNotEquals(false, getenv(IniHelper::ENV_ORIGINAL)); + $this->assertInternalType('string', getenv(IniHelper::ENV_ORIGINAL)); } public function testNoRestartWhenNotLoaded() @@ -42,7 +42,7 @@ class XdebugHandlerTest extends \PHPUnit_Framework_TestCase $xdebug = new XdebugHandlerMock($loaded); $xdebug->check(); $this->assertFalse($xdebug->restarted); - $this->assertEquals(false, getenv(IniHelper::ENV_ORIGINAL)); + $this->assertFalse(getenv(IniHelper::ENV_ORIGINAL)); } public function testNoRestartWhenLoadedAndAllowed()