8000 [Process] Windows: stronger escaping, disabled delayed expansion, dep… · symfony/symfony@bdd686d · GitHub
[go: up one dir, main page]

Skip to content

Commit bdd686d

Browse files
[Process] Windows: stronger escaping, disabled delayed expansion, deprecated options
1 parent f8b02ed commit bdd686d

File tree

10 files changed

+108
-71
lines changed

10 files changed

+108
-71
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ script:
9696
- if [[ ! $deps && ! $PHP = hhvm* ]]; then echo "$COMPONENTS" | parallel --gnu '$PHPUNIT --exclude-group tty,benchmark,intl-data {}'"$REPORT"; fi
9797
- if [[ ! $deps && ! $PHP = hhvm* ]]; then echo -e "\\nRunning tests requiring tty"; $PHPUNIT --group tty; fi
9898
- if [[ ! $deps && $PHP = hhvm* ]]; then $PHPUNIT --exclude-group benchmark,intl-data; fi
99-
- 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
99+
- 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
100100
- 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
101101
- 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
102102
# Test the PhpUnit bridge using the original phpunit script

src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Console\Output\StreamOutput;
1717
use Symfony\Component\Console\Helper\ProcessHelper;
1818
use Symfony\Component\Process\Process;
19+
use Symfony\Component\Process\ProcessUtils;
1920

2021
class ProcessHelperTest extends \PHPUnit_Framework_TestCase
2122
{
@@ -84,7 +85,9 @@ public function provideCommandsAndOutput()
8485

8586
$errorMessage = 'An error occurred';
8687
if ('\\' === DIRECTORY_SEPARATOR) {
87-
$successOutputProcessDebug = str_replace("'", '"', $successOutputProcessDebug);
88+
$args = array('php', '-r', 'echo 42;');
89+
$args = array_map(array(ProcessUtils::class, 'escapeArgument'), $args);
90+
$successOutputProcessDebug = str_replace("'php' '-r' 'echo 42;'", implode(' ', $args), $successOutputProcessDebug);
8891
}
8992

9093
return array(

src/Symfony/Component/Process/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
CHANGELOG
22
=========
33

4+
3.3.0
5+
-----
6+
7+
* disabled delayed expansion on Windows
8+
* added second argument to ProcessUtils::escapeArgument() for controlling the escaping context on Windows
9+
* deprecated `!VAR!` expansion allowed by ProcessUtils::escapeArgument()
10+
* deprecated configuring `proc_open()` options
11+
* deprecated configuring enhanced Windows compatibility
12+
* deprecated configuring enhanced SIGCHLD compatibility
13+
414
2.5.0
515
-----
616

src/Symfony/Component/Process/PhpProcess.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class PhpProcess extends Process
3333
* @param int $timeout The timeout in seconds
3434
* @param array $options An array of options for proc_open
3535
*/
36-
public function __construct($script, $cwd = null, array $env = null, $timeout = 60, array $options = array())
36+
public function __construct($script, $cwd = null, array $env = null, $timeout = 60, array $options = null)
3737
{
3838
$executableFinder = new PhpExecutableFinder();
3939
if (false === $php = $executableFinder->find()) {
@@ -52,6 +52,9 @@ public function __construct($script, $cwd = null, array $env = null, $timeout =
5252
// command with exec
5353
$php = 'exec '.$php;
5454
}
55+
if (null !== $options) {
56+
@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);
57+
}
5558

5659
parent::__construct($php, $cwd, $env, $script, $timeout, $options);
5760
}

src/Symfony/Component/Process/Process.php

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class Process implements \IteratorAggregate
5858
private $lastOutputTime;
5959
private $timeout;
6060
private $idleTimeout;
61-
private $options;
61+
private $options = array('suppress_errors' => true);
6262
private $exitcode;
6363
private $fallbackStatus = array();
6464
private $processInformation;
@@ -145,7 +145,7 @@ class Process implements \IteratorAggregate
145145
*
146146
* @throws RuntimeException When proc_open is not installed
147147
*/
148-
public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array())
148+
public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = null)
149149
{
150150
if (!function_exists('proc_open')) {
151151
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
171171
$this->pty = false;
172172
$this->enhanceWindowsCompatibility = true;
173173
$this->enhanceSigchildCompatibility = '\\' !== DIRECTORY_SEPARATOR && $this->isSigchildEnabled();
174-
$this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options);
174+
if (null !== $options) {
175+
@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);
176+
$this->options = $options + $this->options;
177+
}
175178
}
176179

