From 7d408e382af3d1bcf4d984a8244b858bc7fedf76 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 6 Feb 2025 16:56:13 +0200 Subject: [PATCH 01/14] Issue #43: Control the contents of 'extra' in error log. Signed-off-by: alexmerlin --- composer.json | 1 + config/error-handling.global.php.dist | 47 ++++++++++- src/Extra/ExtraProvider.php | 92 ++++++++++++++++++++++ src/Extra/Processor/CookieProcessor.php | 18 +++++ src/Extra/Processor/HeaderProcessor.php | 16 ++++ src/Extra/Processor/ProcessorInterface.php | 10 +++ src/Extra/Processor/RequestProcessor.php | 40 ++++++++++ src/Extra/Processor/ServerProcessor.php | 13 +++ src/Extra/Processor/SessionProcessor.php | 17 ++++ src/Extra/Processor/TraceProcessor.php | 25 ++++++ src/Extra/Provider/CookieProvider.php | 25 ++++++ src/Extra/Provider/HeaderProvider.php | 25 ++++++ src/Extra/Provider/RequestProvider.php | 25 ++++++ src/Extra/Provider/ServerProvider.php | 25 ++++++ src/Extra/Provider/SessionProvider.php | 25 ++++++ src/Extra/Provider/TraceProvider.php | 25 ++++++ src/LogErrorHandler.php | 50 ++++++++++-- src/LogErrorHandlerFactory.php | 14 +++- 18 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 src/Extra/ExtraProvider.php create mode 100644 src/Extra/Processor/CookieProcessor.php create mode 100644 src/Extra/Processor/HeaderProcessor.php create mode 100644 src/Extra/Processor/ProcessorInterface.php create mode 100644 src/Extra/Processor/RequestProcessor.php create mode 100644 src/Extra/Processor/ServerProcessor.php create mode 100644 src/Extra/Processor/SessionProcessor.php create mode 100644 src/Extra/Processor/TraceProcessor.php create mode 100644 src/Extra/Provider/CookieProvider.php create mode 100644 src/Extra/Provider/HeaderProvider.php create mode 100644 src/Extra/Provider/RequestProvider.php create mode 100644 src/Extra/Provider/ServerProvider.php create mode 100644 src/Extra/Provider/SessionProvider.php create mode 100644 src/Extra/Provider/TraceProvider.php diff --git a/composer.json b/composer.json index c3ce551..35c6aa8 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "dotkernel/dot-log": "^4.1.0", "laminas/laminas-diactoros": "^3.3", + "laminas/laminas-stdlib": "^3.20", "laminas/laminas-stratigility": "^3.11", "mezzio/mezzio": "^3.19", "psr/http-message": "^1.0 || ^2.0", diff --git a/config/error-handling.global.php.dist b/config/error-handling.global.php.dist index eb07019..44d68d4 100644 --- a/config/error-handling.global.php.dist +++ b/config/error-handling.global.php.dist @@ -1,16 +1,55 @@ [ + 'dependencies' => [ 'aliases' => [ ErrorHandlerInterface::class => LogErrorHandler::class, - ] + ], ], 'dot-errorhandler' => [ 'loggerEnabled' => true, - 'logger' => 'dot-log.default_logger' - ] + 'logger' => 'dot-log.default_logger', + 'loggerExtra' => [ + CookieProvider::class => [ + 'enabled' => true, + 'processor' => CookieProcessor::class, + ], + HeaderProvider::class => [ + 'enabled' => true, + 'processor' => HeaderProcessor::class, + ], + RequestProvider::class => [ + 'enabled' => true, + 'processor' => RequestProcessor::class, + ], + ServerProvider::class => [ + 'enabled' => true, + 'processor' => null, + ], + SessionProvider::class => [ + 'enabled' => true, + 'processor' => SessionProcessor::class, + ], + TraceProvider::class => [ + 'enabled' => true, + 'processor' => TraceProcessor::class, + ], + ], + ], ]; diff --git a/src/Extra/ExtraProvider.php b/src/Extra/ExtraProvider.php new file mode 100644 index 0000000..02c2086 --- /dev/null +++ b/src/Extra/ExtraProvider.php @@ -0,0 +1,92 @@ + CookieProvider::class, + 'header' => HeaderProvider::class, + 'request' => RequestProvider::class, + 'server' => ServerProvider::class, + 'session' => SessionProvider::class, + 'trace' => TraceProvider::class, + ]; + + foreach ($extras as $logKey => $logClass) { + $enabled = false; + $processor = null; + if (array_key_exists($logClass, $options)) { + if (isset($options[$logClass]['enabled']) && is_bool($options[$logClass]['enabled'])) { + $enabled = $options[$logClass]['enabled']; + } + if ( + isset($options[$logClass]['processor']) + && is_string($options[$logClass]['processor']) + && class_exists($options[$logClass]['processor']) + ) { + $processor = new $options[$logClass]['processor'](); + if (! $processor instanceof ProcessorInterface) { + $processor = null; + } + } + } + + $this->$logKey = new $logClass($enabled, $processor); + } + } + + public function getCookie(): CookieProvider + { + return $this->cookie; + } + + public function getHeader(): HeaderProvider + { + return $this->header; + } + + public function getRequest(): RequestProvider + { + return $this->request; + } + + public function getServer(): ServerProvider + { + return $this->server; + } + + public function getSession(): SessionProvider + { + return $this->session; + } + + public function getTrace(): TraceProvider + { + return $this->trace; + } +} diff --git a/src/Extra/Processor/CookieProcessor.php b/src/Extra/Processor/CookieProcessor.php new file mode 100644 index 0000000..4b8beda --- /dev/null +++ b/src/Extra/Processor/CookieProcessor.php @@ -0,0 +1,18 @@ + str_pad(substr($cookie, 0, 8), strlen($cookie), '.'), $data); + } +} diff --git a/src/Extra/Processor/HeaderProcessor.php b/src/Extra/Processor/HeaderProcessor.php new file mode 100644 index 0000000..b9f6e39 --- /dev/null +++ b/src/Extra/Processor/HeaderProcessor.php @@ -0,0 +1,16 @@ + implode("; ", $headerSet), $data); + } +} diff --git a/src/Extra/Processor/ProcessorInterface.php b/src/Extra/Processor/ProcessorInterface.php new file mode 100644 index 0000000..1ac7379 --- /dev/null +++ b/src/Extra/Processor/ProcessorInterface.php @@ -0,0 +1,10 @@ + $value) { + if (is_array($value)) { + $return[$key] = $this->process($value); + } elseif (is_string($value)) { + $lowerKey = strtolower($key); + if ( + str_contains($lowerKey, 'password') || + str_contains($lowerKey, 'key') || + str_contains($lowerKey, 'csrf') || + str_contains($lowerKey, 'token') + ) { + $return[$key] = preg_replace('/[\da-z]/i', 'x', $value); + } else { + $return[$key] = $value; + } + } else { + $return[$key] = $value; + } + } + + return $return; + } +} diff --git a/src/Extra/Processor/ServerProcessor.php b/src/Extra/Processor/ServerProcessor.php new file mode 100644 index 0000000..4df18eb --- /dev/null +++ b/src/Extra/Processor/ServerProcessor.php @@ -0,0 +1,13 @@ + ArrayUtils::iteratorToArray($container), $data); + } +} diff --git a/src/Extra/Processor/TraceProcessor.php b/src/Extra/Processor/TraceProcessor.php new file mode 100644 index 0000000..b72260a --- /dev/null +++ b/src/Extra/Processor/TraceProcessor.php @@ -0,0 +1,25 @@ + sprintf( + '%s%s%s:%d', + $trace['class'] ?? $trace['file'] ?? 'unknown', + $trace['type'] ?? '->', + $trace['function'] ?? 'unknown', + $trace['line'] ?? 0 + ), + $data + ); + } +} diff --git a/src/Extra/Provider/CookieProvider.php b/src/Extra/Provider/CookieProvider.php new file mode 100644 index 0000000..34d792e --- /dev/null +++ b/src/Extra/Provider/CookieProvider.php @@ -0,0 +1,25 @@ +processor instanceof ProcessorInterface) { + return $this->processor->process($cookies); + } + + return $cookies; + } +} diff --git a/src/Extra/Provider/HeaderProvider.php b/src/Extra/Provider/HeaderProvider.php new file mode 100644 index 0000000..280116b --- /dev/null +++ b/src/Extra/Provider/HeaderProvider.php @@ -0,0 +1,25 @@ +processor instanceof ProcessorInterface) { + return $this->processor->process($headers); + } + + return $headers; + } +} diff --git a/src/Extra/Provider/RequestProvider.php b/src/Extra/Provider/RequestProvider.php new file mode 100644 index 0000000..de90c5e --- /dev/null +++ b/src/Extra/Provider/RequestProvider.php @@ -0,0 +1,25 @@ +processor instanceof ProcessorInterface) { + return $this->processor->process($request); + } + + return $request; + } +} diff --git a/src/Extra/Provider/ServerProvider.php b/src/Extra/Provider/ServerProvider.php new file mode 100644 index 0000000..4ea967d --- /dev/null +++ b/src/Extra/Provider/ServerProvider.php @@ -0,0 +1,25 @@ +processor instanceof ProcessorInterface) { + return $this->processor->process($server); + } + + return $server; + } +} diff --git a/src/Extra/Provider/SessionProvider.php b/src/Extra/Provider/SessionProvider.php new file mode 100644 index 0000000..a1c42f8 --- /dev/null +++ b/src/Extra/Provider/SessionProvider.php @@ -0,0 +1,25 @@ +processor instanceof ProcessorInterface) { + return $this->processor->process($session); + } + + return $session; + } +} diff --git a/src/Extra/Provider/TraceProvider.php b/src/Extra/Provider/TraceProvider.php new file mode 100644 index 0000000..fc6797b --- /dev/null +++ b/src/Extra/Provider/TraceProvider.php @@ -0,0 +1,25 @@ +processor instanceof ProcessorInterface) { + return $this->processor->process($traces); + } + + return $traces; + } +} diff --git a/src/LogErrorHandler.php b/src/LogErrorHandler.php index dc3e033..bc0ad9b 100644 --- a/src/LogErrorHandler.php +++ b/src/LogErrorHandler.php @@ -4,6 +4,7 @@ namespace Dot\ErrorHandler; +use Dot\ErrorHandler\Extra\ExtraProvider; use Dot\Log\LoggerInterface; use ErrorException; use Laminas\Stratigility\Middleware\ErrorResponseGenerator; @@ -27,6 +28,7 @@ class LogErrorHandler implements MiddlewareInterface, ErrorHandlerInterface /** @var callable */ private $responseFactory; private LoggerInterface|null $logger; + private ?ExtraProvider $extraProvider; /** * @param callable $responseFactory A factory capable of returning an @@ -38,13 +40,15 @@ class LogErrorHandler implements MiddlewareInterface, ErrorHandlerInterface public function __construct( callable $responseFactory, ?callable $responseGenerator = null, - ?LoggerInterface $logger = null + ?LoggerInterface $logger = null, + ?ExtraProvider $extraProvider = null, ) { $this->responseFactory = function () use ($responseFactory): ResponseInterface { return $responseFactory(); }; $this->responseGenerator = $responseGenerator ?: new ErrorResponseGenerator(); $this->logger = $logger; + $this->extraProvider = $extraProvider; } public function attachListener(callable $listener): void @@ -78,18 +82,18 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface * triggers all listeners with the same arguments (but using the response * returned from createErrorResponse()), and then returns the response. * - * If a valid Logger is available, the error and it's message are logged in the + * If a valid Logger is available, the error, and it's message are logged in the * configured format. */ - public function handleThrowable(Throwable $e, ServerRequestInterface $request): ResponseInterface + public function handleThrowable(Throwable $throwable, ServerRequestInterface $request): ResponseInterface { $generator = $this->responseGenerator; if ($this->logger instanceof LoggerInterface) { - $this->logger->err($e->getMessage(), (array) $e); + $this->logger->err($throwable->getMessage(), $this->prepareExtra($throwable, $request)); } - $response = $generator($e, $request, ($this->responseFactory)()); - $this->triggerListeners($e, $request, $response); + $response = $generator($throwable, $request, ($this->responseFactory)()); + $this->triggerListeners($throwable, $request, $response); return $response; } @@ -126,4 +130,38 @@ public function triggerListeners( $listener($error, $request, $response); } } + + private function prepareExtra(Throwable $throwable, ServerRequestInterface $request): array + { + $extra = [ + 'file' => $throwable->getFile(), + 'line' => $throwable->getLine(), + ]; + + if ($this->extraProvider?->getCookie()->enabled) { + $extra['cookie'] = $this->extraProvider?->getCookie()->provide($request->getCookieParams()); + } + + if ($this->extraProvider?->getHeader()->enabled) { + $extra['header'] = $this->extraProvider?->getHeader()->provide($request->getHeaders()); + } + + if ($this->extraProvider?->getRequest()->enabled) { + $extra['request'] = $this->extraProvider?->getRequest()->provide($request->getParsedBody()); + } + + if ($this->extraProvider?->getServer()->enabled) { + $extra['server'] = $this->extraProvider?->getServer()->provide($request->getServerParams()); + } + + if ($this->extraProvider?->getSession()->enabled) { + $extra['session'] = $this->extraProvider?->getSession()->provide($_SESSION ?? []); + } + + if ($this->extraProvider?->getTrace()->enabled) { + $extra['trace'] = $this->extraProvider?->getTrace()->provide($throwable->getTrace()); + } + + return $extra; + } } diff --git a/src/LogErrorHandlerFactory.php b/src/LogErrorHandlerFactory.php index 4220e4a..c0f7f9e 100644 --- a/src/LogErrorHandlerFactory.php +++ b/src/LogErrorHandlerFactory.php @@ -4,6 +4,7 @@ namespace Dot\ErrorHandler; +use Dot\ErrorHandler\Extra\ExtraProvider; use Dot\Log\LoggerInterface; use InvalidArgumentException; use Mezzio\Middleware\ErrorResponseGenerator; @@ -13,6 +14,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Server\MiddlewareInterface; +use function array_key_exists; use function is_array; use function sprintf; @@ -41,6 +43,11 @@ public function __invoke(ContainerInterface $container): MiddlewareInterface ); } + $extraProvider = null; + if (array_key_exists('loggerExtra', $errorHandlerConfig) && is_array($errorHandlerConfig['loggerExtra'])) { + $extraProvider = new ExtraProvider($errorHandlerConfig['loggerExtra']); + } + $logger = null; if ($errorHandlerConfig['loggerEnabled']) { /** @var LoggerInterface $logger */ @@ -51,6 +58,11 @@ public function __invoke(ContainerInterface $container): MiddlewareInterface ? $container->get(ErrorResponseGenerator::class) : null; - return new LogErrorHandler($container->get(ResponseInterface::class), $generator, $logger); + return new LogErrorHandler( + $container->get(ResponseInterface::class), + $generator, + $logger, + $extraProvider + ); } } From fef6ce9e8bcf59f540ed56bc9a064cd5db60001c Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 6 Feb 2025 17:07:59 +0200 Subject: [PATCH 02/14] Revert small BC break Signed-off-by: alexmerlin --- src/LogErrorHandler.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/LogErrorHandler.php b/src/LogErrorHandler.php index bc0ad9b..e0e06a1 100644 --- a/src/LogErrorHandler.php +++ b/src/LogErrorHandler.php @@ -85,15 +85,15 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface * If a valid Logger is available, the error, and it's message are logged in the * configured format. */ - public function handleThrowable(Throwable $throwable, ServerRequestInterface $request): ResponseInterface + public function handleThrowable(Throwable $e, ServerRequestInterface $request): ResponseInterface { $generator = $this->responseGenerator; if ($this->logger instanceof LoggerInterface) { - $this->logger->err($throwable->getMessage(), $this->prepareExtra($throwable, $request)); + $this->logger->err($e->getMessage(), $this->prepareExtra($e, $request)); } - $response = $generator($throwable, $request, ($this->responseFactory)()); - $this->triggerListeners($throwable, $request, $response); + $response = $generator($e, $request, ($this->responseFactory)()); + $this->triggerListeners($e, $request, $response); return $response; } From bb4ebb4914f85a3bdc246235b61c7c23f3fa3eb9 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Wed, 19 Feb 2025 17:30:26 +0200 Subject: [PATCH 03/14] - Code enhancements - Documentation - Tests Signed-off-by: alexmerlin --- .gitignore | 2 + composer.json | 10 +- config/error-handling.global.php.dist | 57 +- config/log.global.php.dist | 17 +- docs/book/v4/extra/cookie.md | 120 +++ docs/book/v4/extra/header.md | 148 ++++ docs/book/v4/extra/introduction.md | 33 + docs/book/v4/extra/request.md | 127 ++++ docs/book/v4/extra/server.md | 148 ++++ docs/book/v4/extra/session.md | 113 +++ docs/book/v4/extra/trace.md | 98 +++ docs/book/v4/log-files.md | 21 + mkdocs.yml | 11 +- phpcs.xml | 1 + src/ErrorHandlerFactory.php | 16 +- src/Extra/ExtraProvider.php | 68 +- src/Extra/Processor/AbstractProcessor.php | 70 ++ src/Extra/Processor/CookieProcessor.php | 28 +- src/Extra/Processor/HeaderProcessor.php | 32 +- src/Extra/Processor/ProcessorInterface.php | 16 + src/Extra/Processor/RequestProcessor.php | 35 +- src/Extra/Processor/ServerProcessor.php | 61 +- src/Extra/Processor/SessionProcessor.php | 7 +- src/Extra/Processor/TraceProcessor.php | 7 +- src/Extra/Provider/AbstractProvider.php | 28 + src/Extra/Provider/CookieProvider.php | 14 +- src/Extra/Provider/HeaderProvider.php | 14 +- src/Extra/Provider/ProviderInterface.php | 16 + src/Extra/Provider/RequestProvider.php | 14 +- src/Extra/Provider/ServerProvider.php | 14 +- src/Extra/Provider/SessionProvider.php | 14 +- src/Extra/Provider/TraceProvider.php | 14 +- src/Extra/ReplacementStrategy.php | 11 + src/LogErrorHandler.php | 28 +- src/LogErrorHandlerFactory.php | 17 +- test/ErrorHandlerFactoryTest.php | 2 +- test/ErrorHandlerTest.php | 12 +- test/Extra/ExtraProviderTest.php | 820 +++++++++++++++++++++ test/LogErrorHandlerFactoryTest.php | 123 +++- test/LogErrorHandlerTest.php | 130 +++- 40 files changed, 2335 insertions(+), 182 deletions(-) create mode 100644 docs/book/v4/extra/cookie.md create mode 100644 docs/book/v4/extra/header.md create mode 100644 docs/book/v4/extra/introduction.md create mode 100644 docs/book/v4/extra/request.md create mode 100644 docs/book/v4/extra/server.md create mode 100644 docs/book/v4/extra/session.md create mode 100644 docs/book/v4/extra/trace.md create mode 100644 docs/book/v4/log-files.md create mode 100644 src/Extra/Processor/AbstractProcessor.php create mode 100644 src/Extra/Provider/AbstractProvider.php create mode 100644 src/Extra/Provider/ProviderInterface.php create mode 100644 src/Extra/ReplacementStrategy.php create mode 100644 test/Extra/ExtraProviderTest.php diff --git a/.gitignore b/.gitignore index d3096f4..d0c6610 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ clover.xml coveralls-upload.json .phpunit.result.cache .phpcs-cache +config/log.global.php +config/error-handling.global.php # Created by .ignore support plugin (hsz.mobi) ### JetBrains template diff --git a/composer.json b/composer.json index 35c6aa8..185a6c0 100644 --- a/composer.json +++ b/composer.json @@ -6,18 +6,14 @@ "homepage": "https://github.com/dotkernel/dot-errorhandler", "authors": [ { - "name": "DotKernel Team", + "name": "Dotkernel Team", "email": "team@dotkernel.com" } ], "keywords": [ "error", "errorhandler", - "factories", - "container", - "laminas", - "mezzio", - "service-manager" + "error_log" ], "config": { "sort-packages": true, @@ -27,7 +23,7 @@ }, "require": { "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "dotkernel/dot-log": "^4.1.0", + "dotkernel/dot-log": "^5.0", "laminas/laminas-diactoros": "^3.3", "laminas/laminas-stdlib": "^3.20", "laminas/laminas-stratigility": "^3.11", diff --git a/config/error-handling.global.php.dist b/config/error-handling.global.php.dist index 44d68d4..ac7908a 100644 --- a/config/error-handling.global.php.dist +++ b/config/error-handling.global.php.dist @@ -3,9 +3,12 @@ declare(strict_types=1); use Dot\ErrorHandler\ErrorHandlerInterface; +use Dot\ErrorHandler\Extra\ExtraProvider; use Dot\ErrorHandler\Extra\Processor\CookieProcessor; use Dot\ErrorHandler\Extra\Processor\HeaderProcessor; +use Dot\ErrorHandler\Extra\Processor\ProcessorInterface; use Dot\ErrorHandler\Extra\Processor\RequestProcessor; +use Dot\ErrorHandler\Extra\Processor\ServerProcessor; use Dot\ErrorHandler\Extra\Processor\SessionProcessor; use Dot\ErrorHandler\Extra\Processor\TraceProcessor; use Dot\ErrorHandler\Extra\Provider\CookieProvider; @@ -14,6 +17,7 @@ use Dot\ErrorHandler\Extra\Provider\RequestProvider; use Dot\ErrorHandler\Extra\Provider\ServerProvider; use Dot\ErrorHandler\Extra\Provider\SessionProvider; use Dot\ErrorHandler\Extra\Provider\TraceProvider; +use Dot\ErrorHandler\Extra\ReplacementStrategy; use Dot\ErrorHandler\LogErrorHandler; return [ @@ -23,32 +27,57 @@ return [ ], ], 'dot-errorhandler' => [ - 'loggerEnabled' => true, - 'logger' => 'dot-log.default_logger', - 'loggerExtra' => [ + 'loggerEnabled' => true, + 'logger' => 'dot-log.default_logger', + ExtraProvider::CONFIG_KEY => [ CookieProvider::class => [ - 'enabled' => true, - 'processor' => CookieProcessor::class, + 'enabled' => false, + 'processor' => [ + 'class' => CookieProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + ProcessorInterface::ALL, + ], + ], ], HeaderProvider::class => [ - 'enabled' => true, - 'processor' => HeaderProcessor::class, + 'enabled' => false, + 'processor' => [ + 'class' => HeaderProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + ], ], RequestProvider::class => [ - 'enabled' => true, - 'processor' => RequestProcessor::class, + 'enabled' => false, + 'processor' => [ + 'class' => RequestProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + 'password', + ], + ], ], ServerProvider::class => [ - 'enabled' => true, - 'processor' => null, + 'enabled' => false, + 'processor' => [ + 'class' => ServerProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + ProcessorInterface::ALL, + ], + ], ], SessionProvider::class => [ - 'enabled' => true, - 'processor' => SessionProcessor::class, + 'enabled' => false, + 'processor' => [ + 'class' => SessionProcessor::class, + ], ], TraceProvider::class => [ 'enabled' => true, - 'processor' => TraceProcessor::class, + 'processor' => [ + 'class' => TraceProcessor::class, + ], ], ], ], diff --git a/config/log.global.php.dist b/config/log.global.php.dist index f8090fd..146b29b 100644 --- a/config/log.global.php.dist +++ b/config/log.global.php.dist @@ -1,27 +1,32 @@ [ 'loggers' => [ 'default_logger' => [ 'writers' => [ 'FileWriter' => [ - 'name' => 'stream', - 'priority' => \Dot\Log\Logger::ALERT, + 'name' => 'stream', + 'level' => Logger::ALERT, 'options' => [ 'stream' => __DIR__ . '/../../log/error-log-{Y}-{m}-{d}.log', // explicitly log all messages - 'filters' => [ + 'filters' => [ 'allMessages' => [ - 'name' => 'priority', + 'name' => 'level', 'options' => [ 'operator' => '>=', - 'priority' => \Dot\Log\Logger::EMERG, + 'level' => Logger::EMERG, ], ], ], 'formatter' => [ - 'name' => \Dot\Log\Formatter\Json::class, + 'name' => Json::class, ], ], ], diff --git a/docs/book/v4/extra/cookie.md b/docs/book/v4/extra/cookie.md new file mode 100644 index 0000000..1c0084d --- /dev/null +++ b/docs/book/v4/extra/cookie.md @@ -0,0 +1,120 @@ +# Log cookie data + +Looking at `dot-errorhandler`'s config file, the array found at `CookieProvider::class` allows you to configure the behaviour of this provider: + +- **enabled**: enabled/disable this provider +- **processor**: an array configuring the data processor to be used by the **CookieProvider**: + - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` + - **replacementStrategy**: whether to replace specific cookie values completely or partially + - **sensitiveParameters**: an array of cookies names that may contain sensitive information so their value should be masked partially/completely + +## Configure provider + +By default, **CookieProvider** is disabled. +It can be enabled only by setting **enabled** to **true**. + +If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `cookie`. +If **enabled** is set to **false**, no additional field is added under the `extra` key. + +## Configure processor + +From here, we assume that **enabled** is set to **true**. + +If **processor** is missing/empty, the processor is ignored the provider will log the raw data available. +If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available. + +From here, we assume that **processor**.**class** is valid. + +### Replacement strategy + +This value should be an instance of `Dot\ErrorHandler\Extra\ReplacementStrategy`. + +If **replacementStrategy** is missing/invalid, the default **replacementStrategy** is used, which is `ReplacementStrategy::Full`. +Else, the value used should be one of: + +- `ReplacementStrategy::Partial` for half-string replacements (e.g.: "abcdef" becomes "abc***") +- `ReplacementStrategy::Full` for full-string replacements (e.g.: "abcdef" becomes "******") + +### Sensitive parameters + +If **sensitiveParameters** is missing/empty, the processor is ignored the provider will log the raw data available. +This is because without a set of **sensitiveParameters**, the processor is unable to determine which key needs to be processed or left untouched. +When specifying the array of **sensitiveParameters**, there are two possibilities: + +- use the constant `ProcessorInterface::ALL`, meaning alter all cookie values using the strategy specified by the **replacementStrategy** + +```php +'sensitiveParameters' => [ + Dot\ErrorHandler\Extra\Processor\ProcessorInterface::ALL, +], +``` + +- use exact strings to list the cookies for which the values should be altered using the strategy specified by the **replacementStrategy** + +```php +'sensitiveParameters' => [ + 'rememberMe', +], +``` + +> **CookieProcessor** uses EXACT cookie name lookups. +> In order to alter the value of a cookie, you need to specify the exact cookie name. + +> The config `sensitiveParameters` is case-insensitive. + +## Why should I use a processor + +Consider the following request cookies: + +```text +[ + "sessionId" => "feb21b39f9c54e3a49af1f862acc8300", + "language" => "en", +] +``` + +Without a **CookieProcessor**, the plain text session cookie identifier would end up saved in the log file: + +```text +..."extra":{"file":"/path/to/some/class.php","line":314,"cookie":{"sessionId":"feb21b39f9c54e3a49af1f862acc8300","language":"en"},... +``` + +But, with a properly configured **CookieProcessor**: + +```php +'processor' => [ + 'class' => CookieProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + 'sessionId', + ], +], +``` + +the logged cookie data becomes: + +```text +..."extra":{"file":"/path/to/some/class.php","line":314,"cookie":{"sessionId":"********************************","language":"en"},... +``` + +## Custom processor + +If the existing processor does not offer enough features, you can create a custom processor. +The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`. +Once the custom processor is ready, you need to configure **CookieProvider** to use it. +For this, open `dot-errorhandler`'s config file and - under **CookieProvider::class** - set **processor**.**class** to the class string of your custom processor: + +```php +CookieProvider::class => [ + 'enabled' => false, + 'processor' => [ + 'class' => CustomCookieProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + ProcessorInterface::ALL, + ], + ], +], +``` + +Using this, cookie data will be processed by `CustomCookieProcessor` and logged as provided by this new processor. diff --git a/docs/book/v4/extra/header.md b/docs/book/v4/extra/header.md new file mode 100644 index 0000000..f6bd033 --- /dev/null +++ b/docs/book/v4/extra/header.md @@ -0,0 +1,148 @@ +# Log header data + +Looking at `dot-errorhandler`'s config file, the array found at `HeaderProvider::class` allows you to configure the behaviour of this provider: + +- **enabled**: enabled/disable this provider +- **processor**: an array configuring the data processor to be used by the **HeaderProvider**: + - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` + - **replacementStrategy**: whether to replace specific header values completely or partially + - **sensitiveParameters**: an array of headers names that may contain sensitive information so their value should be masked partially/completely + +## Configure provider + +By default, **HeaderProvider** is disabled. +It can be enabled only by setting **enabled** to **true**. + +If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `header`. +If **enabled** is set to **false**, no additional field is added under the `extra` key. + +## Configure processor + +From here, we assume that **enabled** is set to **true**. + +If **processor** is missing/empty, the processor is ignored the provider will log the raw data available. +If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available. + +From here, we assume that **processor**.**class** is valid. + +### Replacement strategy + +This value should be an instance of `Dot\ErrorHandler\Extra\ReplacementStrategy`. + +If **replacementStrategy** is missing/invalid, the default **replacementStrategy** is used, which is `ReplacementStrategy::Full`. +Else, the value used should be one of: + +- `ReplacementStrategy::Partial` for half-string replacements (e.g.: "abcdef" becomes "abc***") +- `ReplacementStrategy::Full` for full-string replacements (e.g.: "abcdef" becomes "******") + +### Sensitive parameters + +If **sensitiveParameters** is missing/empty, the processor is ignored the provider will log the raw data available. +This is because without a set of **sensitiveParameters**, the processor is unable to determine which key needs to be processed or left untouched. +When specifying the array of **sensitiveParameters**, there are two possibilities: + +- use the constant `ProcessorInterface::ALL`, meaning alter all header values using the strategy specified by the **replacementStrategy** + +```php +'sensitiveParameters' => [ + Dot\ErrorHandler\Extra\Processor\ProcessorInterface::ALL, +], +``` + +- use exact strings to list the headers for which the values should be altered using the strategy specified by the **replacementStrategy** + +```php +'sensitiveParameters' => [ + 'Authorization', +], +``` + +> **HeaderProcessor** uses EXACT header name lookups. +> In order to alter the value of a header, you need to specify the exact header name. + +> The config `sensitiveParameters` is case-insensitive. + +## Why should I use a processor + +Consider the following request headers: + +```text +[ + "Authorization" => "Bearer 63560eb4398d21024b32f2fb45dacca512db0bc725149e1371f493063a03e687", + "Content-Type" => "application/json", +] +``` + +Without a **HeaderProcessor**, the plain text auth token would end up saved in the log file: + +```text +..."extra":{"file":"/path/to/some/class.php","line":314,"header":{"Authorization":"Bearer 63560eb4398d21024b32f2fb45dacca512db0bc725149e1371f493063a03e687","Content-Type":"application/json"},... +``` + +But, with a properly configured **HeaderProcessor**: + +```php +'processor' => [ + 'class' => HeaderProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + 'Authorization', + ], +], +``` + +the logged header data becomes: + +```text +..."extra":{"file":"/path/to/some/class.php","line":314,"header":{"Authorization":"***********************************************************************","Content-Type":"application/json"},... +``` + +## Special case + +There is a special case, the `cookie` header, which is handled differently than the rest of the headers. + +Let's take an example of a cookie header: + +```text +FRONTEND_SESSID=feb21b39f9c54e3a49af1f862acc8300; rememberMe=63560eb4398d21024b32f2fb45dacca512db0bc725149e1371f493063a03e687 +``` + +If the existing **HeaderProcessor** is not used, then the log file will contain dangerous data that may compromise user security - in this case exposing the value of both cookies. + +> To avoid this issue, the developer should never use **HeaderProvider** without a **HeaderProcessor** in a production environment. + +Depending on **HeaderProvider**'s configuration, **HeaderProcessor** will partially mask the cookie values: + +```text +FRONTEND_SESSID=feb21b39f9c54e3a****************; rememberMe=63560eb4398d21024b32f2****************************************** +``` + +when using `ReplacementStrategy::Partial` or completely mask the cookie values: + +```text +FRONTEND_SESSID=********************************; rememberMe=**************************************************************** +``` + +when using `ReplacementStrategy::Full`. + +## Custom processor + +If the existing processor does not offer enough features, you can create a custom processor. +The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`. +Once the custom processor is ready, you need to configure **HeaderProvider** to use it. +For this, open `dot-errorhandler`'s config file and - under **HeaderProvider::class** - set **processor**.**class** to the class string of your custom processor: + +```php +HeaderProvider::class => [ + 'enabled' => false, + 'processor' => [ + 'class' => CustomHeaderProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + ProcessorInterface::ALL, + ], + ], +], +``` + +Using this, header data will be processed by `CustomHeaderProcessor` and logged as provided by this new processor. diff --git a/docs/book/v4/extra/introduction.md b/docs/book/v4/extra/introduction.md new file mode 100644 index 0000000..5c8f6cc --- /dev/null +++ b/docs/book/v4/extra/introduction.md @@ -0,0 +1,33 @@ +# Extra data + +`dot-errorhandler` provides the following data: + +- **cookie**: an array containing the request cookies +- **header**: an array containing the request headers +- **request**: an array containing the request body (**\$_PATCH**/**\$_POST**/**\$_PUT**) +- **server**: an array containing the **\$_SERVER** values +- **session**: an array containing the **\$_SESSION** values +- **trace**: an array containing the full stack trace of the request + +## Configuring extra data + +At this point, you should already have `dot-errorhandler` configured. +If not, proceed to the [Configuration](../configuration.md) page and when you're done, return to this page. + +In order start logging one or more of the above data, we first need to enable them from the package's config file. +Open the config file and under `dot-errorhandler` locate the `ExtraProvider::CONFIG_KEY` key. +There you will find 6 associative arrays, each array representing a set of data this package can provide: + +- **CookieProvider::class**: request cookie information - [how to use](cookie.md) +- **HeaderProvider::class**: request header information - [how to use](header.md) +- **RequestProvider::class**: request body contents - [how to use](request.md) +- **ServerProvider::class**: server information - [how to use](server.md) +- **SessionProvider::class**: session contents - [how to use](session.md) +- **TraceProvider::class**: trace route - [how to use](trace.md) + +These providers and their configuration values are all optional. +If any of them is missing from the config file, the provider simply stays disabled and code execution continues normally. +Invalid configuration values are simply ignored, because the purpose of these providers is to log the extra data if possible, but not to interfere with the app logic. +That's why it does not throw errors on missing/invalid config values. + +> By default, `*Provider` classes are used in fall-through mode; without a processor, they just return the unaltered data they were given. diff --git a/docs/book/v4/extra/request.md b/docs/book/v4/extra/request.md new file mode 100644 index 0000000..6b87275 --- /dev/null +++ b/docs/book/v4/extra/request.md @@ -0,0 +1,127 @@ +# Log request data + +Looking at `dot-errorhandler`'s config file, the array found at `RequestProvider::class` allows you to configure the behaviour of this provider: + +- **enabled**: enabled/disable this provider +- **processor**: an array configuring the data processor to be used by the **RequestProvider**: + - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` + - **replacementStrategy**: whether to replace specific cookie values completely or partially + - **sensitiveParameters**: an array of cookies names that may contain sensitive information so their value should be masked partially/completely + +## Configure provider + +By default, **RequestProvider** is disabled. +It can be enabled only by setting **enabled** to **true**. + +If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `request`. +If **enabled** is set to **false**, no additional field is added under the `extra` key. + +## Configure processor + +From here, we assume that **enabled** is set to **true**. + +If **processor** is missing/empty, the processor is ignored the provider will log the raw data available. +If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available. + +From here, we assume that **processor**.**class** is valid. + +### Replacement strategy + +This value should be an instance of `Dot\ErrorHandler\Extra\ReplacementStrategy`. + +If **replacementStrategy** is missing/invalid, the default **replacementStrategy** is used, which is `ReplacementStrategy::Full`. +Else, the value used should be one of: + +- `ReplacementStrategy::Partial` for half-string replacements (e.g.: "abcdef" becomes "abc***") +- `ReplacementStrategy::Full` for full-string replacements (e.g.: "abcdef" becomes "******") + +### Sensitive parameters + +If **sensitiveParameters** is missing/empty, the processor is ignored the provider will log the raw data available. +This is because without a set of **sensitiveParameters**, the processor is unable to determine which key needs to be processed or left untouched. +When specifying the array of **sensitiveParameters**, there are two possibilities: + +- use the constant `ProcessorInterface::ALL`, meaning alter all cookie values using the strategy specified by the **replacementStrategy** + +```php +'sensitiveParameters' => [ + Dot\ErrorHandler\Extra\Processor\ProcessorInterface::ALL, +], +``` + +- use exact strings to list the cookies for which the values should be altered using the strategy specified by the **replacementStrategy** + +```php +'sensitiveParameters' => [ + 'password', +], +``` + +> **RequestProcessor** uses recursive search to locate all array keys in a multidimensional array. + +> **RequestProcessor** uses PARTIAL field name lookups. +> In order to alter the value of a request field, it is enough to specify only part of the field name. + +> The config `sensitiveParameters` is case-insensitive. + +## Why should I use a processor + +Consider the following request body sent via **$_POST**: + +```text +[ + "identity" => "myIdentity", + "password" => "p4$$w0rd", + "passwordConfirm" => "p4$$w0rd", + "details" => [ + "secret" => "s3cr3t", + ] +] +``` + +Without a **RequestProcessor**, the plain text passwords would end up saved in the log file: + +```text +..."extra":{"file":"/path/to/some/class.php","line":314,"request":{"identity":"myIdentity","password":"p4$$w0rd","passwordConfirm":"p4$$w0rd","details":{"secret":"s3cr3t"}}},... +``` + +But, with a properly configured **RequestProcessor**: + +```php +'processor' => [ + 'class' => RequestProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + 'password', + 'secret', + ], +], +``` + +the logged request data becomes: + +```text +..."extra":{"file":"/path/to/some/class.php","line":314,"request":{"identity":"myIdentity","password":"********","passwordConfirm":"********","details":{"secret":"******"}}},... +``` + +## Custom processor + +If the existing processor does not offer enough features, you can create a custom processor. +The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`. +Once the custom processor is ready, you need to configure **RequestProvider** to use it. +For this, open `dot-errorhandler`'s config file and - under **RequestProvider::class** - set **processor**.**class** to the class string of your custom processor: + +```php +RequestProvider::class => [ + 'enabled' => false, + 'processor' => [ + 'class' => CustomRequestProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + ProcessorInterface::ALL, + ], + ], +], +``` + +Using this, request data will be processed by `CustomRequestProcessor` and logged as provided by this new processor. diff --git a/docs/book/v4/extra/server.md b/docs/book/v4/extra/server.md new file mode 100644 index 0000000..956cb7f --- /dev/null +++ b/docs/book/v4/extra/server.md @@ -0,0 +1,148 @@ +# Log server data + +Looking at `dot-errorhandler`'s config file, the array found at `ServerProvider::class` allows you to configure the behaviour of this provider: + +- **enabled**: enabled/disable this provider +- **processor**: an array configuring the data processor to be used by the **ServerProvider**: + - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` + - **replacementStrategy**: whether to replace specific server config values completely or partially + - **sensitiveParameters**: an array of server config names that may contain sensitive information so their value should be masked partially/completely + +## Configure provider + +By default, **ServerProvider** is disabled. +It can be enabled only by setting **enabled** to **true**. + +If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `server`. +If **enabled** is set to **false**, no additional field is added under the `extra` key. + +## Configure processor + +From here, we assume that **enabled** is set to **true**. + +If **processor** is missing/empty, the processor is ignored the provider will log the raw data available. +If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available. + +From here, we assume that **processor**.**class** is valid. + +### Replacement strategy + +This value should be an instance of `Dot\ErrorHandler\Extra\ReplacementStrategy`. + +If **replacementStrategy** is missing/invalid, the default **replacementStrategy** is used, which is `ReplacementStrategy::Full`. +Else, the value used should be one of: + +- `ReplacementStrategy::Partial` for half-string replacements (e.g.: "abcdef" becomes "abc***") +- `ReplacementStrategy::Full` for full-string replacements (e.g.: "abcdef" becomes "******") + +### Sensitive parameters + +If **sensitiveParameters** is missing/empty, the processor is ignored the provider will log the raw data available. +This is because without a set of **sensitiveParameters**, the processor is unable to determine which key needs to be processed or left untouched. +When specifying the array of **sensitiveParameters**, there are two possibilities: + +- use the constant `ProcessorInterface::ALL`, meaning alter all server config values using the strategy specified by the **replacementStrategy** + +```php +'sensitiveParameters' => [ + Dot\ErrorHandler\Extra\Processor\ProcessorInterface::ALL, +], +``` + +- use exact strings to list the server configs for which the values should be altered using the strategy specified by the **replacementStrategy** + +```php +'sensitiveParameters' => [ + 'SERVER_ADMIN', +], +``` + +> **ServerProcessor** uses EXACT server config name lookups. +> In order to alter the value of a server config, you need to specify the exact server config name. + +> The config `sensitiveParameters` is case-insensitive. + +## Why should I use a processor + +Consider the following request server configs: + +```text +[ + "SERVER_ADMIN" => "webmaster@localhost", + "SERVER_SOFTWARE" => "Apache/2.4.62 (AlmaLinux)", +] +``` + +Without a **ServerProcessor**, (for the purpose of this example) the key **SERVER_ADMIN** would end up saved in the log file: + +```text +..."extra":{"file":"/path/to/some/class.php","line":314,"server":{"SERVER_ADMIN":"webmaster@localhost","SERVER_SOFTWARE":"Apache/2.4.62 (AlmaLinux)"},... +``` + +But, with a properly configured **ServerProcessor**: + +```php +'processor' => [ + 'class' => ServerProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + 'SERVER_ADMIN', + ], +], +``` + +the logged server config data becomes: + +```text +..."extra":{"file":"/path/to/some/class.php","line":314,"server":{"SERVER_ADMIN":"*******************","SERVER_SOFTWARE":"Apache/2.4.62 (AlmaLinux)"},... +``` + +## Special case + +There is a special case, the `HTTP_COOKIE` server config value, which is handled differently than the rest of the values. + +Let's take an example of a **HTTP_COOKIE** value: + +```text +FRONTEND_SESSID=feb21b39f9c54e3a49af1f862acc8300; rememberMe=63560eb4398d21024b32f2fb45dacca512db0bc725149e1371f493063a03e687 +``` + +If the existing **ServerProcessor** is not used, then the log file will contain dangerous data that may compromise user security - in this case exposing the value of both cookies. + +> To avoid this issue, the developer should never use **ServerProvider** without a **ServerProcessor** in a production environment. + +Depending on **ServerProvider**'s configuration, **ServerProcessor** will partially mask the cookie values: + +```text +FRONTEND_SESSID=feb21b39f9c54e3a****************; rememberMe=63560eb4398d21024b32f2****************************************** +``` + +when using `ReplacementStrategy::Partial` or completely mask the cookie values: + +```text +FRONTEND_SESSID=********************************; rememberMe=**************************************************************** +``` + +when using `ReplacementStrategy::Full`. + +## Custom processor + +If the existing processor does not offer enough features, you can create a custom processor. +The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`. +Once the custom processor is ready, you need to configure **ServerProvider** to use it. +For this, open `dot-errorhandler`'s config file and - under **ServerProvider::class** - set **processor**.**class** to the class string of your custom processor: + +```php +ServerProvider::class => [ + 'enabled' => false, + 'processor' => [ + 'class' => CustomServerProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + ProcessorInterface::ALL, + ], + ], +], +``` + +Using this, server config data will be processed by `CustomServerProcessor` and logged as provided by this new processor. diff --git a/docs/book/v4/extra/session.md b/docs/book/v4/extra/session.md new file mode 100644 index 0000000..c18b8e5 --- /dev/null +++ b/docs/book/v4/extra/session.md @@ -0,0 +1,113 @@ +# Log session data + +Looking at `dot-errorhandler`'s config file, the array found at `SessionProvider::class` allows you to configure the behaviour of this provider: + +- **enabled**: enabled/disable this provider +- **processor**: an array configuring the data processor to be used by the **SessionProvider**: + - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` + +## Configure provider + +By default, **SessionProvider** is disabled. +It can be enabled only by setting **enabled** to **true**. + +If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `session`. +If **enabled** is set to **false**, no additional field is added under the `extra` key. + +## Configure processor + +From here, we assume that **enabled** is set to **true**. + +If **processor** is missing/empty, the processor is ignored the provider will log the raw data available. +If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available. + +From here, we assume that **processor**.**class** is valid. + +### Replacement strategy + +**SessionProcessor** does not use **replacementStrategy**. + +> If you create a custom session processor, you may add **replacementStrategy** to the config file, it will get passed to your processor. + +### Sensitive parameters + +**SessionProcessor** does not use **sensitiveParameters**. + +> If you create a custom session processor, you may add **sensitiveParameters** to the config file, it will get passed to your processor. + +## Why should I use a processor + +The only advantage of using **SessionProcessor** is to reduce the size of the session data that you log. +In the below example, **$_SESSION** contains an **array** in _SessionContainer1_ and an **ArrayObject** in _SessionContainer2_. + +```text + "SessionContainer1" => array:3 [▼ + "_REQUEST_ACCESS_TIME" => 1739973274.7284 + "_VALID" => array:1 [▼ + "Laminas\Session\Validator\Id" => "feb21b39f9c54e3a49af1f862acc8300" + ] + "Default" => array:1 [▼ + "EXPIRE" => 1739969278 + ] + ] + "SessionContainer2" => Laminas\Stdlib\ArrayObject {#795 ▼ + #storage: array:2 [▼ + "tokenList" => array:1 [▼ + "b222251fb72d49ae0643bff11e11057d" => "389b5e5b415409abb61cfe718e7841bf" + ] + "hash" => "389b5e5b415409abb61cfe718e7841bf-b222251fb72d49ae0643bff11e11057d" + ] + #flag: 2 + #iteratorClass: "ArrayIterator" + #protectedProperties: array:4 [▼ + 0 => "storage" + 1 => "flag" + 2 => "iteratorClass" + 3 => "protectedProperties" + ] + } +``` + +By using **SessionProcessor**, the two items are reduced to: + +```text + "SessionContainer1" => array:3 [▼ + "_REQUEST_ACCESS_TIME" => 1739973274.7284 + "_VALID" => array:1 [▼ + "Laminas\Session\Validator\Id" => "feb21b39f9c54e3a49af1f862acc8300" + ] + "Default" => array:1 [▼ + "EXPIRE" => 1739969278 + ] + ] + "SessionContainer2" => array:2 [▼ + "tokenList" => array:1 [▼ + "b222251fb72d49ae0643bff11e11057d" => "389b5e5b415409abb61cfe718e7841bf" + ] + "hash" => "389b5e5b415409abb61cfe718e7841bf-b222251fb72d49ae0643bff11e11057d" + ] +``` + +As you can see, with arrays the difference is almost ignorable, but with ArrayObjects, the difference is significant. + +## Custom processor + +If the existing processor does not offer enough features, you can create a custom processor. +The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`. +Once the custom processor is ready, you need to configure **SessionProvider** to use it. +For this, open `dot-errorhandler`'s config file and - under **SessionProvider::class** - set **processor**.**class** to the class string of your custom processor: + +```php +SessionProvider::class => [ + 'enabled' => false, + 'processor' => [ + 'class' => CustomSessionProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + ProcessorInterface::ALL, + ], + ], +], +``` + +Using this, cookie data will be processed by `CustomSessionProcessor` and logged as provided by this new processor. diff --git a/docs/book/v4/extra/trace.md b/docs/book/v4/extra/trace.md new file mode 100644 index 0000000..abe489d --- /dev/null +++ b/docs/book/v4/extra/trace.md @@ -0,0 +1,98 @@ +# Log trace data + +Looking at `dot-errorhandler`'s config file, the array found at `TraceProvider::class` allows you to configure the behaviour of this provider: + +- **enabled**: enabled/disable this provider +- **processor**: an array configuring the data processor to be used by the **TraceProvider**: + - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` + +## Configure provider + +By default, **TraceProvider** is enabled. +It can be enabled only by setting **enabled** to **true**. + +If **enabled** is set to **true**, your log file will contain an additional field under the `extra` key, called `trace`. +If **enabled** is set to **false**, no additional field is added under the `extra` key. + +## Configure processor + +From here, we assume that **enabled** is set to **true**. + +If **processor** is missing/empty, the processor is ignored the provider will log the raw data available. +If **processor** is specified, but **class** is missing/invalid, the processor is ignored and the provider will log the raw data available. + +From here, we assume that **processor**.**class** is valid. + +### Replacement strategy + +**TraceProcessor** does not use **replacementStrategy**. + +> If you create a custom session processor, you may add **replacementStrategy** to the config file, it will get passed to your processor. + +### Sensitive parameters + +**TraceProcessor** does not use **sensitiveParameters**. + +> If you create a custom session processor, you may add **sensitiveParameters** to the config file, it will get passed to your processor. + +## Why should I use a processor + +The only advantage of using **TraceProcessor** is to reduce the size of the trace data that you log. +In the below example, the trace route contains an array with dozens of items, each with the same structure (except for the last one). + +```text + 0 => array:5 [▼ + "file" => "/path/to/some/file.php" + "line" => 66 + "function" => "someFunction" + "class" => "Path\To\Some\File" + "type" => "->" + ] + 1 => array:5 [▼ + "file" => "/path/to/some/other/file.php" + "line" => 33 + "function" => "someOtherFunction" + "class" => "Path\To\Some\Other\File" + "type" => "->" + ] + ... + 58 => array:3 [▼ + "file" => "/path/to/index.php" + "line" => 36 + "function" => "{closure}" + ] +``` + +By using **TraceProcessor**, the array are reduced to: + +```text + 0 => "Path\To\Some\File->someFunction:66" + 1 => "Path\To\Some\Other\File->someOtherFunction:33" + ... + 58 => "/path/to/index.php->{closure}:36" +``` + +Yet, it maintains the relevant information, but in a compact form. +This works because, since using namespaced classes, it's easy to determine file paths just by looking at a FQCN. + +## Custom processor + +If the existing processor does not offer enough features, you can create a custom processor. +The custom processor must implement `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` or extend `Dot\ErrorHandler\Extra\Processor\AbstractProcessor`, which already implements `Dot\ErrorHandler\Extra\Processor\ProcessorInterface`. +Once the custom processor is ready, you need to configure **TraceProvider** to use it. +For this, open `dot-errorhandler`'s config file and - under **TraceProvider::class** - set **processor**.**class** to the class string of your custom processor: + +```php +TraceProvider::class => [ + 'enabled' => false, + 'processor' => [ + 'class' => CustomTraceProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + ProcessorInterface::ALL, + ], + ], +], +``` + +Using this, trace data will be processed by `CustomTraceProcessor` and logged as provided by this new processor. diff --git a/docs/book/v4/log-files.md b/docs/book/v4/log-files.md new file mode 100644 index 0000000..57b5b93 --- /dev/null +++ b/docs/book/v4/log-files.md @@ -0,0 +1,21 @@ +# Log files + +## What is a log file + +Log files are plain text files, containing rows of log activity, each row being a standalone activity formatted as specified in `dot-errorhandler`'s configuration file. +By default, log activities are formatted with JSON, so each row should be a decodable JSON string. + +## What is in a log file + +Each row in a log file should contain the following values: + +- **timestamp**: string representation of the date and time when the error occurred +- **priority**: numeric representation of the error level +- **priorityName**: string representation of the error level +- **message**: error message describing the error +- **extra**: an array of extra information that may help the developer debug the error: + - **file**: the file in which the error occurred + - **line**: the line from **file** where the error occurred + +By leveraging `dot-errorhandler`'s extra providers, you can also log additional request parameters. +Learn more about what additional parameters are available on the [extra data](extra-data.md) page. diff --git a/mkdocs.yml b/mkdocs.yml index b95a4df..ca1e232 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,12 +12,21 @@ nav: - Overview: v4/overview.md - Installation: v4/installation.md - Configuration: v4/configuration.md + - "Log files": v4/log-files.md + - "Extra data": + - Introduction: v4/extra/introduction.md + - Cookie: v4/extra/cookie.md + - Header: v4/extra/header.md + - Request: v4/extra/request.md + - Server: v4/extra/server.md + - Session: v4/extra/session.md + - "Trace route": v4/extra/trace.md - v3: - Overview: v3/overview.md - Installation: v3/installation.md - Configuration: v3/configuration.md site_name: dot-errorhandler -site_description: "DotKernel's error logging handler" +site_description: "Dotkernel's error logging handler" repo_url: "https://github.com/dotkernel/dot-errorhandler" plugins: - search diff --git a/phpcs.xml b/phpcs.xml index 1efe663..0d4c1dc 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -12,6 +12,7 @@ + config src test diff --git a/src/ErrorHandlerFactory.php b/src/ErrorHandlerFactory.php index 5f25f70..916e5a9 100644 --- a/src/ErrorHandlerFactory.php +++ b/src/ErrorHandlerFactory.php @@ -10,6 +10,9 @@ use Psr\Container\NotFoundExceptionInterface; use Psr\Http\Message\ResponseInterface; +use function assert; +use function is_callable; + class ErrorHandlerFactory { /** @@ -18,10 +21,15 @@ class ErrorHandlerFactory */ public function __invoke(ContainerInterface $container): ErrorHandler { - $generator = $container->has(ErrorResponseGenerator::class) - ? $container->get(ErrorResponseGenerator::class) - : null; + $generator = null; + if ($container->has(ErrorResponseGenerator::class)) { + $generator = $container->get(ErrorResponseGenerator::class); + assert($generator instanceof ErrorResponseGenerator); + } + + $responseInterface = $container->get(ResponseInterface::class); + assert(is_callable($responseInterface)); - return new ErrorHandler($container->get(ResponseInterface::class), $generator); + return new ErrorHandler($responseInterface, $generator); } } diff --git a/src/Extra/ExtraProvider.php b/src/Extra/ExtraProvider.php index 02c2086..290ce91 100644 --- a/src/Extra/ExtraProvider.php +++ b/src/Extra/ExtraProvider.php @@ -12,13 +12,19 @@ use Dot\ErrorHandler\Extra\Provider\SessionProvider; use Dot\ErrorHandler\Extra\Provider\TraceProvider; +use function array_flip; use function array_key_exists; +use function array_map; +use function assert; use function class_exists; -use function is_bool; +use function count; +use function is_array; use function is_string; class ExtraProvider { + public const CONFIG_KEY = 'extraProvider'; + private CookieProvider $cookie; private HeaderProvider $header; private RequestProvider $request; @@ -37,26 +43,50 @@ public function __construct(array $options = []) 'trace' => TraceProvider::class, ]; - foreach ($extras as $logKey => $logClass) { - $enabled = false; - $processor = null; - if (array_key_exists($logClass, $options)) { - if (isset($options[$logClass]['enabled']) && is_bool($options[$logClass]['enabled'])) { - $enabled = $options[$logClass]['enabled']; - } - if ( - isset($options[$logClass]['processor']) - && is_string($options[$logClass]['processor']) - && class_exists($options[$logClass]['processor']) - ) { - $processor = new $options[$logClass]['processor'](); - if (! $processor instanceof ProcessorInterface) { - $processor = null; - } - } + foreach ($extras as $extraKey => $extraClass) { + if ( + ! array_key_exists($extraClass, $options) + || ! array_key_exists('enabled', $options[$extraClass]) + || $options[$extraClass]['enabled'] === false + ) { + $this->$extraKey = new $extraClass(false); + continue; + } + + if ( + ! array_key_exists('processor', $options[$extraClass]) + || ! is_array($options[$extraClass]['processor']) + || ! array_key_exists('class', $options[$extraClass]['processor']) + || ! is_string($options[$extraClass]['processor']['class']) + || ! class_exists($options[$extraClass]['processor']['class']) + ) { + $this->$extraKey = new $extraClass(true); + continue; + } + + $sensitiveParameters = []; + if ( + array_key_exists('sensitiveParameters', $options[$extraClass]['processor']) + && is_array($options[$extraClass]['processor']['sensitiveParameters']) + && count($options[$extraClass]['processor']['sensitiveParameters']) > 0 + ) { + $sensitiveParameters = $options[$extraClass]['processor']['sensitiveParameters']; + $sensitiveParameters = array_map('strtolower', $sensitiveParameters); + $sensitiveParameters = array_flip($sensitiveParameters); + } + + $replacementStrategy = ReplacementStrategy::Full; + if ( + array_key_exists('replacementStrategy', $options[$extraClass]['processor']) + && $options[$extraClass]['processor']['replacementStrategy'] instanceof ReplacementStrategy + ) { + $replacementStrategy = $options[$extraClass]['processor']['replacementStrategy']; } - $this->$logKey = new $logClass($enabled, $processor); + $processor = new $options[$extraClass]['processor']['class']($sensitiveParameters, $replacementStrategy); + assert($processor instanceof ProcessorInterface); + + $this->$extraKey = new $extraClass(true, $processor); } } diff --git a/src/Extra/Processor/AbstractProcessor.php b/src/Extra/Processor/AbstractProcessor.php new file mode 100644 index 0000000..730869c --- /dev/null +++ b/src/Extra/Processor/AbstractProcessor.php @@ -0,0 +1,70 @@ +sensitiveParameters; + } + + public function getReplacementStrategy(): ReplacementStrategy + { + return $this->replacementStrategy; + } + + public function replace( + ReplacementStrategy $replacementStrategy, + string $subject, + string $replacement = ProcessorInterface::ALL + ): string { + return match ($replacementStrategy->name) { + ReplacementStrategy::Full->name => $this->replaceFull($subject, $replacement), + ReplacementStrategy::Partial->name => $this->replacePartial($subject, $replacement), + }; + } + + private function replaceFull(string $subject, string $replacement = ProcessorInterface::ALL): string + { + return str_repeat($replacement, strlen($subject)); + } + + private function replacePartial(string $subject, string $replacement = ProcessorInterface::ALL): string + { + return str_pad(substr($subject, 0, (int) round(strlen($subject) / 2)), strlen($subject), $replacement); + } + + public function replaceInlineCookieValues(ReplacementStrategy $replacementStrategy, string $header): string + { + return (string) preg_replace_callback( + '/([^=\s;]+)=([^;]*)/', + fn (array $matches): string => sprintf( + '%s=%s', + $matches[1] ?? '', + $this->replace($replacementStrategy, $matches[2] ?? '') + ), + $header + ); + } +} diff --git a/src/Extra/Processor/CookieProcessor.php b/src/Extra/Processor/CookieProcessor.php index 4b8beda..b4d858f 100644 --- a/src/Extra/Processor/CookieProcessor.php +++ b/src/Extra/Processor/CookieProcessor.php @@ -4,15 +4,31 @@ namespace Dot\ErrorHandler\Extra\Processor; -use function array_map; -use function str_pad; -use function strlen; -use function substr; +use function count; +use function strtolower; -class CookieProcessor implements ProcessorInterface +class CookieProcessor extends AbstractProcessor { public function process(array $data): array { - return array_map(fn (string $cookie): string => str_pad(substr($cookie, 0, 8), strlen($cookie), '.'), $data); + if (count($this->sensitiveParameters) === 0 || count($data) === 0) { + return $data; + } + + $return = []; + + foreach ($data as $cookieName => $cookieValue) { + if ( + ! isset($this->sensitiveParameters[ProcessorInterface::ALL]) + && ! isset($this->sensitiveParameters[strtolower($cookieName)]) + ) { + $return[$cookieName] = $cookieValue; + continue; + } + + $return[$cookieName] = $this->replace($this->replacementStrategy, $cookieValue); + } + + return $return; } } diff --git a/src/Extra/Processor/HeaderProcessor.php b/src/Extra/Processor/HeaderProcessor.php index b9f6e39..cd65020 100644 --- a/src/Extra/Processor/HeaderProcessor.php +++ b/src/Extra/Processor/HeaderProcessor.php @@ -4,13 +4,39 @@ namespace Dot\ErrorHandler\Extra\Processor; -use function array_map; +use function count; use function implode; +use function is_array; +use function strtolower; -class HeaderProcessor implements ProcessorInterface +class HeaderProcessor extends AbstractProcessor { public function process(array $data): array { - return array_map(fn (array $headerSet): string => implode("; ", $headerSet), $data); + if (count($data) === 0) { + return $data; + } + + $return = []; + + foreach ($data as $headerName => $headerValue) { + if (is_array($headerValue)) { + $headerValue = implode('; ', $headerValue); + } + if ( + ! isset($this->sensitiveParameters[ProcessorInterface::ALL]) + && ! isset($this->sensitiveParameters[strtolower($headerName)]) + && $headerName !== 'cookie' + ) { + $return[$headerName] = $headerValue; + continue; + } + + $return[$headerName] = $headerName === 'cookie' + ? $this->replaceInlineCookieValues($this->replacementStrategy, $headerValue) + : $this->replace($this->replacementStrategy, (string) $headerValue); + } + + return $return; } } diff --git a/src/Extra/Processor/ProcessorInterface.php b/src/Extra/Processor/ProcessorInterface.php index 1ac7379..e6e4bf7 100644 --- a/src/Extra/Processor/ProcessorInterface.php +++ b/src/Extra/Processor/ProcessorInterface.php @@ -4,7 +4,23 @@ namespace Dot\ErrorHandler\Extra\Processor; +use Dot\ErrorHandler\Extra\ReplacementStrategy; + interface ProcessorInterface { + public const ALL = '*'; + public function process(array $data): array; + + public function getSensitiveParameters(): array; + + public function getReplacementStrategy(): ReplacementStrategy; + + public function replace( + ReplacementStrategy $replacementStrategy, + string $subject, + string $replacement = ProcessorInterface::ALL + ): string; + + public function replaceInlineCookieValues(ReplacementStrategy $replacementStrategy, string $header): string; } diff --git a/src/Extra/Processor/RequestProcessor.php b/src/Extra/Processor/RequestProcessor.php index 8a91b7c..a38af50 100644 --- a/src/Extra/Processor/RequestProcessor.php +++ b/src/Extra/Processor/RequestProcessor.php @@ -4,34 +4,39 @@ namespace Dot\ErrorHandler\Extra\Processor; +use function array_filter; +use function array_keys; +use function count; use function is_array; -use function is_string; -use function preg_replace; use function str_contains; use function strtolower; -class RequestProcessor implements ProcessorInterface +class RequestProcessor extends AbstractProcessor { public function process(array $data): array { + if (count($this->sensitiveParameters) === 0 || count($data) === 0) { + return $data; + } + $return = []; + + $sensitiveParameters = array_keys($this->sensitiveParameters); foreach ($data as $key => $value) { if (is_array($value)) { $return[$key] = $this->process($value); - } elseif (is_string($value)) { - $lowerKey = strtolower($key); - if ( - str_contains($lowerKey, 'password') || - str_contains($lowerKey, 'key') || - str_contains($lowerKey, 'csrf') || - str_contains($lowerKey, 'token') - ) { - $return[$key] = preg_replace('/[\da-z]/i', 'x', $value); - } else { + } else { + $matches = array_filter( + $sensitiveParameters, + fn (string $sensitiveParameter) => str_contains(strtolower($key), $sensitiveParameter) + ); + + if (! isset($this->sensitiveParameters[ProcessorInterface::ALL]) && count($matches) === 0) { $return[$key] = $value; + continue; } - } else { - $return[$key] = $value; + + $return[$key] = $this->replace($this->replacementStrategy, (string) $value); } } diff --git a/src/Extra/Processor/ServerProcessor.php b/src/Extra/Processor/ServerProcessor.php index 4df18eb..da6d708 100644 --- a/src/Extra/Processor/ServerProcessor.php +++ b/src/Extra/Processor/ServerProcessor.php @@ -4,10 +4,67 @@ namespace Dot\ErrorHandler\Extra\Processor; -class ServerProcessor implements ProcessorInterface +use function array_keys; +use function array_map; +use function array_reduce; +use function count; +use function explode; +use function implode; +use function sprintf; +use function strtolower; + +class ServerProcessor extends AbstractProcessor { public function process(array $data): array { - return $data; + if (count($data) === 0) { + return $data; + } + + $return = []; + + foreach ($data as $serverKey => $serverValue) { + if ($serverKey === 'HTTP_COOKIE') { + $serverValue = $this->stringToAssociativeArray($serverValue); + $serverValue = $this->process($serverValue); + $serverValue = $this->associativeArrayToString($serverValue); + + $return[$serverKey] = $serverValue; + continue; + } + + if ( + ! isset($this->sensitiveParameters[ProcessorInterface::ALL]) + && ! isset($this->sensitiveParameters[strtolower($serverKey)]) + ) { + $return[$serverKey] = $serverValue; + continue; + } + + $return[$serverKey] = $this->replace($this->replacementStrategy, $serverValue); + } + + return $return; + } + + private function stringToAssociativeArray(string $subject): array + { + return array_reduce(explode('; ', $subject), function (array $result, string $keyValue): array { + $keyValue = explode('=', $keyValue, 2); + $result[$keyValue[0]] = $keyValue[1] ?? ''; + + return $result; + }, []); + } + + private function associativeArrayToString(array $subject): string + { + $subject = array_map( + fn(string $key, string $value) => sprintf('%s=%s', $key, $value), + array_keys($subject), + $subject + ); + + return implode('; ', $subject); } } diff --git a/src/Extra/Processor/SessionProcessor.php b/src/Extra/Processor/SessionProcessor.php index 040999d..f02a570 100644 --- a/src/Extra/Processor/SessionProcessor.php +++ b/src/Extra/Processor/SessionProcessor.php @@ -8,10 +8,13 @@ use function array_map; -class SessionProcessor implements ProcessorInterface +class SessionProcessor extends AbstractProcessor { public function process(array $data): array { - return array_map(fn ($container): array => ArrayUtils::iteratorToArray($container), $data); + return array_map( + fn (iterable $container): array => ArrayUtils::iteratorToArray($container), + $data + ); } } diff --git a/src/Extra/Processor/TraceProcessor.php b/src/Extra/Processor/TraceProcessor.php index b72260a..ec632c7 100644 --- a/src/Extra/Processor/TraceProcessor.php +++ b/src/Extra/Processor/TraceProcessor.php @@ -7,12 +7,15 @@ use function array_map; use function sprintf; -class TraceProcessor implements ProcessorInterface +class TraceProcessor extends AbstractProcessor { public function process(array $data): array { return array_map( - fn ($trace): string => sprintf( + /** + * @param string[] $trace + */ + fn (array $trace): string => sprintf( '%s%s%s:%d', $trace['class'] ?? $trace['file'] ?? 'unknown', $trace['type'] ?? '->', diff --git a/src/Extra/Provider/AbstractProvider.php b/src/Extra/Provider/AbstractProvider.php new file mode 100644 index 0000000..dc45c56 --- /dev/null +++ b/src/Extra/Provider/AbstractProvider.php @@ -0,0 +1,28 @@ +enabled; + } + + public function getProcessor(): ?ProcessorInterface + { + return $this->processor; + } + + abstract public function provide(array $data): array; +} diff --git a/src/Extra/Provider/CookieProvider.php b/src/Extra/Provider/CookieProvider.php index 34d792e..9e68fb3 100644 --- a/src/Extra/Provider/CookieProvider.php +++ b/src/Extra/Provider/CookieProvider.php @@ -6,20 +6,14 @@ use Dot\ErrorHandler\Extra\Processor\ProcessorInterface; -readonly class CookieProvider +class CookieProvider extends AbstractProvider { - public function __construct( - public bool $enabled = false, - public ?ProcessorInterface $processor = null, - ) { - } - - public function provide(array $cookies): array + public function provide(array $data): array { if ($this->processor instanceof ProcessorInterface) { - return $this->processor->process($cookies); + return $this->processor->process($data); } - return $cookies; + return $data; } } diff --git a/src/Extra/Provider/HeaderProvider.php b/src/Extra/Provider/HeaderProvider.php index 280116b..5d838aa 100644 --- a/src/Extra/Provider/HeaderProvider.php +++ b/src/Extra/Provider/HeaderProvider.php @@ -6,20 +6,14 @@ use Dot\ErrorHandler\Extra\Processor\ProcessorInterface; -readonly class HeaderProvider +class HeaderProvider extends AbstractProvider { - public function __construct( - public bool $enabled = false, - public ?ProcessorInterface $processor = null, - ) { - } - - public function provide(array $headers): array + public function provide(array $data): array { if ($this->processor instanceof ProcessorInterface) { - return $this->processor->process($headers); + return $this->processor->process($data); } - return $headers; + return $data; } } diff --git a/src/Extra/Provider/ProviderInterface.php b/src/Extra/Provider/ProviderInterface.php new file mode 100644 index 0000000..ce3c9f5 --- /dev/null +++ b/src/Extra/Provider/ProviderInterface.php @@ -0,0 +1,16 @@ +processor instanceof ProcessorInterface) { - return $this->processor->process($request); + return $this->processor->process($data); } - return $request; + return $data; } } diff --git a/src/Extra/Provider/ServerProvider.php b/src/Extra/Provider/ServerProvider.php index 4ea967d..2b196a4 100644 --- a/src/Extra/Provider/ServerProvider.php +++ b/src/Extra/Provider/ServerProvider.php @@ -6,20 +6,14 @@ use Dot\ErrorHandler\Extra\Processor\ProcessorInterface; -readonly class ServerProvider +class ServerProvider extends AbstractProvider { - public function __construct( - public bool $enabled = false, - public ?ProcessorInterface $processor = null, - ) { - } - - public function provide(array $server): array + public function provide(array $data): array { if ($this->processor instanceof ProcessorInterface) { - return $this->processor->process($server); + return $this->processor->process($data); } - return $server; + return $data; } } diff --git a/src/Extra/Provider/SessionProvider.php b/src/Extra/Provider/SessionProvider.php index a1c42f8..09b4449 100644 --- a/src/Extra/Provider/SessionProvider.php +++ b/src/Extra/Provider/SessionProvider.php @@ -6,20 +6,14 @@ use Dot\ErrorHandler\Extra\Processor\ProcessorInterface; -readonly class SessionProvider +class SessionProvider extends AbstractProvider { - public function __construct( - public bool $enabled = false, - public ?ProcessorInterface $processor = null, - ) { - } - - public function provide(array $session): array + public function provide(array $data): array { if ($this->processor instanceof ProcessorInterface) { - return $this->processor->process($session); + return $this->processor->process($data); } - return $session; + return $data; } } diff --git a/src/Extra/Provider/TraceProvider.php b/src/Extra/Provider/TraceProvider.php index fc6797b..989091f 100644 --- a/src/Extra/Provider/TraceProvider.php +++ b/src/Extra/Provider/TraceProvider.php @@ -6,20 +6,14 @@ use Dot\ErrorHandler\Extra\Processor\ProcessorInterface; -readonly class TraceProvider +class TraceProvider extends AbstractProvider { - public function __construct( - public bool $enabled = false, - public ?ProcessorInterface $processor = null, - ) { - } - - public function provide(array $traces): array + public function provide(array $data): array { if ($this->processor instanceof ProcessorInterface) { - return $this->processor->process($traces); + return $this->processor->process($data); } - return $traces; + return $data; } } diff --git a/src/Extra/ReplacementStrategy.php b/src/Extra/ReplacementStrategy.php new file mode 100644 index 0000000..99508c4 --- /dev/null +++ b/src/Extra/ReplacementStrategy.php @@ -0,0 +1,11 @@ +responseGenerator; if ($this->logger instanceof LoggerInterface) { - $this->logger->err($e->getMessage(), $this->prepareExtra($e, $request)); + $extra = $this->provideExtra($e, $request); + $this->logger->error($e->getMessage(), $extra); } $response = $generator($e, $request, ($this->responseFactory)()); @@ -131,37 +132,42 @@ public function triggerListeners( } } - private function prepareExtra(Throwable $throwable, ServerRequestInterface $request): array + public function provideExtra(Throwable $throwable, ServerRequestInterface $request): array { $extra = [ 'file' => $throwable->getFile(), 'line' => $throwable->getLine(), ]; - if ($this->extraProvider?->getCookie()->enabled) { + if ($this->extraProvider?->getCookie()->isEnabled()) { $extra['cookie'] = $this->extraProvider?->getCookie()->provide($request->getCookieParams()); } - if ($this->extraProvider?->getHeader()->enabled) { + if ($this->extraProvider?->getHeader()->isEnabled()) { $extra['header'] = $this->extraProvider?->getHeader()->provide($request->getHeaders()); } - if ($this->extraProvider?->getRequest()->enabled) { - $extra['request'] = $this->extraProvider?->getRequest()->provide($request->getParsedBody()); + if ($this->extraProvider?->getRequest()->isEnabled()) { + $extra['request'] = $this->extraProvider?->getRequest()->provide((array) $request->getParsedBody()); } - if ($this->extraProvider?->getServer()->enabled) { + if ($this->extraProvider?->getServer()->isEnabled()) { $extra['server'] = $this->extraProvider?->getServer()->provide($request->getServerParams()); } - if ($this->extraProvider?->getSession()->enabled) { + if ($this->extraProvider?->getSession()->isEnabled()) { $extra['session'] = $this->extraProvider?->getSession()->provide($_SESSION ?? []); } - if ($this->extraProvider?->getTrace()->enabled) { + if ($this->extraProvider?->getTrace()->isEnabled()) { $extra['trace'] = $this->extraProvider?->getTrace()->provide($throwable->getTrace()); } return $extra; } + + public function getExtraProvider(): ?ExtraProvider + { + return $this->extraProvider; + } } diff --git a/src/LogErrorHandlerFactory.php b/src/LogErrorHandlerFactory.php index c0f7f9e..9f89f74 100644 --- a/src/LogErrorHandlerFactory.php +++ b/src/LogErrorHandlerFactory.php @@ -5,7 +5,6 @@ namespace Dot\ErrorHandler; use Dot\ErrorHandler\Extra\ExtraProvider; -use Dot\Log\LoggerInterface; use InvalidArgumentException; use Mezzio\Middleware\ErrorResponseGenerator; use Psr\Container\ContainerExceptionInterface; @@ -13,8 +12,10 @@ use Psr\Container\NotFoundExceptionInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Server\MiddlewareInterface; +use Psr\Log\LoggerInterface; use function array_key_exists; +use function count; use function is_array; use function sprintf; @@ -43,11 +44,6 @@ public function __invoke(ContainerInterface $container): MiddlewareInterface ); } - $extraProvider = null; - if (array_key_exists('loggerExtra', $errorHandlerConfig) && is_array($errorHandlerConfig['loggerExtra'])) { - $extraProvider = new ExtraProvider($errorHandlerConfig['loggerExtra']); - } - $logger = null; if ($errorHandlerConfig['loggerEnabled']) { /** @var LoggerInterface $logger */ @@ -58,6 +54,15 @@ public function __invoke(ContainerInterface $container): MiddlewareInterface ? $container->get(ErrorResponseGenerator::class) : null; + $extraProvider = null; + if ( + array_key_exists(ExtraProvider::CONFIG_KEY, $errorHandlerConfig) + && is_array($errorHandlerConfig[ExtraProvider::CONFIG_KEY]) + && count($errorHandlerConfig[ExtraProvider::CONFIG_KEY]) > 0 + ) { + $extraProvider = new ExtraProvider($errorHandlerConfig[ExtraProvider::CONFIG_KEY]); + } + return new LogErrorHandler( $container->get(ResponseInterface::class), $generator, diff --git a/test/ErrorHandlerFactoryTest.php b/test/ErrorHandlerFactoryTest.php index 2cf7de6..48ee691 100644 --- a/test/ErrorHandlerFactoryTest.php +++ b/test/ErrorHandlerFactoryTest.php @@ -17,7 +17,7 @@ class ErrorHandlerFactoryTest extends TestCase { - private ContainerInterface|MockObject $container; + private MockObject|ContainerInterface $container; /** @var callable $responseFactory */ private $responseFactory; diff --git a/test/ErrorHandlerTest.php b/test/ErrorHandlerTest.php index 8b9536b..9d39212 100644 --- a/test/ErrorHandlerTest.php +++ b/test/ErrorHandlerTest.php @@ -24,16 +24,14 @@ class ErrorHandlerTest extends TestCase { private Subject $subject; - private ServerRequestInterface|MockObject $serverRequest; - private ResponseInterface|MockObject $response; + private MockObject&ServerRequestInterface $serverRequest; + private MockObject&ResponseInterface $response; private ErrorResponseGenerator $errorResponseGenerator; /** @var callable():ResponseInterface $responseFactory */ private $responseFactory; - /** @var MockObject&StreamInterface */ - private $body; - /** @var MockObject&RequestHandlerInterface */ - private $handler; - private Throwable|MockObject $exception; + private MockObject&StreamInterface $body; + private MockObject&RequestHandlerInterface $handler; + private Throwable $exception; /** * @throws Exception diff --git a/test/Extra/ExtraProviderTest.php b/test/Extra/ExtraProviderTest.php new file mode 100644 index 0000000..b4aac48 --- /dev/null +++ b/test/Extra/ExtraProviderTest.php @@ -0,0 +1,820 @@ +assertFalse($extraProvider->getCookie()->isEnabled()); + $this->assertNull($extraProvider->getCookie()->getProcessor()); + + $this->assertFalse($extraProvider->getHeader()->isEnabled()); + $this->assertNull($extraProvider->getHeader()->getProcessor()); + + $this->assertFalse($extraProvider->getRequest()->isEnabled()); + $this->assertNull($extraProvider->getRequest()->getProcessor()); + + $this->assertFalse($extraProvider->getServer()->isEnabled()); + $this->assertNull($extraProvider->getServer()->getProcessor()); + + $this->assertFalse($extraProvider->getSession()->isEnabled()); + $this->assertNull($extraProvider->getSession()->getProcessor()); + + $this->assertFalse($extraProvider->getTrace()->isEnabled()); + $this->assertNull($extraProvider->getTrace()->getProcessor()); + } + + public function testWillInstantiateWithInvalidOptions(): void + { + $extraProvider = new ExtraProvider(['test' => 'test']); + + $this->assertFalse($extraProvider->getCookie()->isEnabled()); + $this->assertNull($extraProvider->getCookie()->getProcessor()); + + $this->assertFalse($extraProvider->getHeader()->isEnabled()); + $this->assertNull($extraProvider->getHeader()->getProcessor()); + + $this->assertFalse($extraProvider->getRequest()->isEnabled()); + $this->assertNull($extraProvider->getRequest()->getProcessor()); + + $this->assertFalse($extraProvider->getServer()->isEnabled()); + $this->assertNull($extraProvider->getServer()->getProcessor()); + + $this->assertFalse($extraProvider->getSession()->isEnabled()); + $this->assertNull($extraProvider->getSession()->getProcessor()); + + $this->assertFalse($extraProvider->getTrace()->isEnabled()); + $this->assertNull($extraProvider->getTrace()->getProcessor()); + } + + public function testWillNotEnableProvidersWhenEmptyProviderOptions(): void + { + $extraProvider = new ExtraProvider([ + CookieProvider::class => [], + HeaderProvider::class => [], + RequestProvider::class => [], + ServerProvider::class => [], + SessionProvider::class => [], + TraceProvider::class => [], + ]); + + $this->assertFalse($extraProvider->getCookie()->isEnabled()); + $this->assertNull($extraProvider->getCookie()->getProcessor()); + + $this->assertFalse($extraProvider->getHeader()->isEnabled()); + $this->assertNull($extraProvider->getHeader()->getProcessor()); + + $this->assertFalse($extraProvider->getRequest()->isEnabled()); + $this->assertNull($extraProvider->getRequest()->getProcessor()); + + $this->assertFalse($extraProvider->getServer()->isEnabled()); + $this->assertNull($extraProvider->getServer()->getProcessor()); + + $this->assertFalse($extraProvider->getSession()->isEnabled()); + $this->assertNull($extraProvider->getSession()->getProcessor()); + + $this->assertFalse($extraProvider->getTrace()->isEnabled()); + $this->assertNull($extraProvider->getTrace()->getProcessor()); + } + + public function testWillEnableProvidersWhenEnabledIsTrueInProviderOptions(): void + { + $extraProvider = new ExtraProvider([ + CookieProvider::class => ['enabled' => true], + HeaderProvider::class => ['enabled' => true], + RequestProvider::class => ['enabled' => true], + ServerProvider::class => ['enabled' => true], + SessionProvider::class => ['enabled' => true], + TraceProvider::class => ['enabled' => true], + ]); + + $this->assertTrue($extraProvider->getCookie()->isEnabled()); + $this->assertNull($extraProvider->getCookie()->getProcessor()); + + $this->assertTrue($extraProvider->getHeader()->isEnabled()); + $this->assertNull($extraProvider->getHeader()->getProcessor()); + + $this->assertTrue($extraProvider->getRequest()->isEnabled()); + $this->assertNull($extraProvider->getRequest()->getProcessor()); + + $this->assertTrue($extraProvider->getServer()->isEnabled()); + $this->assertNull($extraProvider->getServer()->getProcessor()); + + $this->assertTrue($extraProvider->getSession()->isEnabled()); + $this->assertNull($extraProvider->getSession()->getProcessor()); + + $this->assertTrue($extraProvider->getTrace()->isEnabled()); + $this->assertNull($extraProvider->getTrace()->getProcessor()); + } + + public function testProvidersWillNotHaveProcessorWhenEmptyProcessorOptions(): void + { + $extraProvider = new ExtraProvider([ + CookieProvider::class => ['enabled' => true, 'processor' => []], + HeaderProvider::class => ['enabled' => true, 'processor' => []], + RequestProvider::class => ['enabled' => true, 'processor' => []], + ServerProvider::class => ['enabled' => true, 'processor' => []], + SessionProvider::class => ['enabled' => true, 'processor' => []], + TraceProvider::class => ['enabled' => true, 'processor' => []], + ]); + + $this->assertTrue($extraProvider->getCookie()->isEnabled()); + $this->assertNull($extraProvider->getCookie()->getProcessor()); + + $this->assertTrue($extraProvider->getHeader()->isEnabled()); + $this->assertNull($extraProvider->getHeader()->getProcessor()); + + $this->assertTrue($extraProvider->getRequest()->isEnabled()); + $this->assertNull($extraProvider->getRequest()->getProcessor()); + + $this->assertTrue($extraProvider->getServer()->isEnabled()); + $this->assertNull($extraProvider->getServer()->getProcessor()); + + $this->assertTrue($extraProvider->getSession()->isEnabled()); + $this->assertNull($extraProvider->getSession()->getProcessor()); + + $this->assertTrue($extraProvider->getTrace()->isEnabled()); + $this->assertNull($extraProvider->getTrace()->getProcessor()); + } + + public function testProvidersWillNotHaveProcessorWhenInvalidProcessorOptions(): void + { + $extraProvider = new ExtraProvider([ + CookieProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']], + HeaderProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']], + RequestProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']], + ServerProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']], + SessionProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']], + TraceProvider::class => ['enabled' => true, 'processor' => ['class' => 'test']], + ]); + + $this->assertTrue($extraProvider->getCookie()->isEnabled()); + $this->assertNull($extraProvider->getCookie()->getProcessor()); + + $this->assertTrue($extraProvider->getHeader()->isEnabled()); + $this->assertNull($extraProvider->getHeader()->getProcessor()); + + $this->assertTrue($extraProvider->getRequest()->isEnabled()); + $this->assertNull($extraProvider->getRequest()->getProcessor()); + + $this->assertTrue($extraProvider->getServer()->isEnabled()); + $this->assertNull($extraProvider->getServer()->getProcessor()); + + $this->assertTrue($extraProvider->getSession()->isEnabled()); + $this->assertNull($extraProvider->getSession()->getProcessor()); + + $this->assertTrue($extraProvider->getTrace()->isEnabled()); + $this->assertNull($extraProvider->getTrace()->getProcessor()); + } + + public function testProvidersWillHaveProcessorWhenValidProcessorOptions(): void + { + $extraProvider = new ExtraProvider([ + CookieProvider::class => ['enabled' => true, 'processor' => ['class' => CookieProcessor::class]], + HeaderProvider::class => ['enabled' => true, 'processor' => ['class' => HeaderProcessor::class]], + RequestProvider::class => ['enabled' => true, 'processor' => ['class' => RequestProcessor::class]], + ServerProvider::class => ['enabled' => true, 'processor' => ['class' => ServerProcessor::class]], + SessionProvider::class => ['enabled' => true, 'processor' => ['class' => SessionProcessor::class]], + TraceProvider::class => ['enabled' => true, 'processor' => ['class' => TraceProcessor::class]], + ]); + + $this->assertTrue($extraProvider->getCookie()->isEnabled()); + $processor = $extraProvider->getCookie()->getProcessor(); + $this->assertInstanceOf(CookieProcessor::class, $processor); + $this->assertCount(0, $processor->getSensitiveParameters()); + $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy()); + + $this->assertTrue($extraProvider->getHeader()->isEnabled()); + $processor = $extraProvider->getHeader()->getProcessor(); + $this->assertInstanceOf(HeaderProcessor::class, $processor); + $this->assertCount(0, $processor->getSensitiveParameters()); + $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy()); + + $this->assertTrue($extraProvider->getRequest()->isEnabled()); + $processor = $extraProvider->getRequest()->getProcessor(); + $this->assertInstanceOf(RequestProcessor::class, $processor); + $this->assertCount(0, $processor->getSensitiveParameters()); + $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy()); + + $this->assertTrue($extraProvider->getServer()->isEnabled()); + $processor = $extraProvider->getServer()->getProcessor(); + $this->assertInstanceOf(ServerProcessor::class, $processor); + $this->assertCount(0, $processor->getSensitiveParameters()); + $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy()); + + $this->assertTrue($extraProvider->getSession()->isEnabled()); + $processor = $extraProvider->getSession()->getProcessor(); + $this->assertInstanceOf(SessionProcessor::class, $processor); + $this->assertCount(0, $processor->getSensitiveParameters()); + $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy()); + + $this->assertTrue($extraProvider->getTrace()->isEnabled()); + $processor = $extraProvider->getTrace()->getProcessor(); + $this->assertInstanceOf(TraceProcessor::class, $processor); + $this->assertCount(0, $processor->getSensitiveParameters()); + $this->assertEquals(ReplacementStrategy::Full, $processor->getReplacementStrategy()); + } + + public function testProvidersWillHaveProcessorWhenAllValidProcessorOptions(): void + { + $sensitiveParameters = ['test']; + + $extraProvider = new ExtraProvider([ + CookieProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => CookieProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => $sensitiveParameters, + ], + ], + HeaderProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => HeaderProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => $sensitiveParameters, + ], + ], + RequestProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => RequestProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => $sensitiveParameters, + ], + ], + ServerProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => ServerProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => $sensitiveParameters, + ], + ], + SessionProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => SessionProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => $sensitiveParameters, + ], + ], + TraceProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => TraceProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => $sensitiveParameters, + ], + ], + ]); + + $this->assertTrue($extraProvider->getCookie()->isEnabled()); + $processor = $extraProvider->getCookie()->getProcessor(); + $this->assertInstanceOf(CookieProcessor::class, $processor); + $this->assertCount(1, $processor->getSensitiveParameters()); + $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters())); + $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy()); + + $this->assertTrue($extraProvider->getHeader()->isEnabled()); + $processor = $extraProvider->getHeader()->getProcessor(); + $this->assertInstanceOf(HeaderProcessor::class, $processor); + $this->assertCount(1, $processor->getSensitiveParameters()); + $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters())); + $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy()); + + $this->assertTrue($extraProvider->getRequest()->isEnabled()); + $processor = $extraProvider->getRequest()->getProcessor(); + $this->assertInstanceOf(RequestProcessor::class, $processor); + $this->assertCount(1, $processor->getSensitiveParameters()); + $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters())); + $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy()); + + $this->assertTrue($extraProvider->getServer()->isEnabled()); + $processor = $extraProvider->getServer()->getProcessor(); + $this->assertInstanceOf(ServerProcessor::class, $processor); + $this->assertCount(1, $processor->getSensitiveParameters()); + $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters())); + $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy()); + + $this->assertTrue($extraProvider->getSession()->isEnabled()); + $processor = $extraProvider->getSession()->getProcessor(); + $this->assertInstanceOf(SessionProcessor::class, $processor); + $this->assertCount(1, $processor->getSensitiveParameters()); + $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters())); + $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy()); + + $this->assertTrue($extraProvider->getTrace()->isEnabled()); + $processor = $extraProvider->getTrace()->getProcessor(); + $this->assertInstanceOf(TraceProcessor::class, $processor); + $this->assertCount(1, $processor->getSensitiveParameters()); + $this->assertEquals($sensitiveParameters, array_flip($processor->getSensitiveParameters())); + $this->assertEquals(ReplacementStrategy::Partial, $processor->getReplacementStrategy()); + } + + public function testWillProvideUnmodifiedCookieDataWhenNoProcessor(): void + { + $extraProvider = new ExtraProvider([ + CookieProvider::class => [ + 'enabled' => true, + ], + ]); + + $input = ['test' => 'test']; + $output = $extraProvider->getCookie()->provide($input); + $this->assertSame($input, $output); + } + + public function testWillProvideModifiedCookieDataWhenProcessorSetToPartialReplaceAllKeys(): void + { + $extraProvider = new ExtraProvider([ + CookieProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => CookieProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => [ProcessorInterface::ALL], + ], + ], + ]); + + $output = $extraProvider->getCookie()->provide(['first' => 'First', 'second' => 'Second', 'third' => 'Third']); + $this->assertSame(['first' => 'Fir**', 'second' => 'Sec***', 'third' => 'Thi**'], $output); + } + + public function testWillProvideModifiedCookieDataWhenProcessorSetToFullReplaceAllKeys(): void + { + $extraProvider = new ExtraProvider([ + CookieProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => CookieProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ProcessorInterface::ALL], + ], + ], + ]); + + $output = $extraProvider->getCookie()->provide(['first' => 'First', 'second' => 'Second', 'third' => 'Third']); + $this->assertSame(['first' => '*****', 'second' => '******', 'third' => '*****'], $output); + } + + public function testWillProvideModifiedCookieDataWhenProcessorSetToPartialReplaceSpecificKeys(): void + { + $extraProvider = new ExtraProvider([ + CookieProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => CookieProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => ['second'], + ], + ], + ]); + + $output = $extraProvider->getCookie()->provide(['first' => 'First', 'second' => 'Second', 'third' => 'Third']); + $this->assertSame(['first' => 'First', 'second' => 'Sec***', 'third' => 'Third'], $output); + } + + public function testWillProvideModifiedCookieDataWhenProcessorSetToFullReplaceSpecificKeys(): void + { + $extraProvider = new ExtraProvider([ + CookieProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => CookieProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => ['second'], + ], + ], + ]); + + $output = $extraProvider->getCookie()->provide(['first' => 'First', 'second' => 'Second', 'third' => 'Third']); + $this->assertSame(['first' => 'First', 'second' => '******', 'third' => 'Third'], $output); + } + + public function testWillProvideUnmodifiedHeaderDataWhenNoProcessor(): void + { + $extraProvider = new ExtraProvider([ + HeaderProvider::class => [ + 'enabled' => true, + ], + ]); + + $input = ['cookie' => 'Test-data']; + $output = $extraProvider->getHeader()->provide($input); + $this->assertSame($input, $output); + } + + public function testWillProvideModifiedHeaderDataWhenProcessorSetToPartialReplaceAllKeys(): void + { + $extraProvider = new ExtraProvider([ + HeaderProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => HeaderProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => [ProcessorInterface::ALL], + ], + ], + ]); + + $output = $extraProvider->getHeader()->provide([ + 'test' => 'Test', + 'cookie' => 'rememberMe=s0me-v3ry-l0ng-h4sh', + ]); + $this->assertSame(['test' => 'Te**', 'cookie' => 'rememberMe=s0me-v3ry-*********'], $output); + + $output = $extraProvider->getHeader()->provide([ + 'test' => 'Test', + 'cookie' => ['rememberMe=s0me-v3ry-l0ng-h4sh'], + ]); + $this->assertSame(['test' => 'Te**', 'cookie' => 'rememberMe=s0me-v3ry-*********'], $output); + } + + public function testWillProvideModifiedHeaderDataWhenProcessorSetToFullReplaceAllKeys(): void + { + $extraProvider = new ExtraProvider([ + HeaderProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => HeaderProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ProcessorInterface::ALL], + ], + ], + ]); + + $output = $extraProvider->getHeader()->provide([ + 'test' => 'Test', + 'cookie' => 'rememberMe=s0me-v3ry-l0ng-h4sh', + ]); + $this->assertSame(['test' => '****', 'cookie' => 'rememberMe=*******************'], $output); + + $output = $extraProvider->getHeader()->provide([ + 'test' => 'Test', + 'cookie' => ['rememberMe=s0me-v3ry-l0ng-h4sh'], + ]); + $this->assertSame(['test' => '****', 'cookie' => 'rememberMe=*******************'], $output); + } + + public function testWillProvideModifiedHeaderDataWhenProcessorSetToPartialReplaceSpecificKeys(): void + { + $extraProvider = new ExtraProvider([ + HeaderProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => HeaderProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => ['test'], + ], + ], + ]); + + $output = $extraProvider->getHeader()->provide([ + 'test' => 'Test', + 'data' => 'Data', + 'cookie' => 'rememberMe=s0me-v3ry-l0ng-h4sh', + ]); + $this->assertSame(['test' => 'Te**', 'data' => 'Data', 'cookie' => 'rememberMe=s0me-v3ry-*********'], $output); + + $output = $extraProvider->getHeader()->provide([ + 'test' => 'Test', + 'data' => 'Data', + 'cookie' => ['rememberMe=s0me-v3ry-l0ng-h4sh'], + ]); + $this->assertSame(['test' => 'Te**', 'data' => 'Data', 'cookie' => 'rememberMe=s0me-v3ry-*********'], $output); + } + + public function testWillProvideModifiedHeaderDataWhenProcessorSetToFullReplaceSpecificKeys(): void + { + $extraProvider = new ExtraProvider([ + HeaderProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => HeaderProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => ['test'], + ], + ], + ]); + + $output = $extraProvider->getHeader()->provide([ + 'test' => 'Test', + 'data' => 'Data', + 'cookie' => 'rememberMe=s0me-v3ry-l0ng-h4sh', + ]); + $this->assertSame(['test' => '****', 'data' => 'Data', 'cookie' => 'rememberMe=*******************'], $output); + + $output = $extraProvider->getHeader()->provide([ + 'test' => 'Test', + 'data' => 'Data', + 'cookie' => ['rememberMe=s0me-v3ry-l0ng-h4sh'], + ]); + $this->assertSame(['test' => '****', 'data' => 'Data', 'cookie' => 'rememberMe=*******************'], $output); + } + + public function testWillProvideUnmodifiedRequestDataWhenNoProcessor(): void + { + $extraProvider = new ExtraProvider([ + RequestProvider::class => [ + 'enabled' => true, + ], + ]); + + $input = ['foo' => 'bar', 'bar' => ['baz' => 'foo']]; + $output = $extraProvider->getRequest()->provide($input); + $this->assertSame($input, $output); + } + + public function testWillProvideModifiedRequestDataWhenProcessorSetToPartialReplaceAllKeys(): void + { + $extraProvider = new ExtraProvider([ + RequestProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => RequestProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => [ProcessorInterface::ALL], + ], + ], + ]); + + $input = ['foo' => 'bar', 'bar' => ['baz' => 'foo']]; + $output = $extraProvider->getRequest()->provide($input); + $this->assertSame(['foo' => 'ba*', 'bar' => ['baz' => 'fo*']], $output); + } + + public function testWillProvideModifiedRequestDataWhenProcessorSetToFullReplaceAllKeys(): void + { + $extraProvider = new ExtraProvider([ + RequestProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => RequestProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ProcessorInterface::ALL], + ], + ], + ]); + + $input = ['foo' => 'bar', 'bar' => ['baz' => 'foo']]; + $output = $extraProvider->getRequest()->provide($input); + $this->assertSame(['foo' => '***', 'bar' => ['baz' => '***']], $output); + } + + public function testWillProvideModifiedRequestDataWhenProcessorSetToPartialReplaceSpecificKeys(): void + { + $extraProvider = new ExtraProvider([ + RequestProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => RequestProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => ['ba'], + ], + ], + ]); + + $input = ['foo' => 'Foo', 'bar' => ['bar' => 'Bar'], 'baz' => 'Baz']; + $output = $extraProvider->getRequest()->provide($input); + $this->assertSame(['foo' => 'Foo', 'bar' => ['bar' => 'Ba*'], 'baz' => 'Ba*'], $output); + } + + public function testWillProvideModifiedRequestDataWhenProcessorSetToFullReplaceSpecificKeys(): void + { + $extraProvider = new ExtraProvider([ + RequestProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => RequestProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => ['ba'], + ], + ], + ]); + + $input = ['foo' => 'Foo', 'bar' => ['bar' => 'Bar'], 'baz' => 'Baz']; + $output = $extraProvider->getRequest()->provide($input); + $this->assertSame(['foo' => 'Foo', 'bar' => ['bar' => '***'], 'baz' => '***'], $output); + } + + public function testWillProvideUnmodifiedServerDataWhenNoProcessor(): void + { + $extraProvider = new ExtraProvider([ + ServerProvider::class => [ + 'enabled' => true, + ], + ]); + + $input = ['foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Bar; baz=Baz']; + $output = $extraProvider->getServer()->provide($input); + $this->assertSame($input, $output); + } + + public function testWillProvideModifiedServerDataWhenProcessorSetToPartialReplaceAllKeys(): void + { + $extraProvider = new ExtraProvider([ + ServerProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => ServerProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => [ProcessorInterface::ALL], + ], + ], + ]); + + $input = ['foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Bar; baz=Baz']; + $output = $extraProvider->getServer()->provide($input); + $this->assertSame( + ['foo' => 'Fo*', 'bar' => 'Ba*', 'baz' => 'Ba*', 'HTTP_COOKIE' => 'foo=Fo*; bar=Ba*; baz=Ba*'], + $output + ); + } + + public function testWillProvideModifiedServerDataWhenProcessorSetToFullReplaceAllKeys(): void + { + $extraProvider = new ExtraProvider([ + ServerProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => ServerProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ProcessorInterface::ALL], + ], + ], + ]); + + $input = ['foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Bar; baz=Baz']; + $output = $extraProvider->getServer()->provide($input); + $this->assertSame( + ['foo' => '***', 'bar' => '***', 'baz' => '***', 'HTTP_COOKIE' => 'foo=***; bar=***; baz=***'], + $output + ); + } + + public function testWillProvideModifiedServerDataWhenProcessorSetToPartialReplaceSpecificKeys(): void + { + $extraProvider = new ExtraProvider([ + ServerProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => ServerProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Partial, + 'sensitiveParameters' => ['bar'], + ], + ], + ]); + + $input = ['foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Bar; baz=Baz']; + $output = $extraProvider->getServer()->provide($input); + $this->assertSame( + ['foo' => 'Foo', 'bar' => 'Ba*', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Ba*; baz=Baz'], + $output + ); + } + + public function testWillProvideModifiedServerDataWhenProcessorSetToFullReplaceSpecificKeys(): void + { + $extraProvider = new ExtraProvider([ + ServerProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => ServerProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => ['bar'], + ], + ], + ]); + + $input = ['foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=Bar; baz=Baz']; + $output = $extraProvider->getServer()->provide($input); + $this->assertSame( + ['foo' => 'Foo', 'bar' => '***', 'baz' => 'Baz', 'HTTP_COOKIE' => 'foo=Foo; bar=***; baz=Baz'], + $output + ); + } + + public function testWillProvideUnmodifiedSessionDataWhenNoProcessor(): void + { + $extraProvider = new ExtraProvider([ + SessionProvider::class => [ + 'enabled' => true, + ], + ]); + + $input = ['foo' => ['foo' => 'Foo'], 'bar' => new ArrayObject(['bar' => 'Bar'])]; + $output = $extraProvider->getSession()->provide($input); + $this->assertSame($input, $output); + } + + public function testWillProvideModifiedSessionDataWhenProcessorIsSpecified(): void + { + $extraProvider = new ExtraProvider([ + SessionProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => SessionProcessor::class, + ], + ], + ]); + + $input = ['foo' => ['foo' => 'Foo'], 'bar' => new ArrayObject(['bar' => 'Bar'])]; + $output = $extraProvider->getSession()->provide($input); + $this->assertSame(['foo' => ['foo' => 'Foo'], 'bar' => ['bar' => 'Bar']], $output); + } + + public function testWillProvideUnmodifiedTraceDataWhenNoProcessor(): void + { + $extraProvider = new ExtraProvider([ + TraceProvider::class => [ + 'enabled' => true, + ], + ]); + + $input = [ + ['file' => '/path/to/some/class.php', 'line' => 1, 'function' => 'foo', 'class' => 'Foo', 'type' => '->'], + ['file' => '/path/to/index.php', 'line' => 1, 'function' => 'bar'], + ]; + $output = $extraProvider->getTrace()->provide($input); + $this->assertSame($input, $output); + } + + public function testWillProvideUnmodifiedTraceDataWhenProcessorIsSpecified(): void + { + $extraProvider = new ExtraProvider([ + TraceProvider::class => [ + 'enabled' => true, + 'processor' => [ + 'class' => TraceProcessor::class, + ], + ], + ]); + + $input = [ + ['file' => '/path/to/some/class.php', 'line' => 8, 'function' => 'foo', 'class' => 'Foo', 'type' => '->'], + ['file' => '/path/to/index.php', 'line' => 8, 'function' => 'bar'], + ]; + $output = $extraProvider->getTrace()->provide($input); + $this->assertSame(['Foo->foo:8', '/path/to/index.php->bar:8'], $output); + + $input = [ + ['file' => '/path/to/some/class.php', 'line' => 8, 'function' => 'foo', 'class' => 'Foo'], + ['file' => '/path/to/index.php', 'line' => 8, 'function' => 'bar'], + ]; + $output = $extraProvider->getTrace()->provide($input); + $this->assertSame(['Foo->foo:8', '/path/to/index.php->bar:8'], $output); + + $input = [ + ['file' => '/path/to/some/class.php', 'line' => 8, 'class' => 'Foo'], + ['file' => '/path/to/index.php', 'line' => 8], + ]; + $output = $extraProvider->getTrace()->provide($input); + $this->assertSame(['Foo->unknown:8', '/path/to/index.php->unknown:8'], $output); + + $input = [ + ['file' => '/path/to/some/class.php', 'line' => 8], + ['file' => '/path/to/index.php', 'line' => 8], + ]; + $output = $extraProvider->getTrace()->provide($input); + $this->assertSame(['/path/to/some/class.php->unknown:8', '/path/to/index.php->unknown:8'], $output); + + $input = [ + ['file' => '/path/to/some/class.php'], + ['file' => '/path/to/index.php'], + ]; + $output = $extraProvider->getTrace()->provide($input); + $this->assertSame(['/path/to/some/class.php->unknown:0', '/path/to/index.php->unknown:0'], $output); + + $input = [[], []]; + $output = $extraProvider->getTrace()->provide($input); + $this->assertSame(['unknown->unknown:0', 'unknown->unknown:0'], $output); + } +} diff --git a/test/LogErrorHandlerFactoryTest.php b/test/LogErrorHandlerFactoryTest.php index 418dc7a..d64583b 100644 --- a/test/LogErrorHandlerFactoryTest.php +++ b/test/LogErrorHandlerFactoryTest.php @@ -4,8 +4,10 @@ namespace DotTest\ErrorHandler; +use Dot\ErrorHandler\Extra\ExtraProvider; use Dot\ErrorHandler\LogErrorHandler; use Dot\ErrorHandler\LogErrorHandlerFactory; +use InvalidArgumentException; use Mezzio\Middleware\ErrorResponseGenerator; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\MockObject\MockObject; @@ -20,7 +22,7 @@ class LogErrorHandlerFactoryTest extends TestCase { - private ContainerInterface|MockObject $container; + private ContainerInterface&MockObject $container; /** @var callable $responseFactory */ private $responseFactory; @@ -43,7 +45,7 @@ public function testWillNotCreateWithoutConfig(): void ->with('config') ->willReturn(false); - $this->expectException('InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( sprintf('\'[%s\'] not found in config', LogErrorHandlerFactory::ERROR_HANDLER_KEY) ); @@ -66,7 +68,7 @@ public function testWillNotCreateWithMissingLoggerKey(): void ], ]); - $this->expectException('InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( sprintf( 'Logger: \'[%s\'] is enabled, but not found in config', @@ -108,6 +110,121 @@ public function testWillCreateWithValidConfigAndMissingLogger(): void $this->assertInstanceOf(LogErrorHandler::class, $result); } + /** + * @throws ContainerExceptionInterface + * @throws Exception + * @throws NotFoundExceptionInterface + */ + public function testWillCreateWithoutExtraProviderConfig(): void + { + $logger = $this->createMock(LoggerInterface::class); + + $this->container->method('has') + ->with(ErrorResponseGenerator::class) + ->willReturn(true); + + $this->container->method('get') + ->willReturnMap([ + [ + 'config', + [ + LogErrorHandlerFactory::ERROR_HANDLER_KEY => [ + 'loggerEnabled' => true, + LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY => 'test', + ], + ], + ], + [ + 'config[' . LogErrorHandlerFactory::ERROR_HANDLER_KEY . '][' + . LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY . ']', + $logger, + ], + [ErrorResponseGenerator::class, $this->createMock(ErrorResponseGenerator::class)], + [ResponseInterface::class, $this->responseFactory], + ]); + + $result = (new LogErrorHandlerFactory())($this->container); + $this->assertInstanceOf(LogErrorHandler::class, $result); + } + + /** + * @throws ContainerExceptionInterface + * @throws Exception + * @throws NotFoundExceptionInterface + */ + public function testWillCreateWithEmptyExtraProviderConfig(): void + { + $logger = $this->createMock(LoggerInterface::class); + + $this->container->method('has') + ->with(ErrorResponseGenerator::class) + ->willReturn(true); + + $this->container->method('get') + ->willReturnMap([ + [ + 'config', + [ + LogErrorHandlerFactory::ERROR_HANDLER_KEY => [ + 'loggerEnabled' => true, + LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY => 'test', + ], + ExtraProvider::CONFIG_KEY => [], + ], + ], + [ + 'config[' . LogErrorHandlerFactory::ERROR_HANDLER_KEY . '][' + . LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY . ']', + $logger, + ], + [ErrorResponseGenerator::class, $this->createMock(ErrorResponseGenerator::class)], + [ResponseInterface::class, $this->responseFactory], + ]); + + $result = (new LogErrorHandlerFactory())($this->container); + $this->assertInstanceOf(LogErrorHandler::class, $result); + } + + /** + * @throws ContainerExceptionInterface + * @throws Exception + * @throws NotFoundExceptionInterface + */ + public function testWillCreateWithInvalidExtraProviderConfig(): void + { + $logger = $this->createMock(LoggerInterface::class); + + $this->container->method('has') + ->with(ErrorResponseGenerator::class) + ->willReturn(true); + + $this->container->method('get') + ->willReturnMap([ + [ + 'config', + [ + LogErrorHandlerFactory::ERROR_HANDLER_KEY => [ + 'loggerEnabled' => true, + LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY => 'test', + ], + ExtraProvider::CONFIG_KEY => [ + 'test' => 'test', + ], + ], + ], + [ + 'config[' . LogErrorHandlerFactory::ERROR_HANDLER_KEY . '][' + . LogErrorHandlerFactory::ERROR_HANDLER_LOGGER_KEY . ']', + $logger, + ], + [ErrorResponseGenerator::class, $this->createMock(ErrorResponseGenerator::class)], + [ResponseInterface::class, $this->responseFactory], + ]); + + $result = (new LogErrorHandlerFactory())($this->container); + $this->assertInstanceOf(LogErrorHandler::class, $result); + } + /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface diff --git a/test/LogErrorHandlerTest.php b/test/LogErrorHandlerTest.php index 2d6e4b6..b48598a 100644 --- a/test/LogErrorHandlerTest.php +++ b/test/LogErrorHandlerTest.php @@ -4,11 +4,17 @@ namespace DotTest\ErrorHandler; +use Dot\ErrorHandler\Extra\ExtraProvider; +use Dot\ErrorHandler\Extra\Provider\CookieProvider; +use Dot\ErrorHandler\Extra\Provider\HeaderProvider; +use Dot\ErrorHandler\Extra\Provider\RequestProvider; +use Dot\ErrorHandler\Extra\Provider\ServerProvider; +use Dot\ErrorHandler\Extra\Provider\SessionProvider; +use Dot\ErrorHandler\Extra\Provider\TraceProvider; use Dot\ErrorHandler\LogErrorHandler; use Dot\ErrorHandler\LogErrorHandler as Subject; use Dot\Log\Formatter\Json; use Dot\Log\Logger; -use Dot\Log\LoggerInterface; use ErrorException; use Laminas\Stratigility\Middleware\ErrorResponseGenerator; use org\bovigo\vfs\vfsStream; @@ -21,6 +27,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Server\RequestHandlerInterface; +use Psr\Log\LoggerInterface; use ReflectionObject; use RuntimeException; use Throwable; @@ -31,13 +38,13 @@ class LogErrorHandlerTest extends TestCase { private Subject $subject; - private ServerRequestInterface|MockObject $serverRequest; - private ResponseInterface|MockObject $response; + private MockObject&ServerRequestInterface $serverRequest; + private MockObject&ResponseInterface $response; /** @var callable():ResponseInterface $responseFactory */ private $responseFactory; - private StreamInterface|MockObject $body; - private RequestHandlerInterface|MockObject $handler; - private Throwable|MockObject $exception; + private MockObject&StreamInterface $body; + private MockObject&RequestHandlerInterface $handler; + private Throwable $exception; private ErrorResponseGenerator $errorResponseGenerator; private vfsStreamDirectory $fileSystem; @@ -77,13 +84,110 @@ public function testCreateErrorHandlerRaisesErrorException(): void $callableErrorHandler = $this->subject->createErrorHandler(); $this->expectException(ErrorException::class); - $callableErrorHandler(error_reporting(), ErrorException::class, 'testErrfile', 0); + $callableErrorHandler(error_reporting(), ErrorException::class, 'testErrorFile', 0); } public function testCreateErrorHandlerSkipsErrorsOutsideErrorReportingMask(): void { $callableErrorHandler = $this->subject->createErrorHandler(); - $this->assertNull($callableErrorHandler(-(error_reporting() + 1), ErrorException::class, 'testErrfile', 0)); + $this->assertNull($callableErrorHandler(-(error_reporting() + 1), ErrorException::class, 'testErrorFile', 0)); + } + + /** + * @throws ContainerExceptionInterface + */ + public function testLogErrorHandlerWillInitiateWhenExtraProviderIsMissing(): void + { + $logErrorHandler = new LogErrorHandler( + $this->responseFactory, + $this->errorResponseGenerator, + new Logger($this->getConfig()), + ); + + $this->assertInstanceOf(LogErrorHandler::class, $logErrorHandler); + $this->assertNull($logErrorHandler->getExtraProvider()); + } + + /** + * @throws ContainerExceptionInterface + */ + public function testLogErrorHandlerWillInitiateWhenExtraProviderIsNull(): void + { + $logErrorHandler = new LogErrorHandler( + $this->responseFactory, + $this->errorResponseGenerator, + new Logger($this->getConfig()), + null + ); + + $this->assertInstanceOf(LogErrorHandler::class, $logErrorHandler); + $this->assertNull($logErrorHandler->getExtraProvider()); + } + + /** + * @throws ContainerExceptionInterface + */ + public function testLogErrorHandlerWillInitiateWhenExtraProviderIsProvided(): void + { + $logErrorHandler = new LogErrorHandler( + $this->responseFactory, + $this->errorResponseGenerator, + new Logger($this->getConfig()), + new ExtraProvider() + ); + + $this->assertInstanceOf(LogErrorHandler::class, $logErrorHandler); + $this->assertInstanceOf(ExtraProvider::class, $logErrorHandler->getExtraProvider()); + } + + /** + * @throws ContainerExceptionInterface + */ + public function testLogErrorHandlerLogsWillOnlyContainDefaultKeysWhenNoProvidersAreEnabled(): void + { + $logErrorHandler = new LogErrorHandler( + $this->responseFactory, + $this->errorResponseGenerator, + new Logger($this->getConfig()), + new ExtraProvider() + ); + + $log = $logErrorHandler->provideExtra(new \Exception('test'), $this->serverRequest); + $this->assertCount(2, $log); + $this->assertArrayHasKey('file', $log); + $this->assertArrayHasKey('line', $log); + } + + /** + * @throws ContainerExceptionInterface + */ + public function testLogErrorHandlerLogsWillContainExtraKeysWhenProvidersAreEnabled(): void + { + $logErrorHandler = new LogErrorHandler( + $this->responseFactory, + $this->errorResponseGenerator, + new Logger($this->getConfig()), + new ExtraProvider([ + CookieProvider::class => ['enabled' => true], + HeaderProvider::class => ['enabled' => true], + RequestProvider::class => ['enabled' => true], + ServerProvider::class => ['enabled' => true], + SessionProvider::class => ['enabled' => true], + TraceProvider::class => ['enabled' => true], + ]) + ); + + $extra = $logErrorHandler->provideExtra(new \Exception('test'), $this->serverRequest); + + $this->assertCount(8, $extra); + $this->assertArrayHasKey('file', $extra); + $this->assertArrayHasKey('line', $extra); + $this->assertArrayHasKey('cookie', $extra); + $this->assertArrayHasKey('header', $extra); + $this->assertArrayHasKey('request', $extra); + $this->assertArrayHasKey('server', $extra); + $this->assertArrayHasKey('session', $extra); + $this->assertArrayHasKey('trace', $extra); } public function testAttachListenerDoesNotAttachDuplicates(): void @@ -244,16 +348,16 @@ private function getConfig(): array return [ 'writers' => [ 'FileWriter' => [ - 'name' => 'stream', - 'priority' => Logger::ALERT, - 'options' => [ + 'name' => 'stream', + 'level' => Logger::ALERT, + 'options' => [ 'stream' => $this->fileSystem->url() . '/test-error-log.log', 'filters' => [ 'allMessages' => [ - 'name' => 'priority', + 'name' => 'level', 'options' => [ 'operator' => '>=', - 'priority' => Logger::EMERG, + 'level' => Logger::EMERG, ], ], ], From ef9a6dc4042a8bd45b095fbe4ab0381c3b677fb5 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Wed, 19 Feb 2025 17:34:53 +0200 Subject: [PATCH 04/14] linting Signed-off-by: alexmerlin --- docs/book/v4/extra/cookie.md | 10 +++++----- docs/book/v4/log-files.md | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/book/v4/extra/cookie.md b/docs/book/v4/extra/cookie.md index 1c0084d..e209e8f 100644 --- a/docs/book/v4/extra/cookie.md +++ b/docs/book/v4/extra/cookie.md @@ -2,11 +2,11 @@ Looking at `dot-errorhandler`'s config file, the array found at `CookieProvider::class` allows you to configure the behaviour of this provider: -- **enabled**: enabled/disable this provider -- **processor**: an array configuring the data processor to be used by the **CookieProvider**: - - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` - - **replacementStrategy**: whether to replace specific cookie values completely or partially - - **sensitiveParameters**: an array of cookies names that may contain sensitive information so their value should be masked partially/completely +* **enabled**: enabled/disable this provider +* **processor**: an array configuring the data processor to be used by the **CookieProvider**: + * **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` + * **replacementStrategy**: whether to replace specific cookie values completely or partially + * **sensitiveParameters**: an array of cookies names that may contain sensitive information so their value should be masked partially/completely ## Configure provider diff --git a/docs/book/v4/log-files.md b/docs/book/v4/log-files.md index 57b5b93..4d9aa3e 100644 --- a/docs/book/v4/log-files.md +++ b/docs/book/v4/log-files.md @@ -9,13 +9,13 @@ By default, log activities are formatted with JSON, so each row should be a deco Each row in a log file should contain the following values: -- **timestamp**: string representation of the date and time when the error occurred -- **priority**: numeric representation of the error level -- **priorityName**: string representation of the error level -- **message**: error message describing the error -- **extra**: an array of extra information that may help the developer debug the error: - - **file**: the file in which the error occurred - - **line**: the line from **file** where the error occurred +* **timestamp**: string representation of the date and time when the error occurred +* **priority**: numeric representation of the error level +* **priorityName**: string representation of the error level +* **message**: error message describing the error +* **extra**: an array of extra information that may help the developer debug the error: + * **file**: the file in which the error occurred + * **line**: the line from **file** where the error occurred By leveraging `dot-errorhandler`'s extra providers, you can also log additional request parameters. -Learn more about what additional parameters are available on the [extra data](extra-data.md) page. +Learn more about what additional parameters are available on the [extra data](extra/introduction.md) page. From bc1db7a363141589bea5e691c5e0154557c5197b Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Wed, 19 Feb 2025 17:53:23 +0200 Subject: [PATCH 05/14] linting Signed-off-by: alexmerlin --- docs/book/v4/extra/cookie.md | 10 +++++----- docs/book/v4/log-files.md | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/book/v4/extra/cookie.md b/docs/book/v4/extra/cookie.md index e209e8f..494cb8d 100644 --- a/docs/book/v4/extra/cookie.md +++ b/docs/book/v4/extra/cookie.md @@ -2,11 +2,11 @@ Looking at `dot-errorhandler`'s config file, the array found at `CookieProvider::class` allows you to configure the behaviour of this provider: -* **enabled**: enabled/disable this provider -* **processor**: an array configuring the data processor to be used by the **CookieProvider**: - * **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` - * **replacementStrategy**: whether to replace specific cookie values completely or partially - * **sensitiveParameters**: an array of cookies names that may contain sensitive information so their value should be masked partially/completely +- **enabled**: enabled/disable this provider +- **processor**: an array configuring the data processor to be used by the **CookieProvider**: + - **class**: data processor class implementing `Dot\ErrorHandler\Extra\Processor\ProcessorInterface` + - **replacementStrategy**: whether to replace specific cookie values completely or partially + - **sensitiveParameters**: an array of cookies names that may contain sensitive information so their value should be masked partially/completely ## Configure provider diff --git a/docs/book/v4/log-files.md b/docs/book/v4/log-files.md index 4d9aa3e..cd686e9 100644 --- a/docs/book/v4/log-files.md +++ b/docs/book/v4/log-files.md @@ -9,13 +9,13 @@ By default, log activities are formatted with JSON, so each row should be a deco Each row in a log file should contain the following values: -* **timestamp**: string representation of the date and time when the error occurred -* **priority**: numeric representation of the error level -* **priorityName**: string representation of the error level -* **message**: error message describing the error -* **extra**: an array of extra information that may help the developer debug the error: - * **file**: the file in which the error occurred - * **line**: the line from **file** where the error occurred +- **timestamp**: string representation of the date and time when the error occurred +- **priority**: numeric representation of the error level +- **priorityName**: string representation of the error level +- **message**: error message describing the error +- **extra**: an array of extra information that may help the developer debug the error: + - **file**: the file in which the error occurred + - **line**: the line from **file** where the error occurred By leveraging `dot-errorhandler`'s extra providers, you can also log additional request parameters. Learn more about what additional parameters are available on the [extra data](extra/introduction.md) page. From 7ab52744b354951d2a356480219e5752d4d19842 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Wed, 19 Feb 2025 17:59:12 +0200 Subject: [PATCH 06/14] test if Psalm version is the issue Signed-off-by: alexmerlin --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 185a6c0..0f4cd3a 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "laminas/laminas-coding-standard": "^3.0", "mikey179/vfsstream": "^1.6.7", "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^6.0" + "vimeo/psalm": "6.6.2" }, "autoload": { "psr-4": { From b0e9f3b2d2520a28fd0edd5abd0a4392ae9e5386 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 20 Feb 2025 10:43:20 +0200 Subject: [PATCH 07/14] Removed duplicate MiddlewareInterface from ErrorHandler Signed-off-by: alexmerlin --- src/ErrorHandler.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php index 2f21f75..67a409c 100644 --- a/src/ErrorHandler.php +++ b/src/ErrorHandler.php @@ -8,7 +8,6 @@ use Laminas\Stratigility\Middleware\ErrorResponseGenerator; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Throwable; @@ -17,7 +16,7 @@ use function restore_error_handler; use function set_error_handler; -class ErrorHandler implements MiddlewareInterface, ErrorHandlerInterface +class ErrorHandler implements ErrorHandlerInterface { /** @var callable[] */ private array $listeners = []; From 9ecee725a77614d8a5bba9868f4c6903adde68c5 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 20 Feb 2025 10:54:28 +0200 Subject: [PATCH 08/14] Try mikey179/vfsstream:1.6.11 Signed-off-by: alexmerlin --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0f4cd3a..a39bc0b 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "require-dev": { "laminas/laminas-coding-standard": "^3.0", - "mikey179/vfsstream": "^1.6.7", + "mikey179/vfsstream": "^1.6.11", "phpunit/phpunit": "^10.5", "vimeo/psalm": "6.6.2" }, From dc468cfe6c2531ebb9f36da004e03b856b50d0a8 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 20 Feb 2025 10:56:20 +0200 Subject: [PATCH 09/14] Try mikey179/vfsstream:1.6.12 Signed-off-by: alexmerlin --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a39bc0b..dd2ca37 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "require-dev": { "laminas/laminas-coding-standard": "^3.0", - "mikey179/vfsstream": "^1.6.11", + "mikey179/vfsstream": "^1.6.12", "phpunit/phpunit": "^10.5", "vimeo/psalm": "6.6.2" }, From 53bc5044b01d4133a0141d51ae117ace5be5a3ac Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 20 Feb 2025 11:04:07 +0200 Subject: [PATCH 10/14] Bump dependencies Signed-off-by: alexmerlin --- composer.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index dd2ca37..2ffef64 100644 --- a/composer.json +++ b/composer.json @@ -24,17 +24,17 @@ "require": { "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "dotkernel/dot-log": "^5.0", - "laminas/laminas-diactoros": "^3.3", + "laminas/laminas-diactoros": "^3.5", "laminas/laminas-stdlib": "^3.20", - "laminas/laminas-stratigility": "^3.11", - "mezzio/mezzio": "^3.19", + "laminas/laminas-stratigility": "^3.13", + "mezzio/mezzio": "^3.20.1", "psr/http-message": "^1.0 || ^2.0", - "psr/http-server-middleware": "^1.0" + "psr/http-server-middleware": "^1.0.2" }, "require-dev": { - "laminas/laminas-coding-standard": "^3.0", + "laminas/laminas-coding-standard": "^3.0.1", "mikey179/vfsstream": "^1.6.12", - "phpunit/phpunit": "^10.5", + "phpunit/phpunit": "^10.5.45", "vimeo/psalm": "6.6.2" }, "autoload": { From ac727b7bd4c3e403eda0877aabf2bb344d455a27 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 20 Feb 2025 11:08:11 +0200 Subject: [PATCH 11/14] Test fix Signed-off-by: alexmerlin --- test/LogErrorHandlerTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/LogErrorHandlerTest.php b/test/LogErrorHandlerTest.php index b48598a..f0e3522 100644 --- a/test/LogErrorHandlerTest.php +++ b/test/LogErrorHandlerTest.php @@ -177,6 +177,8 @@ public function testLogErrorHandlerLogsWillContainExtraKeysWhenProvidersAreEnabl ]) ); + $this->serverRequest->method('getCookieParams')->willReturn([]); + $extra = $logErrorHandler->provideExtra(new \Exception('test'), $this->serverRequest); $this->assertCount(8, $extra); From ea615474a4704dc303842187a9aaa465841ad858 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 20 Feb 2025 11:10:28 +0200 Subject: [PATCH 12/14] Test fix Signed-off-by: alexmerlin --- test/LogErrorHandlerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/LogErrorHandlerTest.php b/test/LogErrorHandlerTest.php index f0e3522..5ec8a86 100644 --- a/test/LogErrorHandlerTest.php +++ b/test/LogErrorHandlerTest.php @@ -178,6 +178,9 @@ public function testLogErrorHandlerLogsWillContainExtraKeysWhenProvidersAreEnabl ); $this->serverRequest->method('getCookieParams')->willReturn([]); + $this->serverRequest->method('getHeader')->willReturn([]); + $this->serverRequest->method('getParsedBody')->willReturn([]); + $this->serverRequest->method('getServerParams')->willReturn([]); $extra = $logErrorHandler->provideExtra(new \Exception('test'), $this->serverRequest); From 3ac9b7bb4f50d2c12d4b62628e98a8c07dbdb00f Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 20 Feb 2025 11:12:33 +0200 Subject: [PATCH 13/14] Test fix Signed-off-by: alexmerlin --- test/LogErrorHandlerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/LogErrorHandlerTest.php b/test/LogErrorHandlerTest.php index 5ec8a86..ff840e0 100644 --- a/test/LogErrorHandlerTest.php +++ b/test/LogErrorHandlerTest.php @@ -178,7 +178,7 @@ public function testLogErrorHandlerLogsWillContainExtraKeysWhenProvidersAreEnabl ); $this->serverRequest->method('getCookieParams')->willReturn([]); - $this->serverRequest->method('getHeader')->willReturn([]); + $this->serverRequest->method('getHeaders')->willReturn([]); $this->serverRequest->method('getParsedBody')->willReturn([]); $this->serverRequest->method('getServerParams')->willReturn([]); From 51786fcdbb1209619ca84dad323ce951b34a2587 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Thu, 20 Feb 2025 13:49:32 +0200 Subject: [PATCH 14/14] Badges, docs and mkdocs description Signed-off-by: alexmerlin --- README.md | 12 +++++++++--- composer.json | 1 - docs/book/v4/overview.md | 19 ++++++++++++++++++- mkdocs.yml | 2 +- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d51f7e1..85167ad 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # dot-errorhandler -Error Logging Handler for Dotkernel +dot-errorhandler is Dotkernel's PSR-15 compliant error handler. + +## Documentation + +Documentation is available at: https://docs.dotkernel.org/dot-errorhandler/ + +## Badges ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-errorhandler) ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-errorhandler/4.1.1) @@ -8,9 +14,9 @@ Error Logging Handler for Dotkernel [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-errorhandler)](https://github.com/dotkernel/dot-errorhandler/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-errorhandler)](https://github.com/dotkernel/dot-errorhandler/network) [![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-errorhandler)](https://github.com/dotkernel/dot-errorhandler/stargazers) -[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-errorhandler)](https://github.com/dotkernel/dot-errorhandler/blob/4.0/LICENSE) +[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-errorhandler)](https://github.com/dotkernel/dot-errorhandler/blob/4.1/LICENSE) -[![Build Static](https://github.com/dotkernel/dot-errorhandler/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0)](https://github.com/dotkernel/dot-errorhandler/actions/workflows/continuous-integration.yml) +[![Build Static](https://github.com/dotkernel/dot-errorhandler/actions/workflows/continuous-integration.yml/badge.svg?branch=4.1)](https://github.com/dotkernel/dot-errorhandler/actions/workflows/continuous-integration.yml) [![codecov](https://codecov.io/gh/dotkernel/dot-errorhandler/branch/4.0/graph/badge.svg?token=0KIJARS5RS)](https://codecov.io/gh/dotkernel/dot-errorhandler) ## Adding the error handler diff --git a/composer.json b/composer.json index 2ffef64..5999689 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,6 @@ "cs-check": "phpcs", "cs-fix": "phpcbf", "test": "phpunit --colors=always", - "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", "static-analysis": "psalm --shepherd --stats" } } diff --git a/docs/book/v4/overview.md b/docs/book/v4/overview.md index 6fc31dd..3eb446f 100644 --- a/docs/book/v4/overview.md +++ b/docs/book/v4/overview.md @@ -1,6 +1,23 @@ # Overview -`dot-errorhandler` is Dotkernel's logging error handler, providing two options: +dot-errorhandler is Dotkernel's PSR-15 compliant error handler. + +## Badges + +![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-errorhandler) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-errorhandler/4.1.1) + +[![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-errorhandler)](https://github.com/dotkernel/dot-errorhandler/issues) +[![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-errorhandler)](https://github.com/dotkernel/dot-errorhandler/network) +[![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-errorhandler)](https://github.com/dotkernel/dot-errorhandler/stargazers) +[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-errorhandler)](https://github.com/dotkernel/dot-errorhandler/blob/4.1/LICENSE) + +[![Build Static](https://github.com/dotkernel/dot-errorhandler/actions/workflows/continuous-integration.yml/badge.svg?branch=4.1)](https://github.com/dotkernel/dot-errorhandler/actions/workflows/continuous-integration.yml) +[![codecov](https://codecov.io/gh/dotkernel/dot-errorhandler/branch/4.0/graph/badge.svg?token=0KIJARS5RS)](https://codecov.io/gh/dotkernel/dot-errorhandler) + +## Features + +This package provides two features: - `Dot\ErrorHandler\ErrorHandler`, same as the Zend Expressive error handling class with the only difference being the removal of the `final` statement for making extension possible - `Dot\ErrorHandler\LogErrorHandler` adds logging support to the default `ErrorHandler` class diff --git a/mkdocs.yml b/mkdocs.yml index ca1e232..9d2f48e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,7 +26,7 @@ nav: - Installation: v3/installation.md - Configuration: v3/configuration.md site_name: dot-errorhandler -site_description: "Dotkernel's error logging handler" +site_description: "Dotkernel's PSR-15 compliant error handler" repo_url: "https://github.com/dotkernel/dot-errorhandler" plugins: - search