baseDir = sys_get_temp_dir().'/davis-webdav-'.bin2hex(random_bytes(4)); mkdir($this->baseDir.'/tmp', 0777, true); mkdir($this->baseDir.'/public', 0777, true); $this->setEnv('WEBDAV_ENABLED', 'true'); $this->setEnv('WEBDAV_TMP_DIR', $this->baseDir.'/tmp'); $this->setEnv('WEBDAV_PUBLIC_DIR', $this->baseDir.'/public'); $this->setEnv('WEBDAV_PUBLIC_DIR_WRITABLE', 'false'); } protected function tearDown(): void { parent::tearDown(); (new Filesystem())->remove($this->baseDir); } private function setEnv(string $name, string $value): void { $_ENV[$name] = $_SERVER[$name] = $value; putenv($name.'='.$value); } public function testWebdavIsMounted(): void { $client = static::createClient(); static::requestDav($client, 'PROPFIND', '/dav/public/', 'test_user:password'); $this->assertResponseStatusCodeSame(207); // The headers sabre sets must survive the conversion to a Symfony response $this->assertResponseHeaderSame('Content-Type', 'application/xml; charset=utf-8'); } /** * Regression test: temporary files (.DS_Store, Thumbs.db, ...) are stored outside the * DAV tree, so the ACL plugin never saw them and anybody could write, read and delete * them without authenticating. */ public function testAnonymousCannotStoreTemporaryFiles(): void { $client = static::createClient(); static::requestDav($client, 'PUT', '/dav/public/.DS_Store'); $this->assertResponseStatusCodeSame(401); $this->assertSame([], glob($this->baseDir.'/tmp/sabredav_*'), 'No temporary file must have been written'); } public function testAnonymousCannotReadOrDeleteTemporaryFiles(): void { $client = static::createClient(); static::requestDav($client, 'PUT', '/dav/public/.DS_Store', 'test_user:password'); $this->assertResponseStatusCodeSame(201); $this->assertCount(1, glob($this->baseDir.'/tmp/sabredav_*') ?: []); static::requestDav($client, 'GET', '/dav/public/.DS_Store'); $this->assertResponseStatusCodeSame(401); static::requestDav($client, 'DELETE', '/dav/public/.DS_Store'); $this->assertResponseStatusCodeSame(401); $this->assertCount(1, glob($this->baseDir.'/tmp/sabredav_*') ?: [], 'The temporary file must still be there'); } public function testAuthenticatedUserCanUseTemporaryFiles(): void { $client = static::createClient(); static::requestDav($client, 'PUT', '/dav/public/.DS_Store', 'test_user:password'); $this->assertResponseStatusCodeSame(201); static::requestDav($client, 'GET', '/dav/public/.DS_Store', 'test_user:password'); $this->assertResponseStatusCodeSame(200); $this->assertSame('true', $client->getResponse()->headers->get('X-Sabre-Temp')); static::requestDav($client, 'DELETE', '/dav/public/.DS_Store', 'test_user:password'); $this->assertResponseStatusCodeSame(204); $this->assertSame([], glob($this->baseDir.'/tmp/sabredav_*')); } public function testWebdavDirectoryInsideTheWebRootIsRefused(): void { $client = static::createClient(); $webRootDir = \dirname(__DIR__, 2).'/public/webdav-test-'.bin2hex(random_bytes(4)); mkdir($webRootDir); $this->setEnv('WEBDAV_PUBLIC_DIR', $webRootDir); try { static::requestDav($client, 'PROPFIND', '/dav/public/', 'test_user:password'); $this->assertResponseStatusCodeSame(500); } finally { rmdir($webRootDir); } } public function testRelativeWebdavDirectoryIsRefused(): void { $client = static::createClient(); $this->setEnv('WEBDAV_TMP_DIR', 'var/tmp'); static::requestDav($client, 'PROPFIND', '/dav/public/', 'test_user:password'); $this->assertResponseStatusCodeSame(500); } /** * The tmp dir holds the locks database and the temporary files, the homes dir holds * other users' files: none of them may be reachable through the public directory. */ public function testWebdavDirectoriesNestedInThePublicDirectoryAreRefused(): void { $client = static::createClient(); mkdir($this->baseDir.'/public/tmp'); $this->setEnv('WEBDAV_TMP_DIR', $this->baseDir.'/public/tmp'); static::requestDav($client, 'PROPFIND', '/dav/public/', 'test_user:password'); $this->assertResponseStatusCodeSame(500); $this->setEnv('WEBDAV_TMP_DIR', $this->baseDir.'/tmp'); mkdir($this->baseDir.'/public/homes'); $this->setEnv('WEBDAV_HOMES_DIR', $this->baseDir.'/public/homes'); static::requestDav($client, 'PROPFIND', '/dav/public/', 'test_user:password'); $this->assertResponseStatusCodeSame(500); // Siblings are fine mkdir($this->baseDir.'/homes'); $this->setEnv('WEBDAV_HOMES_DIR', $this->baseDir.'/homes'); static::requestDav($client, 'PROPFIND', '/dav/public/', 'test_user:password'); $this->assertResponseStatusCodeSame(207); } /** * Fixtures: test_user is an admin principal, test_user2 is not. */ public function testPublicDirectoryIsReadableByEveryUserButWritableByAdminsOnly(): void { $client = static::createClient(); // Regular user: read yes, write no static::requestDav($client, 'PROPFIND', '/dav/public/', 'test_user2:password2'); $this->assertResponseStatusCodeSame(207); static::requestDav($client, 'PUT', '/dav/public/notes.txt', 'test_user2:password2'); $this->assertResponseStatusCodeSame(403); $this->assertFileDoesNotExist($this->baseDir.'/public/notes.txt'); static::requestDav($client, 'MKCOL', '/dav/public/folder', 'test_user2:password2'); $this->assertResponseStatusCodeSame(403); $this->assertDirectoryDoesNotExist($this->baseDir.'/public/folder'); // Admin: everything static::requestDav($client, 'PUT', '/dav/public/notes.txt', 'test_user:password'); $this->assertResponseStatusCodeSame(201); $this->assertFileExists($this->baseDir.'/public/notes.txt'); // Regular user can read what the admin published, but not remove it. The test client // cannot send a request body, so the content is written on disk; the GET then goes // through the streamed response path. file_put_contents($this->baseDir.'/public/notes.txt', 'Hello from the public dir'); static::requestDav($client, 'GET', '/dav/public/notes.txt', 'test_user2:password2'); $this->assertResponseStatusCodeSame(200); $this->assertSame('Hello from the public dir', $client->getInternalResponse()->getContent()); $this->assertResponseHeaderSame('Content-Length', '25'); static::requestDav($client, 'DELETE', '/dav/public/notes.txt', 'test_user2:password2'); $this->assertResponseStatusCodeSame(403); $this->assertFileExists($this->baseDir.'/public/notes.txt'); static::requestDav($client, 'DELETE', '/dav/public/notes.txt', 'test_user:password'); $this->assertResponseStatusCodeSame(204); $this->assertFileDoesNotExist($this->baseDir.'/public/notes.txt'); } public function testPublicDirectoryIsNeverReadableAnonymously(): void { $client = static::createClient(); static::requestDav($client, 'PROPFIND', '/dav/public/'); $this->assertResponseStatusCodeSame(401); $this->assertStringStartsWith('Basic realm="', $client->getResponse()->headers->get('WWW-Authenticate') ?? ''); $this->setEnv('WEBDAV_PUBLIC_DIR_WRITABLE', 'true'); static::requestDav($client, 'PUT', '/dav/public/notes.txt'); $this->assertResponseStatusCodeSame(401); $this->assertFileDoesNotExist($this->baseDir.'/public/notes.txt'); } public function testPublicDirectoryCanBeOpenedToEveryUser(): void { $this->setEnv('WEBDAV_PUBLIC_DIR_WRITABLE', 'true'); $client = static::createClient(); static::requestDav($client, 'PUT', '/dav/public/notes.txt', 'test_user2:password2'); $this->assertResponseStatusCodeSame(201); static::requestDav($client, 'MKCOL', '/dav/public/folder', 'test_user2:password2'); $this->assertResponseStatusCodeSame(201); static::requestDav($client, 'DELETE', '/dav/public/notes.txt', 'test_user2:password2'); $this->assertResponseStatusCodeSame(204); } /** * Temporary files follow the ACL of their directory, like real files. */ public function testTemporaryFilesFollowThePublicDirectoryAcl(): void { $client = static::createClient(); static::requestDav($client, 'PUT', '/dav/public/.DS_Store', 'test_user2:password2'); $this->assertResponseStatusCodeSame(403); $this->assertSame([], glob($this->baseDir.'/tmp/sabredav_*')); $this->setEnv('WEBDAV_PUBLIC_DIR_WRITABLE', 'true'); static::requestDav($client, 'PUT', '/dav/public/.DS_Store', 'test_user2:password2'); $this->assertResponseStatusCodeSame(201); $this->assertCount(1, glob($this->baseDir.'/tmp/sabredav_*') ?: []); } }