177180
public function __destruct()
@@ -274,24 +277,23 @@ public function start(callable $callback = null)
274277
if ('\\' === DIRECTORY_SEPARATOR && !empty($this->options['bypass_shell']) && !$this->enhanceWindowsCompatibility) {
275278
throw new LogicException('The "bypass_shell" option must be false to inherit environment variables while enhanced Windows compatibility is off');
276279
}
280+
$escape = '\\' === DIRECTORY_SEPARATOR ? function ($v) { return ProcessUtils::escapeArgument($v, ProcessUtils::ESC_WINDOWS_CMD); } : 'escapeshellarg';
277281
$env = '\\' === DIRECTORY_SEPARATOR ? '(SET %s)&&' : 'export %s;';
278282
foreach ($this->env as $k => $v) {
279-
$envline .= sprintf($env, ProcessUtils::escapeArgument("$k=$v"));
283+
$envline .= sprintf($env, $escape("$k=$v"));
280284
}
281285
$env = null;
282286
} else {
283287
$env = $this->env;
284288
}
285289
if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) {
286-
$commandline = 'cmd /V:ON /E:ON /D /C "('.$envline.$commandline.')';
290+
$commandline = 'cmd /V:OFF /E:ON /D /C "('.$envline.$commandline.')';
287291
foreach ($this->processPipes->getFiles() as $offset => $filename) {
288-
$commandline .= ' '.$offset.'>'.ProcessUtils::escapeArgument($filename);
292+
$commandline .= ' '.$offset.'>'.ProcessUtils::escapeArgument($filename, ProcessUtils::ESC_WINDOWS_CMD);
289293
}
290294
$commandline .= '"';
291295

292-
if (!isset($this->options['bypass_shell'])) {
293-
$this->options['bypass_shell'] = true;
294-
}
296+
$this->options['bypass_shell'] = true;
295297
} elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
296298
// last exit code is output on the fourth pipe and caught to work around --enable-sigchild
297299
$descriptors[3] = array('pipe', 'w');
@@ -1148,9 +1150,13 @@ public function setInput($input)
11481150
* Gets the options for proc_open.
11491151
*
11501152
* @return array The current options
1153+
*
1154+
* @deprecated since version 3.3, to be removed in 4.0.
11511155
*/
11521156
public function getOptions()
11531157
{
1158+
@trigger_error(sprintf('The %s method is deprecated since version 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED);
1159+
11541160
return $this->options;
11551161
}
11561162

@@ -1160,9 +1166,13 @@ public function getOptions()
11601166
* @param array $options The new options
11611167
*
11621168
* @return self The current Process instance
1169+
*
1170+
* @deprecated since version 3.3, to be removed in 4.0.
11631171
*/
11641172
public function setOptions(array $options)
11651173
{
1174+
@trigger_error(sprintf('The %s method is deprecated since version 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED);
1175+
11661176
$this->options = $options;
11671177

11681178
return $this;
@@ -1174,9 +1184,13 @@ public function setOptions(array $options)
11741184
* This is true by default.
11751185
*
11761186
* @return bool
1187+
*
1188+
* @deprecated since version 3.3, to be removed in 4.0.
11771189
*/
11781190
public function getEnhanceWindowsCompatibility()
11791191
{
1192+
@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);
1193+
11801194
return $this->enhanceWindowsCompatibility;
11811195
}
11821196

@@ -1186,9 +1200,13 @@ public function getEnhanceWindowsCompatibility()
11861200
* @param bool $enhance
11871201
*
11881202
* @return self The current Process instance
1203+
*
1204+
* @deprecated since version 3.3, to be removed in 4.0.
11891205
*/
11901206
public function setEnhanceWindowsCompatibility($enhance)
11911207
{
1208+
@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);
1209+
11921210
$this->enhanceWindowsCompatibility = (bool) $enhance;
11931211

11941212
return $this;
@@ -1198,9 +1216,13 @@ public function setEnhanceWindowsCompatibility($enhance)
11981216
* Returns whether sigchild compatibility mode is activated or not.
11991217
*
12001218
* @return bool
1219+
*
1220+
* @deprecated since version 3.3, to be removed in 4.0.
12011221
*/
12021222
public function getEnhanceSigchildCompatibility()
12031223
{
1224+
@trigger_error(sprintf('The %s method is deprecated since version 3.3 and will be removed in 4.0. SIGCHLD compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED);
1225+
12041226
return $this->enhanceSigchildCompatibility;
12051227
}
12061228

@@ -1214,9 +1236,13 @@ public function getEnhanceSigchildCompatibility()
12141236
* @param bool $enhance
12151237
*
12161238
* @return self The current Process instance
1239+
*
1240+
* @deprecated since version 3.3, to be removed in 4.0.
12171241
*/
12181242
public function setEnhanceSigchildCompatibility($enhance)
12191243
{
1244+
@trigger_error(sprintf('The %s method is deprecated since version 3.3 and will be removed in 4.0. SIGCHLD compatibility will always be enabled.', __METHOD__), E_USER_DEPRECATED);
1245+
12201246
$this->enhanceSigchildCompatibility = (bool) $enhance;
12211247

12221248
return $this;

src/Symfony/Component/Process/ProcessBuilder.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class ProcessBuilder
2626
private $env = array();
2727
private $input;
2828
private $timeout = 60;
29-
private $options = array();
29+
private $options;
3030
private $inheritEnv = true;
3131
private $prefix = array();
3232
private $outputDisabled = false;
@@ -217,9 +217,13 @@ public function setTimeout($timeout)
217217
* @param string $value The option value
218218
*
219219
* @return $this
220+
*
221+
* @deprecated since version 3.3, to be removed in 4.0.
220222
*/
221223
public function setOption($name, $value)
222224
{
225+
@trigger_error(sprintf('The %s method is deprecated since version 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED);
226+
223227
$this->options[$name] = $value;
224228

225229
return $this;
@@ -262,12 +266,10 @@ public function getProcess()
262266
throw new LogicException('You must add() command arguments before calling getProcess().');
263267
}
264268

265-
$options = $this->options;
266-
267269
$arguments = array_merge($this->prefix, $this->arguments);
268270
$script = implode(' ', array_map(array(__NAMESPACE__.'\\ProcessUtils', 'escapeArgument'), $arguments));
269271

270-
$process = new Process($script, $this->cwd, $this->env, $this->input, $this->timeout, $options);
272+
$process = new Process($script, $this->cwd, $this->env, $this->input, $this->timeout, $this->options);
271273

272274
if ($this->inheritEnv) {
273275
$process->inheritEnvironmentVariables();

src/Symfony/Component/Process/ProcessUtils.php

Lines changed: 31 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
* ProcessUtils is a bunch of utility methods.
1818
*
1919
* This class contains static methods only and is not meant to be instantiated.
20-
*
21-
* @author Martin Hasoň <martin.hason@gmail.com>
2220
*/
2321
class ProcessUtils
2422
{
23+
const ESC_WINDOWS_ARGV = 1;
24+
const ESC_WINDOWS_CMD = 2;
25+
const ESC_WINDOWS_ARGV_CMD = 3;
26+
2527
/**
2628
* This class should not be instantiated.
2729
*/
@@ -32,46 +34,42 @@ private function __construct()
3234
/**
3335
* Escapes a string to be used as a shell argument.
3436
*
37+
* Provides a more robust method on Windows than escapeshellarg.
38+
* See https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
39+
*
3540
* @param string $argument The argument that will be escaped
41+
* @param int $mode A bitfield of self::ESC_WINDOWS_* constants to configure escaping context on Windows.
3642
*
3743
* @return string The escaped argument
3844
*/
39-
public static function escapeArgument($argument)
45+
public static function escapeArgument($argument, $mode = self::ESC_WINDOWS_ARGV_CMD)
4046
{
41-
//Fix for PHP bug #43784 escapeshellarg removes % from given string
42-
//Fix for PHP bug #49446 escapeshellarg doesn't work on Windows
43-
//@see https://bugs.php.net/bug.php?id=43784
44-
//@see https://bugs.php.net/bug.php?id=49446
45-
if ('\\' === DIRECTORY_SEPARATOR) {
46-
if ('' === $argument) {
47-
return escapeshellarg($argument);
48-
}
47+
if ('\\' !== DIRECTORY_SEPARATOR) {
48+
return escapeshellarg($argument);
49+
}
50+
if (!(self::ESC_WINDOWS_ARGV_CMD & $mode)) {
51+
throw new \InvalidArgumentException(sprintf('The $mode argument of %s must be a non-zero bitfield of self::ESC_WINDOWS_* constants.', __METHOD__));
52+
}
4953

50-
$escapedArgument = '';
51-
$quote = false;
52-
foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) {
53-
if ('"' === $part) {
54-
$escapedArgument .= '\\"';
55-
} elseif (self::isSurroundedBy($part, '%')) {
56-
// Avoid environment variable expansion
57-
$escapedArgument .= '^%"'.substr($part, 1, -1).'"^%';
58-
} else {
59-
// escape trailing backslash
60-
if ('\\' === substr($part, -1)) {
61-
$part .= '\\';
62-
}
63-
$quote = true;
64-
$escapedArgument .= $part;
65-
}
66-
}
67-
if ($quote) {
68-
$escapedArgument = '"'.$escapedArgument.'"';
69-
}
54+
if (1 === func_num_args()) {
55+
$argument = preg_replace_callback("/!([^!=\n]++)!/", function ($m) {
56+
@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 ProcessUtil::escapeArgument().', $m[0]), E_USER_DEPRECATED);
7057

71-
return $escapedArgument;
58+
return getenv($m[1]);
59+
}, $argument);
7260
}
7361

74-
return escapeshellarg($argument);
62+
if ((self::ESC_WINDOWS_ARGV & $mode) && (false !== strpbrk($argument, " \t\n\v\"") || !isset($argument[0]))) {
63+
$argument = preg_replace('/(\\\\*+)"/', '$1$1\\"', $argument);
64+
$argument = preg_replace('/(\\\\++)$/', '$1$1', $argument);
65+
$argument = '"'.$argument.'"';
66+
}
67+
68+
if (self::ESC_WINDOWS_CMD & $mode) {
69+
$argument = preg_replace('/[()%!^"<>&|]/', '^$0', $argument);
70+
}
71+
72+
return $argument;
7573
}
7674

7775
/**
@@ -111,9 +109,4 @@ public static function validateInput($caller, $input)
111109

112110
return $input;
113111
}
114-
115-
private static function isSurroundedBy($arg, $char)
116-
{
117-
return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1];
118-
}
119112
}

0 commit comments

Comments
 (0)
0