Skip to content

Commit 0ded545

Browse files
srebbrodrigoprimo
andauthored
Resolve relative paths to absolute before validation to avoid open_basedir warnings (#272)
When `installed_paths` in the PHPCS configuration contains relative paths, subsequent `composer install` runs can trigger PHP `open_basedir` restriction warnings. This happens because `is_dir()` is called directly on the relative path, which may fall outside the allowed `open_basedir` directories. This commit changes the code to resolve relative paths to absolute paths (using the PHPCS install path as the base directory) before validating them with `is_dir()` and `is_readable()`. This ensures path checks operate on fully qualified paths that respect `open_basedir` restrictions. --------- Co-authored-by: Rodrigo Primo <rodrigosprimo@gmail.com>
1 parent 467286b commit 0ded545

2 files changed

Lines changed: 257 additions & 16 deletions

File tree

src/Plugin.php

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -405,17 +405,13 @@ private function cleanInstalledPaths()
405405
{
406406
$changes = false;
407407
foreach ($this->installedPaths as $key => $path) {
408-
// This might be a relative path as well
409-
$alternativePath = realpath($this->getPHPCodeSnifferInstallPath() . \DIRECTORY_SEPARATOR . $path);
408+
// Resolve relative paths to absolute using the PHPCS install path as the base
409+
// to avoid potential open_basedir warnings from is_dir() on relative paths.
410+
if ($this->filesystem->isAbsolutePath($path) === false) {
411+
$path = realpath($this->getPHPCodeSnifferInstallPath() . \DIRECTORY_SEPARATOR . $path);
412+
}
410413

411-
if (
412-
(is_dir($path) === false || is_readable($path) === false) &&
413-
(
414-
$alternativePath === false ||
415-
is_dir($alternativePath) === false ||
416-
is_readable($alternativePath) === false
417-
)
418-
) {
414+
if ($path === false || is_dir($path) === false || is_readable($path) === false) {
419415
unset($this->installedPaths[$key]);
420416
$changes = true;
421417
}

tests/IntegrationTest/PreexistingPHPCSInstalledPathsConfigTest.php

Lines changed: 251 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,140 @@ protected function tear_down()
7373
$this->removeTestEnvironment();
7474
}
7575

76+
/**
77+
* Test correctly handling a pre-existing PHPCS configuration file which includes
78+
* a pre-set, valid relative path in `installed_paths`.
79+
*
80+
* @dataProvider dataPHPCSVersions
81+
*
82+
* @param string $phpcsVersion PHPCS version to use in this test.
83+
* This version is randomly selected from the PHPCS versions compatible
84+
* with the PHP version used in the test.
85+
*
86+
* @return void
87+
*/
88+
public function testPreexistingValidRelativeInstalledPathsConfigIsKeptIntact($phpcsVersion)
89+
{
90+
$config = $this->composerConfig;
91+
$config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion;
92+
93+
$this->writeComposerJsonFile($config, static::$tempLocalPath);
94+
95+
/*
96+
* 1. Install PHPCS and the plugin.
97+
*/
98+
$this->assertExecute(
99+
sprintf('composer install -v --working-dir=%s', escapeshellarg(static::$tempLocalPath)),
100+
0, // Expected exit code.
101+
null, // No stdout expectation.
102+
null, // No stderr expectation.
103+
'Failed to install PHPCS.'
104+
);
105+
106+
/*
107+
* 2. Create a valid standard inside the vendor directory and set it as a relative path.
108+
*
109+
* The standard is placed at "vendor/test-extra-stnd/TestExtraStnd/" so that the relative
110+
* path from the PHPCS install dir (vendor/squizlabs/php_codesniffer) is "../../test-extra-stnd".
111+
*/
112+
$extraStndInVendor = static::$tempLocalPath . '/vendor/test-extra-stnd/TestExtraStnd';
113+
$this->assertNotFalse(
114+
mkdir($extraStndInVendor, 0766, true),
115+
"Failed to create the $extraStndInVendor directory for the test"
116+
);
117+
$this->assertDirectoryExists($extraStndInVendor);
118+
119+
$this->assertNotFalse(
120+
file_put_contents(
121+
$extraStndInVendor . '/ruleset.xml',
122+
'<?xml version="1.0"?>' . \PHP_EOL
123+
. '<ruleset name="TestExtraStnd">' . \PHP_EOL
124+
. ' <description>Test standard for valid relative path testing.</description>' . \PHP_EOL
125+
. '</ruleset>' . \PHP_EOL
126+
),
127+
'Failed to create the ruleset.xml fixture file'
128+
);
129+
130+
/*
131+
* Set a valid absolute path first via --config-set, then convert it to a relative path
132+
* by editing the config file directly. This avoids potential PHPCS validation issues
133+
* with relative paths passed to --config-set.
134+
*/
135+
$absoluteStndPath = static::$tempLocalPath . '/vendor/test-extra-stnd';
136+
$relativeStndPath = '../../test-extra-stnd';
137+
138+
$command = sprintf(
139+
'"vendor/bin/phpcs" --config-set installed_paths %s',
140+
escapeshellarg($absoluteStndPath)
141+
);
142+
$result = $this->executeCliCommand($command, static::$tempLocalPath);
143+
$this->assertSame(
144+
0,
145+
$result['exitcode'],
146+
'Exitcode for "phpcs --config-set installed_paths" did not match 0'
147+
);
148+
149+
// Replace the absolute path with a relative path in the config file.
150+
$confFile = static::$tempLocalPath . '/vendor/squizlabs/php_codesniffer/CodeSniffer.conf';
151+
$confContents = file_get_contents($confFile);
152+
$this->assertNotFalse($confContents, 'Failed to read the PHPCS configuration file');
153+
$confContents = str_replace($absoluteStndPath, $relativeStndPath, $confContents);
154+
$this->assertNotFalse(
155+
file_put_contents($confFile, $confContents),
156+
'Failed to write the PHPCS configuration file'
157+
);
158+
159+
// Verify that the config contains the relative path.
160+
$result = $this->executeCliCommand('"vendor/bin/phpcs" --config-show', static::$tempLocalPath);
161+
$this->assertSame(0, $result['exitcode'], 'Exitcode for "phpcs --config-show" did not match 0 (first run)');
162+
163+
$expected = array(
164+
'/test-extra-stnd',
165+
);
166+
167+
$this->assertSame(
168+
$expected,
169+
$this->configShowToPathsArray($result['stdout']),
170+
'PHPCS configuration does not show the manually set relative installed_paths correctly'
171+
);
172+
173+
/*
174+
* 3. Install an external standard.
175+
*/
176+
$command = sprintf(
177+
'composer require --dev phpcs-composer-installer/dummy-subdir --no-ansi -v --working-dir=%s',
178+
escapeshellarg(static::$tempLocalPath)
179+
);
180+
$this->assertExecute(
181+
$command,
182+
0, // Expected exit code.
183+
'PHP CodeSniffer Config installed_paths set to ', // Expectation for stdout.
184+
null, // No stderr expectation.
185+
'Failed to install Dummy subdir standard.'
186+
);
187+
188+
/*
189+
* Verify that the valid relative path is retained and the new standard is registered correctly.
190+
*/
191+
$result = $this->executeCliCommand('"vendor/bin/phpcs" --config-show', static::$tempLocalPath);
192+
$this->assertSame(0, $result['exitcode'], 'Exitcode for "phpcs --config-show" did not match 0 (second run)');
193+
194+
$expected = array(
195+
'/phpcs-composer-installer/dummy-subdir',
196+
'/test-extra-stnd',
197+
);
198+
sort($expected, \SORT_NATURAL);
199+
200+
$this->assertSame(
201+
$expected,
202+
$this->configShowToPathsArray($result['stdout']),
203+
'Paths as updated by the plugin does not contain the expected paths'
204+
);
205+
}
206+
76207
/**
77208
* Test correctly handling a pre-existing PHPCS configuration file which includes a pre-set,
78-
* valid `installed_paths`.
209+
* valid absolute path in `installed_paths`.
79210
*
80211
* @dataProvider dataPHPCSVersions
81212
*
@@ -85,7 +216,7 @@ protected function tear_down()
85216
*
86217
* @return void
87218
*/
88-
public function testPreexistingValidInstalledPathsConfigIsKeptIntact($phpcsVersion)
219+
public function testPreexistingValidAbsoluteInstalledPathsConfigIsKeptIntact($phpcsVersion)
89220
{
90221
$config = $this->composerConfig;
91222
$config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion;
@@ -163,7 +294,7 @@ public function testPreexistingValidInstalledPathsConfigIsKeptIntact($phpcsVersi
163294

164295
/**
165296
* Test correctly handling a pre-existing PHPCS configuration file which includes both
166-
* a pre-set, valid path, as well as an invalid path in `installed_paths`.
297+
* a pre-set, valid path, as well as an invalid relative path in `installed_paths`.
167298
*
168299
* @dataProvider dataPHPCSVersions
169300
*
@@ -173,7 +304,7 @@ public function testPreexistingValidInstalledPathsConfigIsKeptIntact($phpcsVersi
173304
*
174305
* @return void
175306
*/
176-
public function testPreexistingInvalidInstalledPathsConfigIsRemoved($phpcsVersion)
307+
public function testPreexistingInvalidRelativeInstalledPathsConfigIsRemoved($phpcsVersion)
177308
{
178309
$config = $this->composerConfig;
179310
$config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion;
@@ -218,13 +349,16 @@ public function testPreexistingInvalidInstalledPathsConfigIsRemoved($phpcsVersio
218349
*/
219350
$confFile = static::$tempLocalPath . '/vendor/squizlabs/php_codesniffer/CodeSniffer.conf';
220351
$confContents = file_get_contents($confFile);
221-
$this->assertNotFalse($confContents);
352+
$this->assertNotFalse($confContents, 'Failed to read the PHPCS configuration file');
222353
$confContents = str_replace(
223354
$this->tempExtraStndsSubdir,
224355
$this->tempExtraStndsSubdir . ',path/to/somecloned-stnd',
225356
$confContents
226357
);
227-
$this->assertNotFalse(file_put_contents($confFile, $confContents));
358+
$this->assertNotFalse(
359+
file_put_contents($confFile, $confContents),
360+
'Failed to write the PHPCS configuration file'
361+
);
228362

229363
// Verify that the config contains the newly set value.
230364
$result = $this->executeCliCommand('"vendor/bin/phpcs" --config-show', static::$tempLocalPath);
@@ -277,6 +411,117 @@ public function testPreexistingInvalidInstalledPathsConfigIsRemoved($phpcsVersio
277411
);
278412
}
279413

414+
/**
415+
* Test correctly handling a pre-existing PHPCS configuration file which includes both
416+
* a pre-set, valid path, as well as an invalid absolute path in `installed_paths`.
417+
*
418+
* @dataProvider dataPHPCSVersions
419+
*
420+
* @param string $phpcsVersion PHPCS version to use in this test.
421+
* This version is randomly selected from the PHPCS versions compatible
422+
* with the PHP version used in the test.
423+
*
424+
* @return void
425+
*/
426+
public function testPreexistingInvalidAbsoluteInstalledPathsConfigIsRemoved($phpcsVersion)
427+
{
428+
$config = $this->composerConfig;
429+
$config['require-dev']['squizlabs/php_codesniffer'] = $phpcsVersion;
430+
431+
$this->writeComposerJsonFile($config, static::$tempLocalPath);
432+
433+
/*
434+
* 1. Install PHPCS and the plugin.
435+
*/
436+
$this->assertExecute(
437+
sprintf('composer install -v --working-dir=%s', escapeshellarg(static::$tempLocalPath)),
438+
0, // Expected exit code.
439+
null, // No stdout expectation.
440+
null, // No stderr expectation.
441+
'Failed to install PHPCS.'
442+
);
443+
444+
/*
445+
* 2. Set the installed_paths and verify it is registered correctly.
446+
*/
447+
$command = sprintf(
448+
'"vendor/bin/phpcs" --config-set installed_paths %s',
449+
escapeshellarg($this->tempExtraStndsPath)
450+
);
451+
$result = $this->executeCliCommand($command, static::$tempLocalPath);
452+
$this->assertSame(
453+
0,
454+
$result['exitcode'],
455+
'Exitcode for "phpcs --config-set installed_paths" did not match 0'
456+
);
457+
458+
/*
459+
* Manipulate the value of installed_paths to add an invalid absolute path.
460+
*/
461+
$confFile = static::$tempLocalPath . '/vendor/squizlabs/php_codesniffer/CodeSniffer.conf';
462+
$confContents = file_get_contents($confFile);
463+
$this->assertNotFalse($confContents, 'Failed to read the PHPCS configuration file');
464+
$confContents = str_replace(
465+
$this->tempExtraStndsSubdir,
466+
$this->tempExtraStndsSubdir . ',/nonexistent/absolute/path/to/standard',
467+
$confContents
468+
);
469+
$this->assertNotFalse(
470+
file_put_contents($confFile, $confContents),
471+
'Failed to write the PHPCS configuration file'
472+
);
473+
474+
// Verify that the config contains the newly set value.
475+
$result = $this->executeCliCommand('"vendor/bin/phpcs" --config-show', static::$tempLocalPath);
476+
$this->assertSame(0, $result['exitcode'], 'Exitcode for "phpcs --config-show" did not match 0 (first run)');
477+
478+
$expected = array(
479+
$this->tempExtraStndsPath,
480+
'/nonexistent/absolute/path/to/standard',
481+
);
482+
sort($expected, \SORT_NATURAL);
483+
484+
$this->assertSame(
485+
$expected,
486+
$this->configShowToPathsArray($result['stdout']),
487+
'PHPCS configuration does not show the manually set installed_paths correctly'
488+
);
489+
490+
/*
491+
* 3. Install an external standard.
492+
*/
493+
$command = sprintf(
494+
'composer require --dev phpcs-composer-installer/dummy-subdir --no-ansi -v --working-dir=%s',
495+
escapeshellarg(static::$tempLocalPath)
496+
);
497+
$this->assertExecute(
498+
$command,
499+
0, // Expected exit code.
500+
'PHP CodeSniffer Config installed_paths set to ', // Expectation for stdout.
501+
null, // No stderr expectation.
502+
'Failed to install Dummy subdir standard.'
503+
);
504+
505+
/*
506+
* Verify that the valid preset path is retained, that the invalid absolute path is removed
507+
* and the new standard is registered correctly.
508+
*/
509+
$result = $this->executeCliCommand('"vendor/bin/phpcs" --config-show', static::$tempLocalPath);
510+
$this->assertSame(0, $result['exitcode'], 'Exitcode for "phpcs --config-show" did not match 0 (second run)');
511+
512+
$expected = array(
513+
$this->tempExtraStndsPath,
514+
'/phpcs-composer-installer/dummy-subdir',
515+
);
516+
sort($expected, \SORT_NATURAL);
517+
518+
$this->assertSame(
519+
$expected,
520+
$this->configShowToPathsArray($result['stdout']),
521+
'Paths as updated by the plugin does not contain the expected paths'
522+
);
523+
}
524+
280525
/**
281526
* Data provider.
282527
*

0 commit comments

Comments
 (0)