From b97e4a2f027f9c5185bbd4184f81009aca50a01a Mon Sep 17 00:00:00 2001 From: Louis Charette Date: Tue, 24 Oct 2023 22:35:22 -0400 Subject: [PATCH] Fix issue with Stream fopen would fail to create a file if it didn't exist in write mode. However touch was working for same file Locator returned null because it couldn't find the file to be created All can't be set on read because you do want to read across all sprinkle In write mode, you'll write to the first location anyways --- .../StreamWrapper/Stream.php | 13 +++- .../StreamWrapper/StreamTest.php | 60 ++++++++++++++++++- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/UniformResourceLocator/StreamWrapper/Stream.php b/src/UniformResourceLocator/StreamWrapper/Stream.php index 84aa56e7..3d104cfc 100644 --- a/src/UniformResourceLocator/StreamWrapper/Stream.php +++ b/src/UniformResourceLocator/StreamWrapper/Stream.php @@ -41,7 +41,14 @@ public static function setLocator(ResourceLocatorInterface $locator): void */ public function stream_open(string $uri, string $mode, int $options, ?string &$opened_path): bool { - $path = $this->findPath($uri); + // In write mode, we want to write to the first existing path (should + // be a shared location) because fopen will attempts to create the file. + // Otherwise, we need to find the first found path, across location. + if (in_array($mode, ['w', 'w+', 'a', 'a+', 'x', 'x+'], true)) { + $path = $this->findPath($uri, true); + } else { + $path = $this->findPath($uri); + } if ($path === null) { return false; @@ -49,8 +56,8 @@ public function stream_open(string $uri, string $mode, int $options, ?string &$o $handle = @fopen($path, $mode); - // fopen will return false if mode is 'x' and file already exist. - // See : https://www.php.net/manual/en/function.fopen + // fopen will return false if file is not found or if mode is 'x' and + // file already exist. See : https://www.php.net/manual/en/function.fopen if ($handle === false) { return false; } diff --git a/tests/UniformResourceLocator/StreamWrapper/StreamTest.php b/tests/UniformResourceLocator/StreamWrapper/StreamTest.php index 57b790bd..1059da66 100644 --- a/tests/UniformResourceLocator/StreamWrapper/StreamTest.php +++ b/tests/UniformResourceLocator/StreamWrapper/StreamTest.php @@ -162,17 +162,73 @@ public function testChmod(): void } /** + * Test on a file that already exist. * @depends testSimpleFile */ public function testFOpen(): void + { + touch($this->file); + $this->assertTrue(file_exists($this->file)); // Make sure file exist, it's the point of the test + $this->assertIsResource(fopen($this->file, 'w')); + unlink($this->file); + } + + /** + * Make sure fopen create the file if it doesn't exist. + * @depends testSimpleFile + */ + public function testFOpenFileNotExistButWillCreate(): void + { + $this->assertFalse(file_exists($this->file)); + $this->assertIsResource(fopen($this->file, 'w')); + $this->assertTrue(file_exists($this->file)); + unlink($this->file); + } + + /** + * Make sure fopen will return false if the file doesn't exist in readonly + * mode. Even if locator return a resource path (with all flag). + * + * @depends testSimpleFile + */ + public function testFOpenFileNotExist(): void { // stream_open will trigger an error even if the stream return false. + // fopen(bar://test.txt): Failed to open stream: "UserFrosting\UniformResourceLocator\StreamWrapper\Stream::stream_open" call failed + // This suppress this error, and allow us to test the return value. set_error_handler(function ($no, $str, $file, $line) { // @phpstan-ignore-line }); - touch($this->file); // Touch basic file + $this->assertFalse(file_exists($this->file)); + $this->assertFalse(fopen($this->file, 'r')); + $this->assertFalse(file_exists($this->file)); + } + + /** + * @depends testSimpleFile + */ + public function testFOpenInvalidPath(): void + { + // Catch exception + set_error_handler(function ($no, $str, $file, $line) { // @phpstan-ignore-line + }); + + $this->assertFalse(fopen('bar://test.txt', 'w')); + } + + /** + * fopen will return false if mode is 'x' and file already exist. + * @depends testSimpleFile + */ + public function testFOpenFalseReturn(): void + { + // Catch exception + set_error_handler(function ($no, $str, $file, $line) { // @phpstan-ignore-line + }); + + touch($this->file); $this->assertFalse(fopen($this->file, 'x')); - unlink($this->file); // Reset state + unlink($this->file); } /**