diff --git a/docs/book/v3/migration/v2-to-v3.md b/docs/book/v3/migration/v2-to-v3.md index 802eebde..92ef8670 100644 --- a/docs/book/v3/migration/v2-to-v3.md +++ b/docs/book/v3/migration/v2-to-v3.md @@ -131,6 +131,23 @@ Additionally, `$options['pattern']` _must_ be provided at construction time or a Exceptions for invalid or empty patterns are now thrown during construct rather than when the filter is invoked. +#### `RealPath` + +The following methods have been removed: + +- `setExists` +- `getExists` + +The constructor now only accepts an associative array of [documented options](../standard-filters.md#realPath). + +`false` is no longer returned when the path must exist and does not. +Instead, the original value is returned. +Filters are not intended to provide validation. +So, to check if the path exists, ensure a validator (such as `Laminas\Validator\File\Exists') is also used. + +Windows support has been dropped. +Which in some cases may now need a custom filter to handle Windows specific issues. + #### `SeparatorToCamelCase` The constructor now only accepts an associative array of [documented options](../word.md#separatorToCamelCase). diff --git a/docs/book/v3/standard-filters.md b/docs/book/v3/standard-filters.md index df554c3b..9723ef17 100644 --- a/docs/book/v3/standard-filters.md +++ b/docs/book/v3/standard-filters.md @@ -1125,8 +1125,7 @@ For more complex usage, read the ## RealPath -This filter will resolve given links and pathnames, and returns the canonicalized -absolute pathnames. +This filter will resolve given links and pathnames, and returns the canonicalized absolute pathnames. ### Supported Options @@ -1137,13 +1136,12 @@ The following options are supported for `Laminas\Filter\RealPath`: ### Basic Usage -For any given link or pathname, its absolute path will be returned. References -to `/./`, `/../` and extra `/` sequences in the input path will be stripped. The -resulting path will not have any symbolic links, `/./`, or `/../` sequences. +For any given link or pathname, its absolute path will be returned. +References to `/./`, `/../` and extra `/` sequences in the input path will be stripped. +The resulting path will not have any symbolic links, `/./`, or `/../` sequences. -`Laminas\Filter\RealPath` will return `FALSE` on failure, e.g. if the file does not exist. On BSD -systems `Laminas\Filter\RealPath` doesn't fail if only the last path component doesn't exist, while -other systems will return `FALSE`. +`Laminas\Filter\RealPath` will return the value passed to the filter on failure, e.g. if the file does not exist. +On BSD systems `Laminas\Filter\RealPath` doesn't fail if only the last path component doesn't exist, while other systems will return the value passed to the filter. ```php $filter = new Laminas\Filter\RealPath(); @@ -1155,12 +1153,11 @@ $filtered = $filter->filter($path); ### Non-Existing Paths -Sometimes it is useful to get paths to files that do n0t exist; e.g., when you -want to get the real path for a path you want to create. You can then either -provide a `FALSE` `exists` value at initiation, or use `setExists()` to set it. +Sometimes it is useful to get paths to files that do not exist; e.g., when you want to get the real path for a path you want to create. +You can then provide `false` for the `exists` option during construction. ```php -$filter = new Laminas\Filter\RealPath(false); +$filter = new Laminas\Filter\RealPath(['exists' => false]); $path = '/www/var/path/../../non/existing/path'; $filtered = $filter->filter($path); diff --git a/psalm-baseline.xml b/psalm-baseline.xml index b0a966b6..d6c7d4e2 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -24,6 +24,7 @@ + @@ -536,27 +537,6 @@ - - - - - - options]]> - true, - ]]]> - - - - - - - - - - - - @@ -828,10 +808,9 @@ - - - + + diff --git a/src/RealPath.php b/src/RealPath.php index dff15b3c..7992bd0c 100644 --- a/src/RealPath.php +++ b/src/RealPath.php @@ -4,122 +4,58 @@ namespace Laminas\Filter; -use Laminas\Stdlib\ErrorHandler; -use Traversable; - use function array_pop; use function explode; +use function file_exists; use function getcwd; use function implode; use function is_string; -use function preg_match; -use function preg_replace; use function realpath; use function str_starts_with; -use function stripos; -use function substr; use const DIRECTORY_SEPARATOR; -use const PHP_OS; /** * @psalm-type Options = array{ - * exists: bool, - * ... + * exists?: bool, * } - * @template TOptions of Options - * @extends AbstractFilter + * @implements FilterInterface */ -final class RealPath extends AbstractFilter +final class RealPath implements FilterInterface { - /** @var TOptions $options */ - protected $options = [ - 'exists' => true, - ]; - - /** - * @param bool|Traversable|Options $existsOrOptions Options to set - */ - public function __construct($existsOrOptions = true) - { - if ($existsOrOptions !== null) { - if (! static::isOptions($existsOrOptions)) { - $this->setExists($existsOrOptions); - } else { - $this->setOptions($existsOrOptions); - } - } - } + private readonly bool $pathMustExist; - /** - * Sets if the path has to exist - * TRUE when the path must exist - * FALSE when not existing paths can be given - * - * @param bool $flag Path must exist - * @return self - */ - public function setExists($flag = true) + /** @param Options $options */ + public function __construct(array $options = []) { - $this->options['exists'] = (bool) $flag; - return $this; + $this->pathMustExist = $options['exists'] ?? true; } - /** - * Returns true if the filtered path must exist - * - * @return bool - */ - public function getExists() - { - return $this->options['exists']; - } - - /** - * Defined by Laminas\Filter\FilterInterface - * - * Returns realpath($value) - * - * If the value provided is non-scalar, the value will remain unfiltered - * - * @psalm-return ($value is string ? string : mixed) - */ public function filter(mixed $value): mixed { if (! is_string($value)) { return $value; } - $path = (string) $value; - if ($this->options['exists']) { - return realpath($path); + if ($this->pathMustExist && ! file_exists($value)) { + return $value; } - ErrorHandler::start(); - $realpath = realpath($path); - ErrorHandler::stop(); - if ($realpath !== false) { - return $realpath; + $realPath = realpath($value); + + if ($realPath !== false) { + return $realPath; } - $drive = ''; - if (stripos(PHP_OS, 'WIN') === 0) { - $path = preg_replace('/[\\\\\/]/', DIRECTORY_SEPARATOR, $path); - if (preg_match('/([a-zA-Z]\:)(.*)/', $path, $matches)) { - [, $drive, $path] = $matches; - } else { - $cwd = getcwd(); - $drive = substr($cwd, 0, 2); - if (! str_starts_with($path, DIRECTORY_SEPARATOR)) { - $path = substr($cwd, 3) . DIRECTORY_SEPARATOR . $path; - } - } - } elseif (! str_starts_with($path, DIRECTORY_SEPARATOR)) { + $path = $value; + + if (! str_starts_with($path, DIRECTORY_SEPARATOR)) { $path = getcwd() . DIRECTORY_SEPARATOR . $path; } $stack = []; $parts = explode(DIRECTORY_SEPARATOR, $path); + foreach ($parts as $dir) { if ($dir !== '' && $dir !== '.') { if ($dir === '..') { @@ -130,6 +66,11 @@ public function filter(mixed $value): mixed } } - return $drive . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $stack); + return DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $stack); + } + + public function __invoke(mixed $value): mixed + { + return $this->filter($value); } } diff --git a/test/RealPathTest.php b/test/RealPathTest.php index a5a2d5ea..943c7e6e 100644 --- a/test/RealPathTest.php +++ b/test/RealPathTest.php @@ -9,69 +9,83 @@ use PHPUnit\Framework\TestCase; use stdClass; +use function dirname; +use function getcwd; use function str_contains; -use const DIRECTORY_SEPARATOR; use const PHP_OS; class RealPathTest extends TestCase { - private RealPathFilter $filter; + public static function returnExistingFilePathDataProvider(): array + { + return [ + [__DIR__ . '/_files/file.1'], + [__DIR__ . '/_files/../_files/file.1'], + [__DIR__ . '/_files/././file.1'], + [__DIR__ . '///_files///file.1'], + ]; + } - public function setUp(): void + #[DataProvider('returnExistingFilePathDataProvider')] + public function testExistingFileReturnsRealPath(string $filePath): void { - $this->filter = new RealPathFilter(); + $filter = new RealPathFilter(); + + $result = $filter->filter($filePath); + + self::assertSame(__DIR__ . '/_files/file.1', $result); } - /** - * Ensures expected behavior for existing file - */ - public function testFileExists(): void + public function testPathWithNonExistingPartsButRealResolutionIsNotValid(): void { - $filename = __DIR__ . '/_files/file.1'; - $result = $this->filter->filter($filename); - self::assertStringContainsString($filename, $result); + $filter = new RealPathFilter(); + + $path = __DIR__ . '/_files/foo/../bar/../file.1'; + + $result = $filter->filter($path); + + self::assertSame($path, $result); } - /** - * Ensures expected behavior for nonexistent file - */ - public function testFileNonexistent(): void + public function testNonexistentFileReturnsValuePassedToFilter(): void { + $filter = new RealPathFilter(); + $path = '/path/to/nonexistent'; + self::assertSame($path, $filter->filter($path)); + } + + public function testBSDAllowsLastPortionToNotExist(): void + { + $filter = new RealPathFilter(); + + $path = './nonexistent'; + if (str_contains(PHP_OS, 'BSD')) { - self::assertSame($path, $this->filter->filter($path)); + self::assertSame(getcwd() . '/nonexistent', $filter($path)); } else { - self::assertSame(false, $this->filter->filter($path)); + self::assertSame($path, $filter($path)); } } - public function testGetAndSetExistsParameter(): void + public static function returnNonExistentPathDataProvider(): array { - self::assertTrue($this->filter->getExists()); - $this->filter->setExists(false); - self::assertFalse($this->filter->getExists()); - - $this->filter->setExists(['unknown']); - self::assertTrue($this->filter->getExists()); + return [ + ['/nonexistent/absolute/path', '/nonexistent/absolute/path'], + ['/nonexistent/absolute/extra///slashes', '/nonexistent/absolute/extra/slashes'], + ['./nonexistent/relative/path', getcwd() . '/nonexistent/relative/path'], + ['./dropped/parts/../../path', getcwd() . '/path'], + ['../relative/from/parent', dirname(getcwd()) . '/relative/from/parent'], + ]; } - public function testNonExistentPath(): void + #[DataProvider('returnNonExistentPathDataProvider')] + public function testNonExistentPathAllowed(string $path, string $expectedPath): void { - $filter = $this->filter; - $filter->setExists(false); - - $path = __DIR__ . DIRECTORY_SEPARATOR . '_files'; - self::assertSame($path, $filter($path)); - - $path2 = __DIR__ . DIRECTORY_SEPARATOR . '_files' - . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '_files'; - self::assertSame($path, $filter($path2)); + $filter = new RealPathFilter(['exists' => false]); - $path3 = __DIR__ . DIRECTORY_SEPARATOR . '_files' - . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '.' - . DIRECTORY_SEPARATOR . '_files'; - self::assertSame($path, $filter($path3)); + self::assertSame($expectedPath, $filter($path)); } /** @return list */