Changeset View
Changeset View
Standalone View
Standalone View
src/xsprintf/PhutilCommandString.php
<?php | <?php | ||||
final class PhutilCommandString extends Phobject { | final class PhutilCommandString extends Phobject { | ||||
private $argv; | private $argv; | ||||
private $escapingMode = false; | private $escapingMode = false; | ||||
const MODE_DEFAULT = 'default'; | const MODE_DEFAULT = 'default'; | ||||
const MODE_WIN_PASSTHRU = 'winargv'; | |||||
const MODE_WIN_CMD = 'wincmd'; | |||||
const MODE_POWERSHELL = 'powershell'; | const MODE_POWERSHELL = 'powershell'; | ||||
public function __construct(array $argv) { | public function __construct(array $argv) { | ||||
$this->argv = $argv; | $this->argv = $argv; | ||||
$this->escapingMode = self::MODE_DEFAULT; | $this->escapingMode = self::MODE_DEFAULT; | ||||
// This makes sure we throw immediately if there are errors in the | |||||
// parameters. | |||||
$this->getMaskedString(); | |||||
} | } | ||||
public function __toString() { | public function __toString() { | ||||
return $this->getMaskedString(); | return $this->getMaskedString(); | ||||
} | } | ||||
public function getUnmaskedString() { | public function getUnmaskedString() { | ||||
return $this->renderString(true); | return $this->renderString(true); | ||||
Show All 16 Lines | return xsprintf( | ||||
'mode' => $this->escapingMode, | 'mode' => $this->escapingMode, | ||||
), | ), | ||||
$this->argv); | $this->argv); | ||||
} | } | ||||
public static function escapeArgument($value, $mode) { | public static function escapeArgument($value, $mode) { | ||||
switch ($mode) { | switch ($mode) { | ||||
case self::MODE_DEFAULT: | case self::MODE_DEFAULT: | ||||
return escapeshellarg($value); | return phutil_is_windows() ? self::escapeWindowsCMD($value) : escapeshellarg($value); | ||||
hach-que: There should be `MODE_WIN_CMD` to force Windows CMD escaping for e.g. when Drydock is executing… | |||||
Not Done Inline ActionsGood point. That said where should I change to make Drydock use that mode? Also in that case does MODE_POWERSHELL become obsolete? BYK: Good point. That said where should I change to make Drydock use that mode? Also in that case… | |||||
Not Done Inline ActionsNo need to change Drydock. Adding a specific mode will just future proof this code. hach-que: No need to change Drydock. Adding a specific mode will just future proof this code. | |||||
Not Done Inline ActionsDropping CMD now (apart from the other breakage) will mean we have to come back and add it later, because you can't remotely execute without cmd. hach-que: Dropping CMD now (apart from the other breakage) will mean we have to come back and add it… | |||||
Not Done Inline Actions
I'm not sure if I understand what is behind this. Can you elaborate? BYK: > because you can't remotely execute without cmd.
I'm not sure if I understand what is behind… | |||||
case self::MODE_POWERSHELL: | case self::MODE_POWERSHELL: | ||||
return self::escapePowershell($value); | return self::escapePowershell($value); | ||||
case self::MODE_WIN_PASSTHRU: | |||||
return self::escapeWindowsArgv($value); | |||||
case self::MODE_WIN_CMD: | |||||
return self::escapeWindowsCMD($value); | |||||
default: | default: | ||||
throw new Exception(pht('Unknown escaping mode!')); | throw new Exception(pht('Unknown escaping mode!')); | ||||
} | } | ||||
} | } | ||||
/** | |||||
* Escapes a single argument to be glued together and passed into | |||||
* CreateProcess on Windows through `proc_open`. | |||||
* | |||||
* Adapted from https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ | |||||
* | |||||
* @param string The argument to be escaped | |||||
* @result string Escaped argument that can be used in a CreateProcess call | |||||
*/ | |||||
public static function escapeWindowsArgv($value) { | |||||
if (strpos($value, "\0") !== false) { | |||||
throw new UnexpectedValueException(pht("Can't pass NULL BYTE in command arguments!")); | |||||
} | |||||
// Don't quote unless we actually need to do so hopefully | |||||
// avoid problems if programs won't parse quotes properly | |||||
if ($value && !preg_match('/["[:space:]]/', $value)) { | |||||
return $value; | |||||
} | |||||
$result = '"'; | |||||
$len = strlen($value); | |||||
for ($i = 0; $i < $len; $i++) { | |||||
$numBackslashes = 0; | |||||
while ($i < $len && $value[$i] == '\\') { | |||||
$i++; | |||||
$numBackslashes++; | |||||
} | |||||
if ($i == $len) { | |||||
// Escape all backslashes, but let the terminating | |||||
// double quotation mark we add below be interpreted | |||||
// as a metacharacter. | |||||
$result .= str_repeat('\\', $numBackslashes * 2); | |||||
break; | |||||
} else if ($value[$i] == '"') { | |||||
// Escape all backslashes and the following double quotation mark. | |||||
$result .= str_repeat('\\', $numBackslashes * 2 + 1); | |||||
$result .= $value[$i]; | |||||
} else { | |||||
// Backslashes aren't special here. | |||||
$result .= str_repeat('\\', $numBackslashes); | |||||
$result .= $value[$i]; | |||||
} | |||||
} | |||||
$result .= '"'; | |||||
return $result; | |||||
} | |||||
/** | |||||
* Escapes all CMD metacharacters with a `^`. | |||||
* | |||||
* Adapted from https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ | |||||
* | |||||
* @param string The argument to be escaped | |||||
* @result string Escaped argument that can be used in CMD.exe | |||||
*/ | |||||
private static function escapeWindowsCMD($value) { | |||||
if (preg_match('/[\n\r]/', $value)) { | |||||
throw new UnexpectedValueException(pht("Can't pass line breaks to CMD.exe!")); | |||||
} | |||||
// Make sure this is CreateProcess-safe first | |||||
$value = self::escapeWindowsArgv($value); | |||||
// Now prefix all CMD meta characters with a `^` to escape them | |||||
return preg_replace('/[()%!^"<>&|]/', '^$0', $value); | |||||
} | |||||
private static function escapePowershell($value) { | private static function escapePowershell($value) { | ||||
// These escape sequences are from http://ss64.com/ps/syntax-esc.html | // These escape sequences are from http://ss64.com/ps/syntax-esc.html | ||||
// Replace backticks first. | // Replace backticks first. | ||||
$value = str_replace('`', '``', $value); | $value = str_replace('`', '``', $value); | ||||
// Now replace other required notations. | // Now replace other required notations. | ||||
$value = str_replace("\0", '`0', $value); | $value = str_replace("\0", '`0', $value); | ||||
Show All 19 Lines |
There should be MODE_WIN_CMD to force Windows CMD escaping for e.g. when Drydock is executing a command on a remote Windows host (in this scenario, phutil_is_windows returns false because the Phabricator host is Linux).