diff --git a/.travis.yml b/.travis.yml index ad3d86cf026cc..c00e525ce1297 100644 --- a/.travis.yml +++ b/.travis.yml @@ -96,7 +96,7 @@ script: - if [[ ! $deps && ! $PHP = hhvm* ]]; then echo "$COMPONENTS" | parallel --gnu '$PHPUNIT --exclude-group tty,benchmark,intl-data {}'"$REPORT"; fi - if [[ ! $deps && ! $PHP = hhvm* ]]; then echo -e "\\nRunning tests requiring tty"; $PHPUNIT --group tty; fi - if [[ ! $deps && $PHP = hhvm* ]]; then $PHPUNIT --exclude-group benchmark,intl-data; fi - - if [[ ! $deps && $PHP = ${MIN_PHP%.*} ]]; then echo -e "1\\n0" | xargs -I{} sh -c 'echo "\\nPHP --enable-sigchild enhanced={}" && ENHANCE_SIGCHLD={} php-$MIN_PHP/sapi/cli/php .phpunit/phpunit-4.8/phpunit --colors=always src/Symfony/Component/Process/'; fi + - if [[ ! $deps && $PHP = ${MIN_PHP%.*} ]]; then echo -e "1\\n0" | xargs -I{} sh -c 'echo "\\nPHP --enable-sigchild enhanced={}" && SYMFONY_DEPRECATIONS_HELPER=weak ENHANCE_SIGCHLD={} php-$MIN_PHP/sapi/cli/php .phpunit/phpunit-4.8/phpunit --colors=always src/Symfony/Component/Process/'; fi - if [[ $deps = high ]]; then echo "$COMPONENTS" | parallel --gnu -j10% 'cd {}; composer update --no-progress --ansi; $PHPUNIT --exclude-group tty,benchmark,intl-data'$LEGACY"$REPORT"; fi - if [[ $deps = low ]]; then echo "$COMPONENTS" | parallel --gnu -j10% 'cd {}; composer update --no-progress --ansi --prefer-lowest --prefer-stable; $PHPUNIT --exclude-group tty,benchmark,intl-data'"$REPORT"; fi # Test the PhpUnit bridge using the original phpunit script diff --git a/UPGRADE-3.3.md b/UPGRADE-3.3.md index afd31e75ec88b..c607023e4eac0 100644 --- a/UPGRADE-3.3.md +++ b/UPGRADE-3.3.md @@ -41,6 +41,17 @@ HttpKernel * The `Psr6CacheClearer::addPool()` method has been deprecated. Pass an array of pools indexed by name to the constructor instead. +Process +------- + + * On Windows, `!VAR!` expansion inside escaped arguments is deprecated. + + * Not inheriting environment variables is deprecated. + + * Configuring `proc_open()` options is deprecated. + + * Configuring Windows and sigchild compatibility is deprecated - they will be always enabled in 4.0. + Security -------- diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index d88fc607d7a9b..793f05fe63d36 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -203,6 +203,17 @@ HttpKernel * The `Psr6CacheClearer::addPool()` method has been removed. Pass an array of pools indexed by name to the constructor instead. +Process +------- + + * On Windows, `!VAR!` variables are not expanded anymore in escaped arguments. + + * Environment variables are always inherited in sub-processes. + + * Configuring `proc_open()` options has been removed. + + * Configuring Windows and sigchild compatibility is not possible anymore - they are always enabled. + Security -------- diff --git a/src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php index 1dcddf8badd49..05aff8576b4dd 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Helper\ProcessHelper; use Symfony\Component\Process\Process; +use Symfony\Component\Process\ProcessUtils; class ProcessHelperTest extends \PHPUnit_Framework_TestCase { @@ -84,7 +85,9 @@ public function provideCommandsAndOutput() $errorMessage = 'An error occurred'; if ('\\' === DIRECTORY_SEPARATOR) { - $successOutputProcessDebug = str_replace("'", '"', $successOutputProcessDebug); + $args = array('php', '-r', 'echo 42;'); + $args = array_map(array(ProcessUtils::class, 'escapeArgument'), $args); + $successOutputProcessDebug = str_replace("'php' '-r' 'echo 42;'", implode(' ', $args), $successOutputProcessDebug); } return array( diff --git a/src/Symfony/Component/Process/CHANGELOG.md b/src/Symfony/Component/Process/CHANGELOG.md index 2f3c1beb74b7e..49567208968fb 100644 --- a/src/Symfony/Component/Process/CHANGELOG.md +++ b/src/Symfony/Component/Process/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +3.3.0 +----- + + * deprecated `!VAR!` expansion inside escaped arguments + * deprecated not inheriting environment variables + * deprecated configuring `proc_open()` options + * deprecated configuring enhanced Windows compatibility + * deprecated configuring enhanced sigchild compatibility + 2.5.0 ----- diff --git a/src/Symfony/Component/Process/PhpProcess.php b/src/Symfony/Component/Process/PhpProcess.php index 76425ceb8cdd6..a7f6d941ea04d 100644 --- a/src/Symfony/Component/Process/PhpProcess.php +++ b/src/Symfony/Component/Process/PhpProcess.php @@ -33,7 +33,7 @@ class PhpProcess extends Process * @param int $timeout The timeout in seconds * @param array $options An array of options for proc_open */ - public function __construct($script, $cwd = null, array $env = null, $timeout = 60, array $options = array()) + public function __construct($script, $cwd = null, array $env = null, $timeout = 60, array $options = null) { $executableFinder = new PhpExecutableFinder(); if (false === $php = $executableFinder->find()) { @@ -52,6 +52,9 @@ public function __construct($script, $cwd = null, array $env = null, $timeout = // command with exec $php = 'exec '.$php; } + if (null !== $options) { + @trigger_error(sprintf('The $options parameter of the %s constructor is deprecated since version 3.3 and will be removed in 4.0.', __CLASS__), E_USER_DEPRECATED); + } parent::__construct($php, $cwd, $env, $script, $timeout, $options); } diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index ffe59f0c3e856..fda53ee25e756 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -58,7 +58,7 @@ class Process implements \IteratorAggregate private $lastOutputTime; private $timeout; private $idleTimeout; - private $options; + private $options = array('suppress_errors' => true); private $exitcode; private $fallbackStatus = array(); private $processInformation; @@ -145,7 +145,7 @@ class Process implements \IteratorAggregate * * @throws RuntimeException When proc_open is not installed */ - public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) + public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = null) { if (!function_exists('proc_open')) { throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); @@ -171,7 +171,10 @@ public function __construct($commandline, $cwd = null, array $env = null, $input $this->pty = false; $this->enhanceWindowsCompatibility = true; $this->enhanceSigchildCompatibility = '\\' !== DIRECTORY_SEPARATOR && $this->isSigchildEnabled(); - $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options); + if (null !== $options) { + @trigger_error(sprintf('The $options parameter of the %s constructor is deprecated since version 3.3 and will be removed in 4.0.', __CLASS__), E_USER_DEPRECATED); + $this->options = array_replace($this->options, $options); + } } public function __destruct() @@ -268,47 +271,40 @@ public function start(callable $callback = null) $descriptors = $this->getDescriptors(); $commandline = $this->commandline; - $envline = ''; - if (null !== $this->env && $this->inheritEnv) { - if ('\\' === DIRECTORY_SEPARATOR && !empty($this->options['bypass_shell']) && !$this->enhanceWindowsCompatibility) { - throw new LogicException('The "bypass_shell" option must be false to inherit environment variables while enhanced Windows compatibility is off'); - } - $env = '\\' === DIRECTORY_SEPARATOR ? '(SET %s)&&' : 'export %s;'; - foreach ($this->env as $k => $v) { - $envline .= sprintf($env, ProcessUtils::escapeArgument("$k=$v")); + $env = $this->env; + $envBackup = array(); + if (null !== $env && $this->inheritEnv) { + foreach ($env as $k => $v) { + $envBackup[$k] = getenv($v); + putenv(false === $v || null === $v ? $k : "$k=$v"); } $env = null; - } else { - $env = $this->env; + } elseif (null !== $env) { + @trigger_error(sprintf('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', __METHOD__), E_USER_DEPRECATED); } if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) { - $commandline = 'cmd /V:ON /E:ON /D /C "('.$envline.$commandline.')'; - foreach ($this->processPipes->getFiles() as $offset => $filename) { - $commandline .= ' '.$offset.'>'.ProcessUtils::escapeArgument($filename); - } - $commandline .= '"'; - - if (!isset($this->options['bypass_shell'])) { - $this->options['bypass_shell'] = true; - } + $this->options['bypass_shell'] = true; + $commandline = $this->prepareWindowsCommandLine($commandline, $envBackup); } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors[3] = array('pipe', 'w'); // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input - $commandline = $envline.'{ ('.$this->commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; + $commandline = '{ ('.$this->commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code'; // Workaround for the bug, when PTS functionality is enabled. // @see : https://bugs.php.net/69442 $ptsWorkaround = fopen(__FILE__, 'r'); - } elseif ('' !== $envline) { - $commandline = $envline.$commandline; } $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $env, $this->options); + foreach ($envBackup as $k => $v) { + putenv(false === $v ? $k : "$k=$v"); + } + if (!is_resource($this->process)) { throw new RuntimeException('Unable to launch a new process.'); } @@ -1089,6 +1085,7 @@ public function getEnv() * * An environment variable value should be a string. * If it is an array, the variable is ignored. + * If it is false, it will be removed when env vars are otherwise inherited. * * That happens in PHP when 'argv' is registered into * the $_ENV array for instance. @@ -1106,7 +1103,7 @@ public function setEnv(array $env) $this->env = array(); foreach ($env as $key => $value) { - $this->env[$key] = (string) $value; + $this->env[$key] = $value; } return $this; @@ -1148,9 +1145,13 @@ public function setInput($input) * Gets the options for proc_open. * * @return array The current options + * + * @deprecated since version 3.3, to be removed in 4.0. */ public function getOptions() { + @trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED); + return $this->options; } @@ -1160,9 +1161,13 @@ public function getOptions() * @param array $options The new options * * @return self The current Process instance + * + * @deprecated since version 3.3, to be removed in 4.0. */ public function setOptions(array $options) { + @trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED); + $this->options = $options; return $this; @@ -1174,9 +1179,13 @@ public function setOptions(array $options) * This is true by default. * * @return bool + * + * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled. */ public function getEnhanceWindowsCompatibility() { + @trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); + return $this->enhanceWindowsCompatibility; } @@ -1186,9 +1195,13 @@ public function getEnhanceWindowsCompatibility() * @param bool $enhance * * @return self The current Process instance + * + * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled. */ public function setEnhanceWindowsCompatibility($enhance) { + @trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); + $this->enhanceWindowsCompatibility = (bool) $enhance; return $this; @@ -1198,9 +1211,13 @@ public function setEnhanceWindowsCompatibility($enhance) * Returns whether sigchild compatibility mode is activated or not. * * @return bool + * + * @deprecated since version 3.3, to be removed in 4.0. Sigchild compatibility will always be enabled. */ public function getEnhanceSigchildCompatibility() { + @trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); + return $this->enhanceSigchildCompatibility; } @@ -1214,9 +1231,13 @@ public function getEnhanceSigchildCompatibility() * @param bool $enhance * * @return self The current Process instance + * + * @deprecated since version 3.3, to be removed in 4.0. */ public function setEnhanceSigchildCompatibility($enhance) { + @trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED); + $this->enhanceSigchildCompatibility = (bool) $enhance; return $this; @@ -1231,6 +1252,10 @@ public function setEnhanceSigchildCompatibility($enhance) */ public function inheritEnvironmentVariables($inheritEnv = true) { + if (!$inheritEnv) { + @trigger_error(sprintf('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', __METHOD__), E_USER_DEPRECATED); + } + $this->inheritEnv = (bool) $inheritEnv; return $this; @@ -1240,9 +1265,13 @@ public function inheritEnvironmentVariables($inheritEnv = true) * Returns whether environment variables will be inherited or not. * * @return bool + * + * @deprecated since version 3.3, to be removed in 4.0. Environment variables will always be inherited. */ public function areEnvironmentVariablesInherited() { + @trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Environment variables will always be inherited.', __METHOD__), E_USER_DEPRECATED); + return $this->inheritEnv; } @@ -1561,6 +1590,50 @@ private function doSignal($signal, $throwException) return true; } + private function prepareWindowsCommandLine($cmd, array &$envBackup) + { + $uid = uniqid('', true); + $varCount = 0; + $varCache = array(); + $cmd = preg_replace_callback( + '/"( + [^"%!^]*+ + (?: + (?: !LF! | "(?:\^[%!^])?+" ) + [^"%!^]*+ + )++ + )"/x', + function ($m) use (&$envBackup, &$varCache, &$varCount, $uid) { + if (isset($varCache[$m[0]])) { + return $varCache[$m[0]]; + } + if (false !== strpos($value = $m[1], "\0")) { + $value = str_replace("\0", '?', $value); + } + if (false === strpbrk($value, "\"%!\n")) { + return '"'.$value.'"'; + } + + $value = str_replace(array('!LF!', '"^!"', '"^%"', '"^^"', '""'), array("\n", '!', '%', '^', '"'), $value); + $value = preg_replace('/(\\\\*)"/', '$1$1\\"', $value); + + $var = $uid.++$varCount; + putenv("$var=\"$value\""); + $envBackup[$var] = false; + + return $varCache[$m[0]] = '!'.$var.'!'; + }, + $cmd + ); + + $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $cmd .= ' '.$offset.'>"'.$filename.'"'; + } + + return $cmd; + } + /** * Ensures the process is running or terminated, throws a LogicException if the process has a not started. * diff --git a/src/Symfony/Component/Process/ProcessBuilder.php b/src/Symfony/Component/Process/ProcessBuilder.php index cc91371f4cba7..02069136c6dd5 100644 --- a/src/Symfony/Component/Process/ProcessBuilder.php +++ b/src/Symfony/Component/Process/ProcessBuilder.php @@ -26,7 +26,7 @@ class ProcessBuilder private $env = array(); private $input; private $timeout = 60; - private $options = array(); + private $options; private $inheritEnv = true; private $prefix = array(); private $outputDisabled = false; @@ -120,9 +120,13 @@ public function setWorkingDirectory($cwd) * @param bool $inheritEnv * * @return $this + * + * @deprecated since version 3.3, to be removed in 4.0. */ public function inheritEnvironmentVariables($inheritEnv = true) { + @trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED); + $this->inheritEnv = $inheritEnv; return $this; @@ -217,9 +221,13 @@ public function setTimeout($timeout) * @param string $value The option value * * @return $this + * + * @deprecated since version 3.3, to be removed in 4.0. */ public function setOption($name, $value) { + @trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED); + $this->options[$name] = $value; return $this; @@ -262,12 +270,10 @@ public function getProcess() throw new LogicException('You must add() command arguments before calling getProcess().'); } - $options = $this->options; - $arguments = array_merge($this->prefix, $this->arguments); $script = implode(' ', array_map(array(__NAMESPACE__.'\\ProcessUtils', 'escapeArgument'), $arguments)); - $process = new Process($script, $this->cwd, $this->env, $this->input, $this->timeout, $options); + $process = new Process($script, $this->cwd, $this->env, $this->input, $this->timeout, $this->options); if ($this->inheritEnv) { $process->inheritEnvironmentVariables(); diff --git a/src/Symfony/Component/Process/ProcessUtils.php b/src/Symfony/Component/Process/ProcessUtils.php index 500202e5844cd..a631fc75191cf 100644 --- a/src/Symfony/Component/Process/ProcessUtils.php +++ b/src/Symfony/Component/Process/ProcessUtils.php @@ -17,8 +17,6 @@ * ProcessUtils is a bunch of utility methods. * * This class contains static methods only and is not meant to be instantiated. - * - * @author Martin Hasoň */ class ProcessUtils { @@ -38,40 +36,30 @@ private function __construct() */ public static function escapeArgument($argument) { - //Fix for PHP bug #43784 escapeshellarg removes % from given string - //Fix for PHP bug #49446 escapeshellarg doesn't work on Windows - //@see https://bugs.php.net/bug.php?id=43784 - //@see https://bugs.php.net/bug.php?id=49446 - if ('\\' === DIRECTORY_SEPARATOR) { - if ('' === $argument) { - return escapeshellarg($argument); - } + if ('\\' !== DIRECTORY_SEPARATOR) { + return escapeshellarg($argument); + } + $argument = preg_replace_callback("/!([^!=\n]++)!/", function ($m) { + if (false !== $v = getenv($m[1])) { + @trigger_error(sprintf('Delayed variables are deprecated since Symfony 3.3 and will be left unresolved in 4.0. Resolve the %s variable before calling ProcessUtils::escapeArgument().', $m[0]), E_USER_DEPRECATED); - $escapedArgument = ''; - $quote = false; - foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { - if ('"' === $part) { - $escapedArgument .= '\\"'; - } elseif (self::isSurroundedBy($part, '%')) { - // Avoid environment variable expansion - $escapedArgument .= '^%"'.substr($part, 1, -1).'"^%'; - } else { - // escape trailing backslash - if ('\\' === substr($part, -1)) { - $part .= '\\'; - } - $quote = true; - $escapedArgument .= $part; - } - } - if ($quote) { - $escapedArgument = '"'.$escapedArgument.'"'; + return $v; } - return $escapedArgument; + return $m[0]; + }, $argument); + if ('' === $argument) { + return '""'; + } + if (false !== strpos($argument, "\0")) { + $argument = str_replace("\0", '?', $argument); + } + if (!preg_match('/[()%!^"<>&|\s]/', $argument)) { + return $argument; } + $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); - return escapeshellarg($argument); + return '"'.str_replace(array('"', '^', '%', '!', "\n"), array('""', '"^^"', '"^%"', '"^!"', '!LF!'), $argument).'"'; } /** diff --git a/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php b/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php index e229c1bea6c1e..581aa52d5f405 100644 --- a/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessBuilderTest.php @@ -15,6 +15,29 @@ class ProcessBuilderTest extends \PHPUnit_Framework_TestCase { + /** + * @group legacy + * @expectedDeprecation Delayed variables are deprecated since Symfony 3.3 and will be left unresolved in 4.0. Resolve the !FOO! variable before calling ProcessUtils::escapeArgument(). + */ + public function testDelayedVars() + { + if ('\\' !== DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test is for Windows platform only'); + } + putenv('FOO=BAR'); + + $proc = ProcessBuilder::create() + ->add('echo') + ->add('!FOO!') + ->getProcess(); + + $proc->run(); + $this->assertSame('BAR', trim($proc->getOutput())); + } + + /** + * @group legacy + */ public function testInheritEnvironmentVars() { $proc = ProcessBuilder::create() @@ -22,6 +45,13 @@ public function testInheritEnvironmentVars() ->getProcess(); $this->assertTrue($proc->areEnvironmentVariablesInherited()); + + $proc = ProcessBuilder::create() + ->add('foo') + ->inheritEnvironmentVariables(false) + ->getProcess(); + + $this->assertFalse($proc->areEnvironmentVariablesInherited()); } public function testAddEnvironmentVariables() @@ -35,12 +65,10 @@ public function testAddEnvironmentVariables() ->add('command') ->setEnv('foo', 'bar2') ->addEnvironmentVariables($env) - ->inheritEnvironmentVariables(false) ->getProcess() ; $this->assertSame($env, $proc->getEnv()); - $this->assertFalse($proc->areEnvironmentVariablesInherited()); } /** @@ -82,14 +110,14 @@ public function testPrefixIsPrependedToAllGeneratedProcess() $proc = $pb->setArguments(array('-v'))->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php" "-v"', $proc->getCommandLine()); + $this->assertEquals('/usr/bin/php -v', $proc->getCommandLine()); } else { $this->assertEquals("'/usr/bin/php' '-v'", $proc->getCommandLine()); } $proc = $pb->setArguments(array('-i'))->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php" "-i"', $proc->getCommandLine()); + $this->assertEquals('/usr/bin/php -i', $proc->getCommandLine()); } else { $this->assertEquals("'/usr/bin/php' '-i'", $proc->getCommandLine()); } @@ -102,14 +130,14 @@ public function testArrayPrefixesArePrependedToAllGeneratedProcess() $proc = $pb->setArguments(array('-v'))->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php" "composer.phar" "-v"', $proc->getCommandLine()); + $this->assertEquals('/usr/bin/php composer.phar -v', $proc->getCommandLine()); } else { $this->assertEquals("'/usr/bin/php' 'composer.phar' '-v'", $proc->getCommandLine()); } $proc = $pb->setArguments(array('-i'))->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php" "composer.phar" "-i"', $proc->getCommandLine()); + $this->assertEquals('/usr/bin/php composer.phar -i', $proc->getCommandLine()); } else { $this->assertEquals("'/usr/bin/php' 'composer.phar' '-i'", $proc->getCommandLine()); } @@ -121,7 +149,7 @@ public function testShouldEscapeArguments() $proc = $pb->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertSame('^%"path"^% "foo \\" bar" "%baz%baz"', $proc->getCommandLine()); + $this->assertSame('""^%"path"^%"" "foo "" bar" ""^%"baz"^%"baz"', $proc->getCommandLine()); } else { $this->assertSame("'%path%' 'foo \" bar' '%baz%baz'", $proc->getCommandLine()); } @@ -134,7 +162,7 @@ public function testShouldEscapeArgumentsAndPrefix() $proc = $pb->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertSame('^%"prefix"^% "arg"', $proc->getCommandLine()); + $this->assertSame('""^%"prefix"^%"" arg', $proc->getCommandLine()); } else { $this->assertSame("'%prefix%' 'arg'", $proc->getCommandLine()); } @@ -155,7 +183,7 @@ public function testShouldNotThrowALogicExceptionIfNoArgument() ->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php"', $process->getCommandLine()); + $this->assertEquals('/usr/bin/php', $process->getCommandLine()); } else { $this->assertEquals("'/usr/bin/php'", $process->getCommandLine()); } @@ -167,7 +195,7 @@ public function testShouldNotThrowALogicExceptionIfNoPrefix() ->getProcess(); if ('\\' === DIRECTORY_SEPARATOR) { - $this->assertEquals('"/usr/bin/php"', $process->getCommandLine()); + $this->assertEquals('/usr/bin/php', $process->getCommandLine()); } else { $this->assertEquals("'/usr/bin/php'", $process->getCommandLine()); } diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index c19206117e9fc..bb960c4deff72 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Pipes\PipesInterface; use Symfony\Component\Process\Process; +use Symfony\Component\Process\ProcessUtils; /** * @author Robert Schönthal @@ -1383,15 +1384,12 @@ public function testChainedProcesses() $this->assertSame('456', $p2->getOutput()); } - public function testInheritEnvEnabled() + public function testEnvIsInherited() { $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('echo serialize($_SERVER);'), null, array('BAR' => 'BAZ')); putenv('FOO=BAR'); - $this->assertSame($process, $process->inheritEnvironmentVariables(1)); - $this->assertTrue($process->areEnvironmentVariablesInherited()); - $process->run(); $expected = array('BAR' => 'BAZ', 'FOO' => 'BAR'); @@ -1400,12 +1398,16 @@ public function testInheritEnvEnabled() $this->assertEquals($expected, $env); } + /** + * @group legacy + */ public function testInheritEnvDisabled() { $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('echo serialize($_SERVER);'), null, array('BAR' => 'BAZ')); putenv('FOO=BAR'); + $this->assertSame($process, $process->inheritEnvironmentVariables(false)); $this->assertFalse($process->areEnvironmentVariablesInherited()); $process->run(); @@ -1417,6 +1419,27 @@ public function testInheritEnvDisabled() $this->assertSame($expected, $env); } + /** + * @dataProvider provideEscapeArgument + */ + public function testEscapeArgument($arg) + { + $p = new Process(self::$phpBin.' -r '.escapeshellarg('echo $argv[1];').' '.ProcessUtils::escapeArgument($arg)); + $p->run(); + + $this->assertSame($arg, $p->getOutput()); + } + + public function provideEscapeArgument() + { + yield array('a"b%c%'); + yield array('a"b^c^'); + yield array("a\nb'c"); + yield array("a^b c!"); + yield array("a!b\tc"); + yield array('a\\\\"\\"'); + } + /** * @param string $commandline * @param null|string $cwd @@ -1427,9 +1450,10 @@ public function testInheritEnvDisabled() * * @return Process */ - private function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) + private function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60) { - $process = new Process($commandline, $cwd, $env, $input, $timeout, $options); + $process = new Process($commandline, $cwd, $env, $input, $timeout); + $process->inheritEnvironmentVariables(); if (false !== $enhance = getenv('ENHANCE_SIGCHLD')) { try { diff --git a/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php b/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php index e6564cde5ba6d..8c2eb5f6af080 100644 --- a/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessUtilsTest.php @@ -27,12 +27,13 @@ public function dataArguments() { if ('\\' === DIRECTORY_SEPARATOR) { return array( - array('"\"php\" \"-v\""', '"php" "-v"'), + array('"""php"" ""-v"""', '"php" "-v"'), array('"foo bar"', 'foo bar'), - array('^%"path"^%', '%path%'), - array('"<|>\\" \\"\'f"', '<|>" "\'f'), + array('""^%"path"^%""', '%path%'), + array('"<|>"" ""\'f"', '<|>" "\'f'), array('""', ''), - array('"with\trailingbs\\\\"', 'with\trailingbs\\'), + array('with\trailingbs\\', 'with\trailingbs\\'), + array('"with\trailing bs\\\\"', 'with\trailing bs\\'), ); }