diff --git a/composer.json b/composer.json index c3ea095ab..61edf95d7 100644 --- a/composer.json +++ b/composer.json @@ -9,11 +9,11 @@ }, "require": { "php": "^7.2", - "ext-fileinfo": "*" + "ext-fileinfo": "*", + "ext-posix": "*" }, "require-dev": { - "phpunit/phpunit": "^8.5", - "ext-posix": "*" + "phpunit/phpunit": "^8.5" }, "license": "MIT", "authors": [ diff --git a/mocked-functions.php b/mocked-functions.php new file mode 100644 index 000000000..34ca30364 --- /dev/null +++ b/mocked-functions.php @@ -0,0 +1,12 @@ + - + src/ diff --git a/src/Local/LocalFilesystem.php b/src/Local/LocalFilesystem.php index e0b53c48f..389e54204 100644 --- a/src/Local/LocalFilesystem.php +++ b/src/Local/LocalFilesystem.php @@ -20,7 +20,6 @@ use League\Flysystem\UnableToSetVisibility; use League\Flysystem\UnableToUpdateFile; use League\Flysystem\UnableToWriteFile; -use League\Flysystem\UnreadableFileEncountered; use League\Flysystem\Visibility; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -34,8 +33,8 @@ use function file_exists; use function is_dir; use function is_file; +use function mkdir; use function rename; -use function rmdir; use function stream_copy_to_stream; use function unlink; @@ -177,8 +176,9 @@ public function deleteDirectory(string $prefix): void /** @var SplFileInfo $file */ foreach ($contents as $file) { - $this->guardAgainstUnreadableFileInfo($file); - $this->deleteFileInfoObject($file); + if ( ! $this->deleteFileInfoObject($file)) { + throw UnableToDeleteDirectory::atLocation($prefix, "Unable to delete file at " . $file->getPathname()); + } } unset($contents); @@ -197,25 +197,15 @@ private function listDirectoryRecursively( ); } - protected function guardAgainstUnreadableFileInfo(SplFileInfo $file) - { - if ( ! $file->isReadable()) { - $location = $file->getType() === 'link' ? $file->getPathname() : $file->getRealPath(); - throw UnreadableFileEncountered::atLocation($location); - } - } - - protected function deleteFileInfoObject(SplFileInfo $file): void + protected function deleteFileInfoObject(SplFileInfo $file): bool { switch ($file->getType()) { case 'dir': - rmdir($file->getRealPath()); - break; + return @rmdir($file->getRealPath()); case 'link': - unlink($file->getPathname()); - break; + return @unlink($file->getPathname()); default: - unlink($file->getRealPath()); + return @unlink($file->getRealPath()); } } @@ -232,7 +222,7 @@ public function listContents(string $path, bool $recursive): Generator foreach ($iterator as $fileInfo) { if ($fileInfo->isLink()) { - if ($this->linkHandling & self::DISALLOW_LINKS) { + if ($this->linkHandling & self::SKIP_LINKS) { continue; } throw SymbolicLinkEncountered::atLocation($fileInfo->getPathname()); @@ -296,8 +286,16 @@ public function fileExists(string $location): bool return is_file($location); } - public function createDirectory(string $location, Config $config): void + public function createDirectory(string $path, Config $config): void { + $location = $this->prefixer->prefixPath($path); + $visibility = $config->get('visibility', $config->get('directory_visibilty')); + $permissions = $this->resolveDirectoryVisibility($visibility); + error_clear_last(); + + if ( ! @mkdir($location, $permissions, true)) { + throw UnableToCreateDirectory::atLocation($path, error_get_last()['message'] ?? ''); + } } public function setVisibility(string $location, $visibility): void diff --git a/src/Local/LocalFilesystemTest.php b/src/Local/LocalFilesystemTest.php index 23a955f9a..f2ce89bb9 100644 --- a/src/Local/LocalFilesystemTest.php +++ b/src/Local/LocalFilesystemTest.php @@ -6,7 +6,9 @@ use League\Flysystem\Config; use League\Flysystem\StorageAttributes; +use League\Flysystem\SymbolicLinkEncountered; use League\Flysystem\UnableToCreateDirectory; +use League\Flysystem\UnableToDeleteDirectory; use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToSetVisibility; use League\Flysystem\UnableToUpdateFile; @@ -14,14 +16,21 @@ use League\Flysystem\Visibility; use PHPUnit\Framework\TestCase; +use function array_shift; use function file_get_contents; use function file_put_contents; use function fileperms; use function fwrite; use function getenv; use function is_dir; +use function is_string; use function iterator_to_array; +use function mkdir; use function rewind; +use function substr; +use function symlink; + +use const LOCK_EX; class LocalFilesystemTest extends TestCase { @@ -29,11 +38,13 @@ class LocalFilesystemTest extends TestCase protected function setUp(): void { + reset_function_mocks(); $this->deleteDirectory(static::ROOT); } protected function tearDown(): void { + reset_function_mocks(); $this->deleteDirectory(static::ROOT); } @@ -150,8 +161,7 @@ public function writing_a_file_with_a_stream_and_visibility() public function writing_a_file_with_visibility() { $adapter = new LocalFilesystem( - static::ROOT, - new PublicAndPrivateVisibilityInterpreting() + static::ROOT, new PublicAndPrivateVisibilityInterpreting() ); $adapter->write('/file.txt', 'contents', new Config(['visibility' => 'private'])); $this->assertFileContains(static::ROOT . '/file.txt', 'contents'); @@ -253,10 +263,7 @@ public function deleting_a_file_that_does_not_exist() */ public function deleting_a_file_that_cannot_be_deleted() { - if (posix_getuid() === 0 || getenv('FLYSYSTEM_TEST_DELETE_FAILURE') !== 'yes') { - $this->markTestSkipped('Skipping this out of precaution.'); - } - + $this->maybeSkipDangerousTests(); $this->expectException(UnableToDeleteFile::class); $adapter = new LocalFilesystem('/'); $adapter->delete('/etc/hosts'); @@ -290,7 +297,7 @@ public function listing_contents() { $adapter = new LocalFilesystem(static::ROOT); $adapter->write('directory/filename.txt', 'content', new Config()); - $adapter->write('filename.txt', 'content' , new Config()); + $adapter->write('filename.txt', 'content', new Config()); $contents = iterator_to_array($adapter->listContents('/', false)); $this->assertCount(2, $contents); @@ -304,7 +311,7 @@ public function listing_contents_recursively() { $adapter = new LocalFilesystem(static::ROOT); $adapter->write('directory/filename.txt', 'content', new Config()); - $adapter->write('filename.txt', 'content' , new Config()); + $adapter->write('filename.txt', 'content', new Config()); $contents = iterator_to_array($adapter->listContents('/', true)); $this->assertCount(3, $contents); @@ -322,6 +329,106 @@ public function listing_a_non_existing_directory() $this->assertCount(0, $contents); } + /** + * @test + */ + public function listing_directory_contents_with_link_skipping() + { + $adapter = new LocalFilesystem(static::ROOT, null, LOCK_EX, LocalFilesystem::SKIP_LINKS); + file_put_contents(static::ROOT . '/file.txt', 'content'); + symlink(static::ROOT . '/file.txt', static::ROOT . '/link.txt'); + + $contents = iterator_to_array($adapter->listContents('/', true)); + + $this->assertCount(1, $contents); + } + + /** + * @test + */ + public function listing_directory_contents_with_disallowing_links() + { + $this->expectException(SymbolicLinkEncountered::class); + $adapter = new LocalFilesystem(static::ROOT, null, LOCK_EX, LocalFilesystem::DISALLOW_LINKS); + file_put_contents(static::ROOT . '/file.txt', 'content'); + symlink(static::ROOT . '/file.txt', static::ROOT . '/link.txt'); + + $adapter->listContents('/', true)->next(); + } + + /** + * @test + */ + public function deleting_a_directory() + { + $adapter = new LocalFilesystem(static::ROOT); + mkdir(static::ROOT . '/directory/subdir/', 0744, true); + $this->assertDirectoryExists(static::ROOT . '/directory/subdir/'); + file_put_contents(static::ROOT . '/directory/subdir/file.txt', 'content'); + symlink(static::ROOT . '/directory/subdir/file.txt', static::ROOT . '/directory/subdir/link.txt'); + $adapter->deleteDirectory('directory/subdir'); + $this->assertDirectoryNotExists(static::ROOT . '/directory/subdir/'); + $adapter->deleteDirectory('directory'); + $this->assertDirectoryNotExists(static::ROOT . '/directory/'); + } + + /** + * @test + */ + public function deleting_a_non_existing_directory() + { + $adapter = new LocalFilesystem(static::ROOT); + $adapter->deleteDirectory('/non-existing-directory/'); + $this->assertTrue(true); + } + + /** + * @test + */ + public function not_being_able_to_delete_a_directory() + { + $this->expectException(UnableToDeleteDirectory::class); + + mock_function('rmdir', false); + + $adapter = new LocalFilesystem(static::ROOT); + $adapter->createDirectory('/etc/', new Config()); + $adapter->deleteDirectory('/etc/'); + } + + /** + * @test + */ + public function not_being_able_to_delete_a_sub_directory() + { + $this->expectException(UnableToDeleteDirectory::class); + + mock_function('rmdir', false); + + $adapter = new LocalFilesystem(static::ROOT); + $adapter->createDirectory('/etc/subdirectory/', new Config()); + $adapter->deleteDirectory('/etc/'); + } + + /** + * @test + */ + public function creating_a_directory() + { + $adapter = new LocalFilesystem(static::ROOT); + $adapter->createDirectory('public', new Config(['visibility' => 'public'])); + $this->assertDirectoryExists(static::ROOT . '/public'); + $this->assertFileHasPermissions(static::ROOT . '/public', 0755); + + $adapter->createDirectory('private', new Config(['visibility' => 'private'])); + $this->assertDirectoryExists(static::ROOT . '/private'); + $this->assertFileHasPermissions(static::ROOT . '/private', 0700); + + $adapter->createDirectory('also_private', new Config(['directory_visibility' => 'private'])); + $this->assertDirectoryExists(static::ROOT . '/also_private'); + $this->assertFileHasPermissions(static::ROOT . '/also_private', 0700); + } + private function streamWithContents(string $contents) { $stream = fopen('php://temp', 'w+b'); @@ -352,4 +459,13 @@ private function assertFileContains(string $file, string $expectedContents): voi $contents = file_get_contents($file); $this->assertEquals($expectedContents, $contents); } + + private function maybeSkipDangerousTests(): void + { + if (posix_getuid() === 0 || getenv('FLYSYSTEM_TEST_DANGEROUS_THINGS') !== 'yes') { + $this->markTestSkipped( + 'Skipping this out of precaution. Use FLYSYSTEM_TEST_DANGEROUS_THINGS=yes to test them' + ); + } + } } diff --git a/src/Local/PublicAndPrivateVisibilityInterpreting.php b/src/Local/PublicAndPrivateVisibilityInterpreting.php index ce6d9abf6..e9ea5743f 100644 --- a/src/Local/PublicAndPrivateVisibilityInterpreting.php +++ b/src/Local/PublicAndPrivateVisibilityInterpreting.php @@ -38,7 +38,7 @@ public function __construct( int $filePublic = 0644, int $filePrivate = 0600, int $directoryPublic = 0755, - int $directoryPrivate = 0744, + int $directoryPrivate = 0700, string $defaultForDirectories = Visibility::PRIVATE ) { $this->filePublic = $filePublic; diff --git a/test-functions.php b/test-functions.php new file mode 100644 index 000000000..ecc3d5777 --- /dev/null +++ b/test-functions.php @@ -0,0 +1,28 @@ +