diff --git a/.env.example b/.env.example
index 11b1785..88e1e25 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,5 @@
+DEBUG=false
+
DARK_MODE=false
DISPLAY_READMES=true
diff --git a/README.md b/README.md
index bd96ad2..17a7126 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,8 @@ A list of changes can be found on the [GitHub Releases](https://github.com/Direc
Troubleshooting
---------------
+See the [Common Issues](https://github.com/DirectoryLister/DirectoryLister/wiki/Common-Issues) page for a list of common issues and help in solving them.
+
For general help and support join our [Spectrum Community](https://spectrum.chat/directory-lister) or reach out on [Twitter](https://twitter.com/DirectoryLister).
Please report bugs to the [GitHub Issue Tracker](https://github.com/DirectoryLister/DirectoryLister/issues).
diff --git a/app/config/app.php b/app/config/app.php
index dd48cfd..c82e345 100644
--- a/app/config/app.php
+++ b/app/config/app.php
@@ -3,6 +3,17 @@
use App\Support\Helpers;
return [
+ /**
+ * Enable application debugging and display error messages.
+ *
+ * !!! WARNING !!!
+ * It is recommended that debug remains OFF unless troubleshooting an issue.
+ * Leaving this enabled WILL cause leakage of sensitive server information.
+ *
+ * Default value: false
+ */
+ 'debug' => Helpers::env('DEBUG'),
+
/**
* Enable dark mode?
*
diff --git a/app/src/Bootstrap/AppManager.php b/app/src/Bootstrap/AppManager.php
index feca7e9..785fd46 100644
--- a/app/src/Bootstrap/AppManager.php
+++ b/app/src/Bootstrap/AppManager.php
@@ -2,11 +2,13 @@
namespace App\Bootstrap;
+use App\Exceptions\ExceptionManager;
+use App\Middlewares;
use App\Providers;
use DI\Bridge\Slim\Bridge;
use DI\Container;
use Invoker\CallableResolver;
-use Middlewares;
+use Middlewares as HttpMiddlewares;
use Slim\App;
use Tightenco\Collect\Support\Collection;
@@ -17,6 +19,12 @@ class AppManager
Providers\ConfigProvider::class,
Providers\FinderProvider::class,
Providers\TwigProvider::class,
+ Providers\WhoopsProvider::class,
+ ];
+
+ /** @const Array of application middlewares */
+ protected const MIDDLEWARES = [
+ Middlewares\WhoopsMiddleware::class
];
/** @var Container The applicaiton container */
@@ -26,7 +34,7 @@ class AppManager
protected $callableResolver;
/**
- * Create a new Provider object.
+ * Create a new AppManager object.
*
* @param \DI\Container $container
* @param \Invoker\CallableResolver $callableResolver
@@ -46,10 +54,9 @@ class AppManager
{
$this->registerProviders();
$app = Bridge::create($this->container);
- $app->add(new Middlewares\Expires([
- 'application/zip' => '+1 hour',
- 'text/json' => '+1 hour',
- ]));
+ $this->registerMiddlewares($app);
+
+ $this->container->call(ExceptionManager::class);
return $app;
}
@@ -69,4 +76,25 @@ class AppManager
}
);
}
+
+ /**
+ * Register application middlewares.
+ *
+ * @param \Slim\App $app
+ *
+ * @return void
+ */
+ protected function registerMiddlewares(App $app): void
+ {
+ Collection::make(self::MIDDLEWARES)->each(
+ function (string $middleware) use ($app): void {
+ $app->add($middleware);
+ }
+ );
+
+ $app->add(new HttpMiddlewares\Expires([
+ 'application/zip' => '+1 hour',
+ 'text/json' => '+1 hour',
+ ]));
+ }
}
diff --git a/app/src/Exceptions/ErrorHandler.php b/app/src/Exceptions/ErrorHandler.php
new file mode 100644
index 0000000..4c8fd2d
--- /dev/null
+++ b/app/src/Exceptions/ErrorHandler.php
@@ -0,0 +1,63 @@
+view = $view;
+ }
+
+ /**
+ * Invoke the ErrorHandler class.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request
+ * @param \Throwable $exception
+ * @param bool $displayErrorDetails
+ * @param bool $logErrors
+ * @param bool $logErrorDetails
+ *
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function __invoke(
+ ServerRequestInterface $request,
+ Throwable $exception,
+ bool $displayErrorDetails,
+ bool $logErrors,
+ bool $logErrorDetails
+ ): ResponseInterface {
+ $response = (new Response)->withStatus(500);
+
+ if (in_array('application/json', explode(',', $request->getHeaderLine('Accept')))) {
+ $response->getBody()->write(json_encode([
+ 'error' => ['message' => self::DEFAULT_ERROR_MESSAGE]
+ ]));
+
+ return $response->withHeader('Content-Type', 'application/json');
+ }
+
+ return $this->view->render($response, 'error.twig', [
+ 'message' => self::DEFAULT_ERROR_MESSAGE,
+ 'subtext' => 'Enable debugging for additional information',
+ ]);
+ }
+}
diff --git a/app/src/Exceptions/ExceptionManager.php b/app/src/Exceptions/ExceptionManager.php
new file mode 100644
index 0000000..169623e
--- /dev/null
+++ b/app/src/Exceptions/ExceptionManager.php
@@ -0,0 +1,42 @@
+app = $app;
+ $this->config = $config;
+ }
+
+ /**
+ * Set up and configure exception handling.
+ *
+ * @return void
+ */
+ public function __invoke(): void
+ {
+ if ($this->config->get('app.debug', false)) {
+ return;
+ }
+
+ $errorMiddleware = $this->app->addErrorMiddleware(true, true, true);
+ $errorMiddleware->setDefaultErrorHandler(ErrorHandler::class);
+ }
+}
diff --git a/app/src/Middlewares/WhoopsMiddleware.php b/app/src/Middlewares/WhoopsMiddleware.php
new file mode 100644
index 0000000..531a244
--- /dev/null
+++ b/app/src/Middlewares/WhoopsMiddleware.php
@@ -0,0 +1,72 @@
+whoops = $whoops;
+ $this->pageHandler = $pageHandler;
+ $this->jsonHandler = $jsonHandler;
+ $this->config = $config;
+ }
+
+ /**
+ * Invoke the WhoopseMiddleware class.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler
+ *
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function __invoke(Request $request, RequestHandler $handler): ResponseInterface
+ {
+ $this->pageHandler->addDataTable('Application Config', $this->config->split('app')->toArray());
+ $this->pageHandler->setPageTitle(
+ sprintf('%s • Directory Lister', $this->pageHandler->getPageTitle())
+ );
+
+ $this->whoops->pushHandler($this->pageHandler);
+
+ if (in_array('application/json', explode(',', $request->getHeaderLine('Accept')))) {
+ $this->whoops->pushHandler($this->jsonHandler);
+ }
+
+ $this->whoops->register();
+
+ return $handler->handle($request);
+ }
+}
diff --git a/app/src/Providers/WhoopsProvider.php b/app/src/Providers/WhoopsProvider.php
new file mode 100644
index 0000000..a612edc
--- /dev/null
+++ b/app/src/Providers/WhoopsProvider.php
@@ -0,0 +1,37 @@
+container = $container;
+ }
+
+ /**
+ * Initialize and register the Whoops component.
+ *
+ * @return void
+ */
+ public function __invoke(): void
+ {
+ $this->container->set(PrettyPageHandler::class, new PrettyPageHandler);
+ $this->container->set(JsonResponseHandler::class, new JsonResponseHandler);
+ $this->container->set(RunInterface::class, new Run);
+ }
+}
diff --git a/app/views/error.twig b/app/views/error.twig
index e17b61c..96051b3 100644
--- a/app/views/error.twig
+++ b/app/views/error.twig
@@ -4,10 +4,14 @@
{% block content %}
{% include 'components/header.twig' %}
-
+
{{ message | default('An unexpected error occured') }}
+
+
+ {{ subtext }}
+
{% include 'components/footer.twig' %}
diff --git a/composer.json b/composer.json
index 20ef167..4e8d636 100644
--- a/composer.json
+++ b/composer.json
@@ -13,6 +13,7 @@
"php": ">=7.2",
"ext-zip": "*",
"erusev/parsedown-extra": "^0.8.1",
+ "filp/whoops": "^2.7",
"middlewares/cache": "^2.0",
"phlak/config": "^6.1",
"php-di/php-di": "^6.0",
diff --git a/composer.lock b/composer.lock
index f1e6e78..e37f795 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "871b5d48a53bf07bdac340934b41325c",
+ "content-hash": "1a4f0a5fb5a91dc494cb5479886f6001",
"packages": [
{
"name": "erusev/parsedown",
@@ -151,6 +151,67 @@
],
"time": "2020-02-05T20:36:27+00:00"
},
+ {
+ "name": "filp/whoops",
+ "version": "2.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/filp/whoops.git",
+ "reference": "fff6f1e4f36be0e0d0b84d66b413d9dcb0c49130"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/filp/whoops/zipball/fff6f1e4f36be0e0d0b84d66b413d9dcb0c49130",
+ "reference": "fff6f1e4f36be0e0d0b84d66b413d9dcb0c49130",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9 || ^7.0",
+ "psr/log": "^1.0.1"
+ },
+ "require-dev": {
+ "mockery/mockery": "^0.9 || ^1.0",
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0",
+ "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0"
+ },
+ "suggest": {
+ "symfony/var-dumper": "Pretty print complex values better with var-dumper available",
+ "whoops/soap": "Formats errors as SOAP responses"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Whoops\\": "src/Whoops/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Filipe Dobreira",
+ "homepage": "https://github.com/filp",
+ "role": "Developer"
+ }
+ ],
+ "description": "php error handling for cool kids",
+ "homepage": "https://filp.github.io/whoops/",
+ "keywords": [
+ "error",
+ "exception",
+ "handling",
+ "library",
+ "throwable",
+ "whoops"
+ ],
+ "time": "2020-01-15T10:00:00+00:00"
+ },
{
"name": "jeremeamia/superclosure",
"version": "2.4.0",
@@ -1043,6 +1104,53 @@
],
"time": "2018-10-30T17:12:04+00:00"
},
+ {
+ "name": "psr/log",
+ "version": "1.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801",
+ "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "time": "2019-11-01T11:05:21+00:00"
+ },
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
@@ -3765,53 +3873,6 @@
],
"time": "2019-01-08T18:20:26+00:00"
},
- {
- "name": "psr/log",
- "version": "1.1.2",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/log.git",
- "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801",
- "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.1.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Log\\": "Psr/Log/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "http://www.php-fig.org/"
- }
- ],
- "description": "Common interface for logging libraries",
- "homepage": "https://github.com/php-fig/log",
- "keywords": [
- "log",
- "psr",
- "psr-3"
- ],
- "time": "2019-11-01T11:05:21+00:00"
- },
{
"name": "psy/psysh",
"version": "v0.9.12",
diff --git a/tests/Bootstrap/AppManangerTest.php b/tests/Bootstrap/AppManangerTest.php
index ed1994a..75c8523 100644
--- a/tests/Bootstrap/AppManangerTest.php
+++ b/tests/Bootstrap/AppManangerTest.php
@@ -3,6 +3,8 @@
namespace Tests\Bootstrap;
use App\Bootstrap\AppManager;
+use App\Providers;
+use DI\Container;
use Invoker\CallableResolver;
use Slim\App;
use Tests\TestCase;
@@ -16,4 +18,19 @@ class AppManangerTest extends TestCase
$this->assertInstanceOf(App::class, $app);
}
+
+ public function test_it_registeres_providers(): void
+ {
+ $callableResolver = $this->container->get(CallableResolver::class);
+
+ $container = $this->createMock(Container::class);
+ $container->expects($this->atLeast(4))->method('call')->withConsecutive(
+ [$callableResolver->resolve(Providers\ConfigProvider::class)],
+ [$callableResolver->resolve(Providers\FinderProvider::class)],
+ [$callableResolver->resolve(Providers\TwigProvider::class)],
+ [$callableResolver->resolve(Providers\WhoopsProvider::class)],
+ );
+
+ (new AppManager($container, $callableResolver))();
+ }
}
diff --git a/tests/Exceptions/ErrorHandlerTest.php b/tests/Exceptions/ErrorHandlerTest.php
new file mode 100644
index 0000000..fbc609d
--- /dev/null
+++ b/tests/Exceptions/ErrorHandlerTest.php
@@ -0,0 +1,57 @@
+container->call(TwigProvider::class);
+ $errorHandler = new ErrorHandler($this->container->get(Twig::class));
+
+ $response = $errorHandler(
+ $this->createMock(Request::class),
+ new Exception('Test exception; please ignore'),
+ true,
+ true,
+ true
+ );
+
+ $this->assertEquals(500, $response->getStatusCode());
+ $this->assertEquals('text/html', finfo_buffer(
+ finfo_open(), (string) $response->getBody(), FILEINFO_MIME_TYPE
+ ));
+ }
+
+ public function test_it_returns_an_error_for_a_json_request(): void
+ {
+ $this->container->call(TwigProvider::class);
+ $errorHandler = new ErrorHandler($this->container->get(Twig::class));
+
+ $request = $this->createMock(Request::class);
+ $request->expects($this->once())->method('getHeaderLine')->willReturn(
+ 'application/json'
+ );
+
+ $response = $errorHandler(
+ $request,
+ new Exception('Test exception; please ignore'),
+ true,
+ true,
+ true
+ );
+
+ $this->assertEquals(500, $response->getStatusCode());
+ $this->assertEquals('application/json', $response->getHeaderLine('Content-Type'));
+ $this->assertEquals('An unexpected error occured', json_decode(
+ (string) $response->getBody()
+ )->error->message);
+ }
+}
diff --git a/tests/Exceptions/ExceptionManagerTest.php b/tests/Exceptions/ExceptionManagerTest.php
new file mode 100644
index 0000000..6b8aa6d
--- /dev/null
+++ b/tests/Exceptions/ExceptionManagerTest.php
@@ -0,0 +1,37 @@
+createMock(ErrorMiddleware::class);
+ $errorMiddleware->expects($this->once())
+ ->method('setDefaultErrorHandler')
+ ->with(ErrorHandler::class);
+
+ $app = $this->createMock(App::class);
+ $app->expects($this->once())
+ ->method('addErrorMiddleware')
+ ->willReturn($errorMiddleware);
+
+ (new ExceptionManager($app, $this->config))();
+ }
+
+ public function test_it_does_not_set_the_default_error_handler_when_debug_is_enabled(): void
+ {
+ $this->config->set('app.debug', true);
+
+ $app = $this->createMock(App::class);
+ $app->expects($this->never())->method('addErrorMiddleware');
+
+ (new ExceptionManager($app, $this->config))();
+ }
+}
diff --git a/tests/Middlewares/WhoopsMiddlewareTest.php b/tests/Middlewares/WhoopsMiddlewareTest.php
new file mode 100644
index 0000000..be8192c
--- /dev/null
+++ b/tests/Middlewares/WhoopsMiddlewareTest.php
@@ -0,0 +1,73 @@
+createMock(PrettyPageHandler::class);
+ $pageHandler->expects($this->once())->method('getPageTitle')->willReturn(
+ 'Test title; please ignore'
+ );
+ $pageHandler->expects($this->once())->method('setPageTitle')->with(
+ 'Test title; please ignore • Directory Lister'
+ );
+ $pageHandler->expects($this->once())->method('addDataTable')->with(
+ 'Application Config', $this->config->split('app')->toArray()
+ );
+
+ $whoops = $this->createMock(RunInterface::class);
+ $whoops->expects($this->once())->method('pushHandler')->with(
+ $pageHandler
+ );
+
+ $middleware = new WhoopsMiddleware(
+ $whoops, $pageHandler, new JsonResponseHandler, $this->config
+ );
+
+ $middleware(
+ $this->createMock(ServerRequestInterface::class),
+ $this->createMock(RequestHandlerInterface::class)
+ );
+ }
+
+ public function test_it_registers_whoops_with_the_json_handler(): void
+ {
+ $pageHandler = $this->createMock(PrettyPageHandler::class);
+ $pageHandler->expects($this->once())->method('getPageTitle')->willReturn(
+ 'Test title; please ignore'
+ );
+ $pageHandler->expects($this->once())->method('setPageTitle')->with(
+ 'Test title; please ignore • Directory Lister'
+ );
+ $pageHandler->expects($this->once())->method('addDataTable')->with(
+ 'Application Config', $this->config->split('app')->toArray()
+ );
+
+ $jsonHandler = new JsonResponseHandler;
+
+ $whoops = $this->createMock(RunInterface::class);
+ $whoops->expects($this->exactly(2))->method('pushHandler')->withConsecutive(
+ [$pageHandler],
+ [$jsonHandler]
+ );
+
+ $middleware = new WhoopsMiddleware(
+ $whoops, $pageHandler, $jsonHandler, $this->config
+ );
+
+ $request = $this->createMock(ServerRequestInterface::class);
+ $request->expects($this->once())->method('getHeaderLine')->willReturn('application/json');
+
+ $middleware($request, $this->createMock(RequestHandlerInterface::class));
+ }
+}