diff --git a/.docker/nginx/config/default.conf b/.docker/nginx/config/default.conf index d75684f..ca1a51c 100644 --- a/.docker/nginx/config/default.conf +++ b/.docker/nginx/config/default.conf @@ -12,7 +12,7 @@ server { location = /robots.txt { access_log off; log_not_found off; } location / { - try_files $uri /index.php$is_args$args; + try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php { diff --git a/.htaccess b/.htaccess index 8cd7c23..96fa8be 100644 --- a/.htaccess +++ b/.htaccess @@ -7,6 +7,7 @@ RewriteRule ^ %1 [L,R=301] # Handle the front controller... + RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [QSA,L] diff --git a/app/src/Bootstrap/AppManager.php b/app/src/Bootstrap/AppManager.php index bfe90ca..1537a6e 100644 --- a/app/src/Bootstrap/AppManager.php +++ b/app/src/Bootstrap/AppManager.php @@ -2,7 +2,6 @@ namespace App\Bootstrap; -use App\Middleware; use App\Providers; use DI\Bridge\Slim\Bridge; use DI\Container; @@ -22,7 +21,7 @@ class AppManager /** @const Constant description */ protected const MIDDLEWARES = [ - Middleware\StripBasePathMiddleware::class, + // ... ]; /** @var Container The applicaiton container */ diff --git a/app/src/Controllers/DirectoryController.php b/app/src/Controllers/DirectoryController.php deleted file mode 100644 index 272400c..0000000 --- a/app/src/Controllers/DirectoryController.php +++ /dev/null @@ -1,101 +0,0 @@ -container = $container; - $this->config = $config; - $this->view = $view; - } - - /** - * Invoke the DirectoryController. - * - * @param \Symfony\Component\Finder\Finder $files - * @param \Slim\Psr7\Response $response - * @param string $path - * - * @return \Psr\Http\Message\ResponseInterface - */ - public function __invoke( - Finder $files, - Request $request, - Response $response, - string $path = '.' - ) { - $search = $request->getQueryParams()['search'] ?? null; - - try { - $files = $files->in($path); - } catch (DirectoryNotFoundException $exception) { - return $this->view->render($response->withStatus(404), '404.twig'); - } - - return $this->view->render($response, 'index.twig', [ - 'files' => $search ? $files->name( - sprintf('/(?:.*)%s(?:.*)/i', preg_quote($search, '/')) - ) : $files->depth(0), - 'path' => $path, - 'readme' => $this->readme($path), - 'search' => $search, - ]); - } - - /** - * Return the README file for a given path. - * - * @param string $path - * - * @return \Symfony\Component\Finder\SplFileInfo|null - */ - protected function readme($path): ?SplFileInfo - { - if (! $this->config->get('app.display_readmes', true)) { - return null; - } - - $readmes = Finder::create()->in($path)->depth(0)->name('/^README(?:\..+)?$/i'); - $readmes->filter(function (SplFileInfo $file) { - return (bool) preg_match('/text\/.+/', mime_content_type($file->getPathname())); - }); - $readmes->sort(function (SplFileInfo $file1, SplFileInfo $file2) { - return $file1->getExtension() <=> $file2->getExtension(); - }); - - if (! $readmes->hasResults()) { - return null; - } - - $readmeArray = iterator_to_array($readmes); - - return array_shift($readmeArray); - } -} diff --git a/app/src/Controllers/IndexController.php b/app/src/Controllers/IndexController.php new file mode 100644 index 0000000..8c746f5 --- /dev/null +++ b/app/src/Controllers/IndexController.php @@ -0,0 +1,46 @@ +container = $container; + } + + /** + * Invoke the IndexController. + * + * @param \Slim\Psr7\Request $request + * @param \Slim\Psr7\Response $response + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function __invoke(Request $request, Response $response) + { + switch (true) { + case array_key_exists('info', $request->getQueryParams()): + return $this->container->call(Handlers\FileInfoHandler::class, [$request, $response]); + + case array_key_exists('search', $request->getQueryParams()): + return $this->container->call(Handlers\SearchHandler::class, [$request, $response]); + + default: + return $this->container->call(Handlers\DirectoryHandler::class, [$request, $response]); + } + } +} diff --git a/app/src/Handlers/DirectoryHandler.php b/app/src/Handlers/DirectoryHandler.php new file mode 100644 index 0000000..444e55b --- /dev/null +++ b/app/src/Handlers/DirectoryHandler.php @@ -0,0 +1,90 @@ +config = $config; + $this->finder = $finder; + $this->view = $view; + } + + /** + * Invoke the IndexController. + * + * @param \Slim\Psr7\Request $request + * @param \Slim\Psr7\Response $response + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function __invoke(Request $request, Response $response) + { + $path = $request->getQueryParams()['dir'] ?? '.'; + + try { + $files = $this->finder->in($path)->depth(0); + } catch (DirectoryNotFoundException $exception) { + return $this->view->render($response->withStatus(404), '404.twig'); + } + + return $this->view->render($response, 'index.twig', [ + 'files' => $files, + 'path' => $path, + 'readme' => $this->readme($files), + ]); + } + + /** + * Return the README file within a finder object. + * + * @param \Symfony\Component\Finder\Finder $files + * + * @return \Symfony\Component\Finder\SplFileInfo|null + */ + protected function readme(Finder $files): ?SplFileInfo + { + if (! $this->config->get('app.display_readmes', true)) { + return null; + } + + $readmes = (clone $files)->name('/^README(?:\..+)?$/i'); + + $readmes->filter(function (SplFileInfo $file) { + return (bool) preg_match('/text\/.+/', mime_content_type($file->getPathname())); + })->sort(function (SplFileInfo $file1, SplFileInfo $file2) { + return $file1->getExtension() <=> $file2->getExtension(); + }); + + if (! $readmes->hasResults()) { + return null; + } + + return $readmes->getIterator()->current(); + } +} diff --git a/app/src/Controllers/FileInfoController.php b/app/src/Handlers/FileInfoHandler.php similarity index 81% rename from app/src/Controllers/FileInfoController.php rename to app/src/Handlers/FileInfoHandler.php index 9bc1727..3538c5d 100644 --- a/app/src/Controllers/FileInfoController.php +++ b/app/src/Handlers/FileInfoHandler.php @@ -1,13 +1,14 @@ getQueryParams()['info']; + $file = new SplFileInfo( realpath($this->container->get('base_path') . '/' . $path) ); diff --git a/app/src/Handlers/SearchHandler.php b/app/src/Handlers/SearchHandler.php new file mode 100644 index 0000000..40cd167 --- /dev/null +++ b/app/src/Handlers/SearchHandler.php @@ -0,0 +1,51 @@ +finder = $finder; + $this->view = $view; + } + + /** + * Invoke the SearchHandler. + * + * @param \Slim\Psr7\Request $request + * @param \Slim\Psr7\Response $response + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function __invoke(Request $request, Response $response) + { + $search = $request->getQueryParams()['search']; + + $files = $this->finder->in('.')->name( + sprintf('/(?:.*)%s(?:.*)/i', preg_quote($search, '/')) + ); + + return $this->view->render($response, 'index.twig', [ + 'files' => $files, + 'search' => $search, + ]); + } +} diff --git a/app/src/Middleware/StripBasePathMiddleware.php b/app/src/Middleware/StripBasePathMiddleware.php deleted file mode 100644 index 924a702..0000000 --- a/app/src/Middleware/StripBasePathMiddleware.php +++ /dev/null @@ -1,60 +0,0 @@ -container = $container; - } - - /** - * Canonicalize the received path variable. - * - * @param \Slim\Psr7\Request $request - * @param \Slim\Psr7\Response $response - * @param callable $next - * - * @return \Psr\Http\Message\ResponseInterface - */ - public function __invoke( - ServerRequestInterface $request, - RequestHandlerInterface $handler - ): ResponseInterface { - $path = $request->getUri()->getPath(); - - $request = $request->withUri( - $request->getUri()->withPath($this->stripBasePath($path)) - ); - - return $handler->handle($request); - } - - /** - * Strip the base URL path from a path string. - * - * @param string $path - * - * @return string - */ - protected function stripBasePath(string $path): string - { - $pattern = sprintf('/^%s/', preg_quote(dirname($_SERVER['SCRIPT_NAME']), '/')); - - return '/' . ltrim(preg_replace($pattern, '', $path), '/'); - } -} diff --git a/index.php b/index.php index 257a0bd..6d7c24d 100644 --- a/index.php +++ b/index.php @@ -21,8 +21,7 @@ $container->set('base_path', __DIR__); $app = $container->call(AppManager::class); // Register routes -$app->get('/file-info/[{path:.*}]', Controllers\FileInfoController::class); -$app->get('/[{path:.*}]', Controllers\DirectoryController::class); +$app->get('/[{path:.*}]', Controllers\IndexController::class); // Engage! $app->run(); diff --git a/tests/Controllers/FileInfoControllerTest.php b/tests/Controllers/FileInfoControllerTest.php deleted file mode 100644 index 65c7442..0000000 --- a/tests/Controllers/FileInfoControllerTest.php +++ /dev/null @@ -1,42 +0,0 @@ -container, $this->config); - - $response = $controller(new Response(), 'README.md'); - - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertEquals(200, $response->getStatusCode()); - } - - public function test_it_can_return_a_not_found_response(): void - { - $controller = new FileInfoController($this->container, $this->config); - - $response = $controller(new Response(), 'not_a_file.test'); - - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertEquals(404, $response->getStatusCode()); - } - - public function test_it_returns_an_error_when_file_size_is_too_large(): void - { - $this->config->set('app.max_hash_size', 10); - $controller = new FileInfoController($this->container, $this->config); - - $response = $controller(new Response(), 'README.md'); - - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertEquals(500, $response->getStatusCode()); - } -} diff --git a/tests/Controllers/IndexControllerTest.php b/tests/Controllers/IndexControllerTest.php new file mode 100644 index 0000000..c55987c --- /dev/null +++ b/tests/Controllers/IndexControllerTest.php @@ -0,0 +1,74 @@ +createMock(Request::class); + $request->method('getQueryParams')->willReturn(['info' => 'file.test']); + + $container = $this->createMock(Container::class); + $container->expects($this->once())->method('call')->with( + Handlers\FileInfoHandler::class, + [$request, $response = new Response] + ); + + $controller = new IndexController($container); + + $controller($request, $response); + } + + public function test_it_handles_a_search_request(): void + { + $request = $this->createMock(Request::class); + $request->method('getQueryParams')->willReturn(['search' => 'file.test']); + + $container = $this->createMock(Container::class); + $container->expects($this->once())->method('call')->with( + Handlers\SearchHandler::class, + [$request, $response = new Response] + ); + + $controller = new IndexController($container); + + $controller($request, $response); + } + + public function test_it_handles_a_directory_request(): void + { + $request = $this->createMock(Request::class); + $request->method('getQueryParams')->willReturn(['dir' => 'some/directory']); + + $container = $this->createMock(Container::class); + $container->expects($this->once())->method('call')->with( + Handlers\DirectoryHandler::class, + [$request, $response = new Response] + ); + + $controller = new IndexController($container); + + $controller($request, $response); + } + + public function test_it_handles_a_directory_request_by_default(): void + { + $container = $this->createMock(Container::class); + $container->expects($this->once())->method('call')->with( + Handlers\DirectoryHandler::class, + [$request = $this->createMock(Request::class), $response = new Response] + ); + + $controller = new IndexController($container); + + $controller($request, $response); + } +} diff --git a/tests/Controllers/DirectoryControllerTest.php b/tests/Handlers/DirectoryHandlerTest.php similarity index 62% rename from tests/Controllers/DirectoryControllerTest.php rename to tests/Handlers/DirectoryHandlerTest.php index 212b00c..2dadd70 100644 --- a/tests/Controllers/DirectoryControllerTest.php +++ b/tests/Handlers/DirectoryHandlerTest.php @@ -2,7 +2,7 @@ namespace Tests\Controllers; -use App\Controllers\DirectoryController; +use App\Handlers\DirectoryHandler; use App\Providers\TwigProvider; use Psr\Http\Message\ResponseInterface; use Slim\Psr7\Request; @@ -11,7 +11,7 @@ use Slim\Views\Twig; use Symfony\Component\Finder\Finder; use Tests\TestCase; -class DirectoryControllerTest extends TestCase +class DirectoryHandlerTest extends TestCase { /** @dataProvider configOptions */ public function test_it_returns_a_successful_response( @@ -25,18 +25,13 @@ class DirectoryControllerTest extends TestCase $this->container->call(TwigProvider::class); - $controller = new DirectoryController( - $this->container, + $controller = new DirectoryHandler( $this->config, + new Finder, $this->container->get(Twig::class) ); - chdir($this->filePath('.')); - $response = $controller( - new Finder(), - $this->createMock(Request::class), - new Response() - ); + $response = $controller($this->createMock(Request::class), new Response); $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(200, $response->getStatusCode()); @@ -54,19 +49,17 @@ class DirectoryControllerTest extends TestCase $this->container->call(TwigProvider::class); - $controller = new DirectoryController( - $this->container, + $controller = new DirectoryHandler( $this->config, + new Finder, $this->container->get(Twig::class) ); + $request = $this->createMock(Request::class); + $request->method('getQueryParams')->willReturn(['dir' => 'subdir']); + chdir($this->filePath('.')); - $response = $controller( - new Finder(), - $this->createMock(Request::class), - new Response(), - 'subdir' - ); + $response = $controller($request, new Response); $this->assertInstanceOf(ResponseInterface::class, $response); $this->assertEquals(200, $response->getStatusCode()); @@ -76,48 +69,19 @@ class DirectoryControllerTest extends TestCase { $this->container->call(TwigProvider::class); - $controller = new DirectoryController( - $this->container, - $this->config, - $this->container->get(Twig::class) - ); - - chdir($this->filePath('.')); - $response = $controller( - new Finder(), - $this->createMock(Request::class), - new Response(), - '404' - ); - - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertEquals(404, $response->getStatusCode()); - } - - public function test_it_returns_a_successful_response_for_a_search_request(): void - { - $this->container->call(TwigProvider::class); - - $controller = new DirectoryController( - $this->container, + $controller = new DirectoryHandler( $this->config, + new Finder, $this->container->get(Twig::class) ); $request = $this->createMock(Request::class); - $request->method('getQueryParams')->willReturn([ - 'search' => 'charlie' - ]); + $request->method('getQueryParams')->willReturn(['dir' => '404']); - chdir($this->filePath('.')); - $response = $controller( - new Finder(), - $request, - new Response() - ); + $response = $controller($request, new Response); $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(404, $response->getStatusCode()); } /** diff --git a/tests/Handlers/FileInfoHandlerTest.php b/tests/Handlers/FileInfoHandlerTest.php new file mode 100644 index 0000000..5fc8338 --- /dev/null +++ b/tests/Handlers/FileInfoHandlerTest.php @@ -0,0 +1,59 @@ +container, $this->config); + + $request = $this->createMock(Request::class); + $request->method('getQueryParams')->willReturn(['info' => 'README.md']); + + $response = $handler($request, new Response); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(json_encode([ + 'hashes' => [ + 'md5' => '6e35c5c3bca40dfb96cbb449fd06df38', + 'sha1' => '7ea619032a992824fac30026d3df919939c7ebfb', + 'sha256' => '40adf7348820699ed3e72dc950ccd8d8d538065a91eba3c76263c44b1d12df9c', + ] + ]), (string) $response->getBody()); + } + + public function test_it_can_return_a_not_found_response(): void + { + $handler = new FileInfoHandler($this->container, $this->config); + + $request = $this->createMock(Request::class); + $request->method('getQueryParams')->willReturn(['info' => 'not_a_file.test']); + + $response = $handler($request, new Response); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertEquals(404, $response->getStatusCode()); + } + + public function test_it_returns_an_error_when_file_size_is_too_large(): void + { + $this->config->set('app.max_hash_size', 10); + $handler = new FileInfoHandler($this->container, $this->config); + + $request = $this->createMock(Request::class); + $request->method('getQueryParams')->willReturn(['info' => 'README.md']); + + $response = $handler($request, new Response); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertEquals(500, $response->getStatusCode()); + } +} diff --git a/tests/Handlers/SearchHandlerTest.php b/tests/Handlers/SearchHandlerTest.php new file mode 100644 index 0000000..cf556c4 --- /dev/null +++ b/tests/Handlers/SearchHandlerTest.php @@ -0,0 +1,30 @@ +container->call(TwigProvider::class); + + $handler = new SearchHandler(new Finder, $this->container->get(Twig::class)); + + $request = $this->createMock(Request::class); + $request->method('getQueryParams')->willReturn(['search' => 'charlie']); + + $response = $handler($request, new Response); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + } +} diff --git a/tests/Middleware/StripBasePathMiddlewareTest.php b/tests/Middleware/StripBasePathMiddlewareTest.php deleted file mode 100644 index ecec9ba..0000000 --- a/tests/Middleware/StripBasePathMiddlewareTest.php +++ /dev/null @@ -1,51 +0,0 @@ -container); - - $uri = new Uri('http', 'localhost', null, '/foo/bar'); - $request = new Request('GET', $uri, new Headers, [], [], new Stream(fopen('php://memory', 'w+'))); - - $handler = $this->createMock(App::class); - $handler->expects($this->once())->method('handle')->with( - $this->callback(function (ServerRequestInterface $request): bool { - return $request->getUri()->getPath() == '/foo/bar'; - }) - ); - - $middleware($request, $handler); - } - - public function test_it_strips_the_base_path_for_a_request_in_a_subdirectory(): void - { - $_SERVER['SCRIPT_NAME'] = '/some/dir/index.php'; - - $middleware = new StripBasePathMiddleware($this->container); - - $uri = new Uri('http', 'localhost', null, '/some/dir/foo/bar'); - $request = new Request('GET', $uri, new Headers, [], [], new Stream(fopen('php://memory', 'w+'))); - - $handler = $this->createMock(App::class); - $handler->expects($this->once())->method('handle')->with( - $this->callback(function (ServerRequestInterface $request): bool { - return $request->getUri()->getPath() == '/foo/bar'; - }) - ); - - $middleware($request, $handler); - } -}