From 85a554acd7c28522241faf2e97b9541247a0d3d5 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 18 Apr 2024 09:55:03 +0200 Subject: [PATCH 01/20] Auto-close PRs on subtree-splits --- .gitattributes | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 8 +++++ .github/workflows/check-subtree-split.yml | 37 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/check-subtree-split.yml diff --git a/.gitattributes b/.gitattributes index 84c7add0..14c3c359 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4689c4da --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/check-subtree-split.yml b/.github/workflows/check-subtree-split.yml new file mode 100644 index 00000000..16be48ba --- /dev/null +++ b/.github/workflows/check-subtree-split.yml @@ -0,0 +1,37 @@ +name: Check subtree split + +on: + pull_request_target: + +jobs: + close-pull-request: + runs-on: ubuntu-latest + + steps: + - name: Close pull request + uses: actions/github-script@v6 + with: + script: | + if (context.repo.owner === "symfony") { + github.rest.issues.createComment({ + owner: "symfony", + repo: context.repo.repo, + issue_number: context.issue.number, + body: ` + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! + ` + }); + + github.rest.pulls.update({ + owner: "symfony", + repo: context.repo.repo, + pull_number: context.issue.number, + state: "closed" + }); + } From b3da76c30c3f33c21356ceeb631f8958b2b932cd Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 29 Apr 2024 16:31:15 +0200 Subject: [PATCH 02/20] Remove calls to `TestCase::iniSet()` and calls to deprecated methods of `MockBuilder` --- Tests/ExecutableFinderTest.php | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 5c63cf0f..6d089def 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -109,12 +109,16 @@ public function testFindWithOpenBaseDir() $this->markTestSkipped('Cannot test when open_basedir is set'); } - $this->iniSet('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/'); + $initialOpenBaseDir = ini_set('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/'); - $finder = new ExecutableFinder(); - $result = $finder->find($this->getPhpBinaryName()); + try { + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName()); - $this->assertSamePath(\PHP_BINARY, $result); + $this->assertSamePath(\PHP_BINARY, $result); + } finally { + ini_set('open_basedir', $initialOpenBaseDir); + } } /** @@ -130,12 +134,17 @@ public function testFindProcessInOpenBasedir() } $this->setPath(''); - $this->iniSet('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/'); - $finder = new ExecutableFinder(); - $result = $finder->find($this->getPhpBinaryName(), false); + $initialOpenBaseDir = ini_set('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/'); - $this->assertSamePath(\PHP_BINARY, $result); + try { + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName(), false); + + $this->assertSamePath(\PHP_BINARY, $result); + } finally { + ini_set('open_basedir', $initialOpenBaseDir); + } } public function testFindBatchExecutableOnWindows() From 1393de6f0688c254d8e3bc4933670a6700c2c64b Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 7 May 2024 13:49:07 -0400 Subject: [PATCH 03/20] Return false in isTtySupported() when open_basedir restrictions prevent access to /dev/tty. If open_basedir restrictions are in effect, checking if the file /dev/tty is writable will prevent setting tty mode on the process, and avoid failing to create a Process. --- Process.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Process.php b/Process.php index c804a179..bf2e8e85 100644 --- a/Process.php +++ b/Process.php @@ -1211,7 +1211,7 @@ public static function isTtySupported(): bool { static $isTtySupported; - return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)); + return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT) && @is_writable('/dev/tty')); } /** From deedcb3bb4669cae2148bc920eafd2b16dc7c046 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 31 May 2024 16:33:22 +0200 Subject: [PATCH 04/20] Revert "minor #54653 Auto-close PRs on subtree-splits (nicolas-grekas)" This reverts commit 2c9352dd91ebaf37b8a3e3c26fd8e1306df2fb73, reversing changes made to 18c3e87f1512be2cc50e90235b144b13bc347258. --- .gitattributes | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 8 ----- .github/workflows/check-subtree-split.yml | 37 ----------------------- 3 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/workflows/check-subtree-split.yml diff --git a/.gitattributes b/.gitattributes index 14c3c359..84c7add0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.git* export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 4689c4da..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,8 +0,0 @@ -Please do not submit any Pull Requests here. They will be closed. ---- - -Please submit your PR here instead: -https://github.com/symfony/symfony - -This repository is what we call a "subtree split": a read-only subset of that main repository. -We're looking forward to your PR there! diff --git a/.github/workflows/check-subtree-split.yml b/.github/workflows/check-subtree-split.yml deleted file mode 100644 index 16be48ba..00000000 --- a/.github/workflows/check-subtree-split.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Check subtree split - -on: - pull_request_target: - -jobs: - close-pull-request: - runs-on: ubuntu-latest - - steps: - - name: Close pull request - uses: actions/github-script@v6 - with: - script: | - if (context.repo.owner === "symfony") { - github.rest.issues.createComment({ - owner: "symfony", - repo: context.repo.repo, - issue_number: context.issue.number, - body: ` - Thanks for your Pull Request! We love contributions. - - However, you should instead open your PR on the main repository: - https://github.com/symfony/symfony - - This repository is what we call a "subtree split": a read-only subset of that main repository. - We're looking forward to your PR there! - ` - }); - - github.rest.pulls.update({ - owner: "symfony", - repo: context.repo.repo, - pull_number: context.issue.number, - state: "closed" - }); - } From 32354f62488486b6efcbcd61a1dc8a619287fd29 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 5 Sep 2024 18:13:22 +0200 Subject: [PATCH 05/20] Don't use is_resource() on non-streams --- Process.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index a4b0a784..62addf1e 100644 --- a/Process.php +++ b/Process.php @@ -352,7 +352,7 @@ public function start(?callable $callback = null, array $env = []) $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); - if (!\is_resource($this->process)) { + if (!$this->process) { throw new RuntimeException('Unable to launch a new process.'); } $this->status = self::STATUS_STARTED; @@ -1456,8 +1456,9 @@ private function readPipes(bool $blocking, bool $close) private function close(): int { $this->processPipes->close(); - if (\is_resource($this->process)) { + if ($this->process) { proc_close($this->process); + $this->process = null; } $this->exitcode = $this->processInformation['exitcode']; $this->status = self::STATUS_TERMINATED; From 6fd79ab51c8342aa1f3395b2e704d3aa6ac68c2c Mon Sep 17 00:00:00 2001 From: Marcus <25648755+M-arcus@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:09:10 +0200 Subject: [PATCH 06/20] PhpSubprocess: Add flag PREG_OFFSET_CAPTURE to preg_match to identify the offset --- PhpSubprocess.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PhpSubprocess.php b/PhpSubprocess.php index a97f8b26..04fd8ea8 100644 --- a/PhpSubprocess.php +++ b/PhpSubprocess.php @@ -106,7 +106,7 @@ private function writeTmpIni(array $iniFiles, string $tmpDir): string throw new RuntimeException('Unable to read ini: '.$file); } // Check and remove directives after HOST and PATH sections - if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches)) { + if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches, \PREG_OFFSET_CAPTURE)) { $data = substr($data, 0, $matches[0][1]); } From 6f16c626e9fcf3fc7ce9c79ef34432adcf792282 Mon Sep 17 00:00:00 2001 From: Jan Walther Date: Tue, 1 Aug 2023 16:37:55 +0200 Subject: [PATCH 07/20] [Process] Fix finding executables independently of open_basedir --- ExecutableFinder.php | 32 +++++++++------------- PhpExecutableFinder.php | 2 +- Tests/ExecutableFinderTest.php | 50 +++++----------------------------- 3 files changed, 21 insertions(+), 63 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index f392c962..a2f184d5 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -48,25 +48,10 @@ public function addSuffix(string $suffix) */ public function find(string $name, ?string $default = null, array $extraDirs = []) { - if (\ini_get('open_basedir')) { - $searchPath = array_merge(explode(\PATH_SEPARATOR, \ini_get('open_basedir')), $extraDirs); - $dirs = []; - foreach ($searchPath as $path) { - // Silencing against https://bugs.php.net/69240 - if (@is_dir($path)) { - $dirs[] = $path; - } else { - if (basename($path) == $name && @is_executable($path)) { - return $path; - } - } - } - } else { - $dirs = array_merge( - explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), - $extraDirs - ); - } + $dirs = array_merge( + explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), + $extraDirs + ); $suffixes = ['']; if ('\\' === \DIRECTORY_SEPARATOR) { @@ -78,9 +63,18 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { return $file; } + + if (!@is_dir($dir) && basename($dir) === $name.$suffix && @is_executable($dir)) { + return $dir; + } } } + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; + if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && is_executable($executablePath)) { + return $executablePath; + } + return $default; } } diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index 45dbcca4..54fe7443 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -36,7 +36,7 @@ public function find(bool $includeArgs = true) if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { + if (\function_exists('exec') && $php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { if (!is_executable($php)) { return false; } diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 6d089def..a1b8d6d5 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -19,20 +19,9 @@ */ class ExecutableFinderTest extends TestCase { - private $path; - protected function tearDown(): void { - if ($this->path) { - // Restore path if it was changed. - putenv('PATH='.$this->path); - } - } - - private function setPath($path) - { - $this->path = getenv('PATH'); - putenv('PATH='.$path); + putenv('PATH='.($_SERVER['PATH'] ?? $_SERVER['Path'])); } public function testFind() @@ -41,7 +30,7 @@ public function testFind() $this->markTestSkipped('Cannot test when open_basedir is set'); } - $this->setPath(\dirname(\PHP_BINARY)); + putenv('PATH='.\dirname(\PHP_BINARY)); $finder = new ExecutableFinder(); $result = $finder->find($this->getPhpBinaryName()); @@ -57,7 +46,7 @@ public function testFindWithDefault() $expected = 'defaultValue'; - $this->setPath(''); + putenv('PATH='); $finder = new ExecutableFinder(); $result = $finder->find('foo', $expected); @@ -71,7 +60,7 @@ public function testFindWithNullAsDefault() $this->markTestSkipped('Cannot test when open_basedir is set'); } - $this->setPath(''); + putenv('PATH='); $finder = new ExecutableFinder(); @@ -86,7 +75,7 @@ public function testFindWithExtraDirs() $this->markTestSkipped('Cannot test when open_basedir is set'); } - $this->setPath(''); + putenv('PATH='); $extraDirs = [\dirname(\PHP_BINARY)]; @@ -109,6 +98,7 @@ public function testFindWithOpenBaseDir() $this->markTestSkipped('Cannot test when open_basedir is set'); } + putenv('PATH='.\dirname(\PHP_BINARY)); $initialOpenBaseDir = ini_set('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/'); try { @@ -121,32 +111,6 @@ public function testFindWithOpenBaseDir() } } - /** - * @runInSeparateProcess - */ - public function testFindProcessInOpenBasedir() - { - if (\ini_get('open_basedir')) { - $this->markTestSkipped('Cannot test when open_basedir is set'); - } - if ('\\' === \DIRECTORY_SEPARATOR) { - $this->markTestSkipped('Cannot run test on windows'); - } - - $this->setPath(''); - - $initialOpenBaseDir = ini_set('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/'); - - try { - $finder = new ExecutableFinder(); - $result = $finder->find($this->getPhpBinaryName(), false); - - $this->assertSamePath(\PHP_BINARY, $result); - } finally { - ini_set('open_basedir', $initialOpenBaseDir); - } - } - public function testFindBatchExecutableOnWindows() { if (\ini_get('open_basedir')) { @@ -163,7 +127,7 @@ public function testFindBatchExecutableOnWindows() $this->assertFalse(is_executable($target)); - $this->setPath(sys_get_temp_dir()); + putenv('PATH='.sys_get_temp_dir()); $finder = new ExecutableFinder(); $result = $finder->find(basename($target), false); From 1b9fa82b5c62cd49da8c9e3952dd8531ada65096 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 17 Sep 2024 14:46:43 +0200 Subject: [PATCH 08/20] [Process] minor fix --- ExecutableFinder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index a2f184d5..6dc00b7c 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -71,7 +71,7 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && is_executable($executablePath)) { + if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && @is_executable($executablePath)) { return $executablePath; } From 95f3f19d0f8f06e4253c66a0828ddb69f8b8ede4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Sep 2024 11:24:18 +0200 Subject: [PATCH 09/20] Add PR template and auto-close PR on subtree split repositories --- .gitattributes | 3 +-- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++++ .github/workflows/close-pull-request.yml | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/close-pull-request.yml diff --git a/.gitattributes b/.gitattributes index 84c7add0..14c3c359 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4689c4da --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml new file mode 100644 index 00000000..e55b4781 --- /dev/null +++ b/.github/workflows/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! From e2d11b6ca03e3041ca2f53a4da3f16d2f8e45c5a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 Oct 2024 21:56:12 +0100 Subject: [PATCH 10/20] [Process] Fix handling empty path found in the PATH env var with ExecutableFinder --- ExecutableFinder.php | 3 +++ Tests/ExecutableFinderTest.php | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 6dc00b7c..45d91e4a 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -60,6 +60,9 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { + if ('' === $dir) { + $dir = '.'; + } if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { return $file; } diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index a1b8d6d5..c4876e47 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -111,6 +111,9 @@ public function testFindWithOpenBaseDir() } } + /** + * @runInSeparateProcess + */ public function testFindBatchExecutableOnWindows() { if (\ini_get('open_basedir')) { @@ -138,6 +141,24 @@ public function testFindBatchExecutableOnWindows() $this->assertSamePath($target.'.BAT', $result); } + /** + * @runInSeparateProcess + */ + public function testEmptyDirInPath() + { + putenv(sprintf('PATH=%s:', \dirname(\PHP_BINARY))); + + touch('executable'); + chmod('executable', 0700); + + $finder = new ExecutableFinder(); + $result = $finder->find('executable'); + + $this->assertSame('./executable', $result); + + unlink('executable'); + } + private function assertSamePath($expected, $tested) { if ('\\' === \DIRECTORY_SEPARATOR) { From 651830b1a3cbae1b58bc63c8ba75c5a735abe522 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 30 Oct 2024 22:56:41 +0100 Subject: [PATCH 11/20] [Process] Properly deal with not-found executables on Windows --- ExecutableFinder.php | 10 ++++++++-- PhpExecutableFinder.php | 16 ++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 6dc00b7c..d446bb65 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -70,8 +70,14 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } } - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && @is_executable($executablePath)) { + if (!\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { + return $default; + } + + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; + $execResult = exec(\sprintf($command, escapeshellarg($name))); + + if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) { return $executablePath; } diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index 54fe7443..b9aff690 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -35,12 +35,16 @@ public function find(bool $includeArgs = true) { if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if (\function_exists('exec') && $php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { - if (!is_executable($php)) { - return false; - } - } else { + if (!\function_exists('exec') || \strlen($php) !== strcspn($php, '/'.\DIRECTORY_SEPARATOR)) { + return false; + } + + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; + $execResult = exec(\sprintf($command, escapeshellarg($php))); + if (!$php = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) { + return false; + } + if (!is_executable($php)) { return false; } } From 46c203f382b73a2575d043e49a17073d3c808fad Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 2 Nov 2024 14:14:29 +0100 Subject: [PATCH 12/20] [Process] Return built-in cmd.exe commands directly in ExecutableFinder --- ExecutableFinder.php | 12 ++++++++++++ Tests/ExecutableFinderTest.php | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 2293595c..1604b6f0 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -20,6 +20,13 @@ class ExecutableFinder { private $suffixes = ['.exe', '.bat', '.cmd', '.com']; + private const CMD_BUILTINS = [ + 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', + 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', + 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', + 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', + 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', + ]; /** * Replaces default suffixes of executable. @@ -48,6 +55,11 @@ public function addSuffix(string $suffix) */ public function find(string $name, ?string $default = null, array $extraDirs = []) { + // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes + if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) { + return $name; + } + $dirs = array_merge( explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), $extraDirs diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index c4876e47..adb5556d 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -159,6 +159,18 @@ public function testEmptyDirInPath() unlink('executable'); } + public function testFindBuiltInCommandOnWindows() + { + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Can be only tested on windows'); + } + + $finder = new ExecutableFinder(); + $this->assertSame('rmdir', $finder->find('RMDIR')); + $this->assertSame('cd', $finder->find('cd')); + $this->assertSame('move', $finder->find('MoVe')); + } + private function assertSamePath($expected, $tested) { if ('\\' === \DIRECTORY_SEPARATOR) { From b61fb1c70392905d5f5f99824324983124a1dd08 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 4 Nov 2024 09:44:46 +0100 Subject: [PATCH 13/20] [Process] Improve test cleanup by unlinking in a `finally` block --- Tests/ExecutableFinderTest.php | 36 +++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index c4876e47..3995e73a 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -125,18 +125,20 @@ public function testFindBatchExecutableOnWindows() $target = tempnam(sys_get_temp_dir(), 'example-windows-executable'); - touch($target); - touch($target.'.BAT'); - - $this->assertFalse(is_executable($target)); + try { + touch($target); + touch($target.'.BAT'); - putenv('PATH='.sys_get_temp_dir()); + $this->assertFalse(is_executable($target)); - $finder = new ExecutableFinder(); - $result = $finder->find(basename($target), false); + putenv('PATH='.sys_get_temp_dir()); - unlink($target); - unlink($target.'.BAT'); + $finder = new ExecutableFinder(); + $result = $finder->find(basename($target), false); + } finally { + unlink($target); + unlink($target.'.BAT'); + } $this->assertSamePath($target.'.BAT', $result); } @@ -148,15 +150,17 @@ public function testEmptyDirInPath() { putenv(sprintf('PATH=%s:', \dirname(\PHP_BINARY))); - touch('executable'); - chmod('executable', 0700); - - $finder = new ExecutableFinder(); - $result = $finder->find('executable'); + try { + touch('executable'); + chmod('executable', 0700); - $this->assertSame('./executable', $result); + $finder = new ExecutableFinder(); + $result = $finder->find('executable'); - unlink('executable'); + $this->assertSame('./executable', $result); + } finally { + unlink('executable'); + } } private function assertSamePath($expected, $tested) From a56fe7b6066efd82037aedfbd1c657e3bcce1810 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 10:27:52 +0100 Subject: [PATCH 14/20] ignore case of built-in cmd.exe commands --- Tests/ExecutableFinderTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index adb5556d..e335e47c 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -166,9 +166,9 @@ public function testFindBuiltInCommandOnWindows() } $finder = new ExecutableFinder(); - $this->assertSame('rmdir', $finder->find('RMDIR')); - $this->assertSame('cd', $finder->find('cd')); - $this->assertSame('move', $finder->find('MoVe')); + $this->assertSame('rmdir', strtolower($finder->find('RMDIR'))); + $this->assertSame('cd', strtolower($finder->find('cd'))); + $this->assertSame('move', strtolower($finder->find('MoVe'))); } private function assertSamePath($expected, $tested) From 7be8366a553b0ea5ec03d01f68c2214b1ce82e89 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 10:25:02 +0100 Subject: [PATCH 15/20] fix the directory separator being used --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index c4876e47..f85d8c9a 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -146,7 +146,7 @@ public function testFindBatchExecutableOnWindows() */ public function testEmptyDirInPath() { - putenv(sprintf('PATH=%s:', \dirname(\PHP_BINARY))); + putenv(sprintf('PATH=%s%s', \dirname(\PHP_BINARY), \PATH_SEPARATOR)); touch('executable'); chmod('executable', 0700); From 81e1a0cdac68330b5acec27c427cf59be49c73f7 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 11:01:19 +0100 Subject: [PATCH 16/20] fix the path separator being used --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 4a6c2c4b..fbeb7f07 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -157,7 +157,7 @@ public function testEmptyDirInPath() $finder = new ExecutableFinder(); $result = $finder->find('executable'); - $this->assertSame('./executable', $result); + $this->assertSame(sprintf('.%sexecutable', \PATH_SEPARATOR), $result); } finally { unlink('executable'); } From 72baf6b0591f07b051450bdf2608f93fb5c0a6e5 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 11:14:40 +0100 Subject: [PATCH 17/20] fix the constant being used --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index fbeb7f07..4aadd9b2 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -157,7 +157,7 @@ public function testEmptyDirInPath() $finder = new ExecutableFinder(); $result = $finder->find('executable'); - $this->assertSame(sprintf('.%sexecutable', \PATH_SEPARATOR), $result); + $this->assertSame(sprintf('.%sexecutable', \DIRECTORY_SEPARATOR), $result); } finally { unlink('executable'); } From d94dda5a49f8e43523d6966ab705a754001d42fe Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 4 Nov 2024 11:43:26 +0100 Subject: [PATCH 18/20] [Process] Fix escaping /X arguments on Windows --- Process.php | 2 +- Tests/ProcessTest.php | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index 62addf1e..b8012dda 100644 --- a/Process.php +++ b/Process.php @@ -1638,7 +1638,7 @@ private function escapeArgument(?string $argument): string if (str_contains($argument, "\0")) { $argument = str_replace("\0", '?', $argument); } - if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) { + if (!preg_match('/[()%!^"<>&|\s]/', $argument)) { return $argument; } $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index a2e370de..e4d92874 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1424,7 +1424,12 @@ public function testGetCommandLine() { $p = new Process(['/usr/bin/php']); - $expected = '\\' === \DIRECTORY_SEPARATOR ? '"/usr/bin/php"' : "'/usr/bin/php'"; + $expected = '\\' === \DIRECTORY_SEPARATOR ? '/usr/bin/php' : "'/usr/bin/php'"; + $this->assertSame($expected, $p->getCommandLine()); + + $p = new Process(['cd', '/d']); + + $expected = '\\' === \DIRECTORY_SEPARATOR ? 'cd /d' : "'cd' '/d'"; $this->assertSame($expected, $p->getCommandLine()); } From 05c2ccc705cb0336becfdc10f6dd67896d9ba91a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 28 Oct 2024 12:35:32 +0100 Subject: [PATCH 19/20] [Process] Use %PATH% before %CD% to load the shell on Windows --- ExecutableFinder.php | 14 ++++++++------ PhpExecutableFinder.php | 15 ++------------- Process.php | 9 ++++++++- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 1604b6f0..89edd22f 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -19,7 +19,6 @@ */ class ExecutableFinder { - private $suffixes = ['.exe', '.bat', '.cmd', '.com']; private const CMD_BUILTINS = [ 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', @@ -28,6 +27,8 @@ class ExecutableFinder 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', ]; + private $suffixes = []; + /** * Replaces default suffixes of executable. */ @@ -65,11 +66,13 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ $extraDirs ); - $suffixes = ['']; + $suffixes = []; if ('\\' === \DIRECTORY_SEPARATOR) { $pathExt = getenv('PATHEXT'); - $suffixes = array_merge($pathExt ? explode(\PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes); + $suffixes = $this->suffixes; + $suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']); } + $suffixes = '' !== pathinfo($name, PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { if ('' === $dir) { @@ -85,12 +88,11 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } } - if (!\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { + if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { return $default; } - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; - $execResult = exec(\sprintf($command, escapeshellarg($name))); + $execResult = exec('command -v -- '.escapeshellarg($name)); if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) { return $executablePath; diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index b9aff690..c3a9680d 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -34,19 +34,8 @@ public function __construct() public function find(bool $includeArgs = true) { if ($php = getenv('PHP_BINARY')) { - if (!is_executable($php)) { - if (!\function_exists('exec') || \strlen($php) !== strcspn($php, '/'.\DIRECTORY_SEPARATOR)) { - return false; - } - - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; - $execResult = exec(\sprintf($command, escapeshellarg($php))); - if (!$php = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) { - return false; - } - if (!is_executable($php)) { - return false; - } + if (!is_executable($php) && !$php = $this->executableFinder->find($php)) { + return false; } if (@is_dir($php)) { diff --git a/Process.php b/Process.php index 62addf1e..0f3457f3 100644 --- a/Process.php +++ b/Process.php @@ -1592,7 +1592,14 @@ function ($m) use (&$env, &$varCache, &$varCount, $uid) { $cmd ); - $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; + static $comSpec; + + if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) { + // Escape according to CommandLineToArgvW rules + $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec) .'"'; + } + + $cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; foreach ($this->processPipes->getFiles() as $offset => $filename) { $cmd .= ' '.$offset.'>"'.$filename.'"'; } From 01906871cb9b5e3cf872863b91aba4ec9767daf4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 6 Nov 2024 10:18:28 +0100 Subject: [PATCH 20/20] [Process] Fix test --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 4aadd9b2..84e5b3c3 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -123,7 +123,7 @@ public function testFindBatchExecutableOnWindows() $this->markTestSkipped('Can be only tested on windows'); } - $target = tempnam(sys_get_temp_dir(), 'example-windows-executable'); + $target = str_replace('.tmp', '_tmp', tempnam(sys_get_temp_dir(), 'example-windows-executable')); try { touch($target);