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/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 c3ce551..5999689 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,18 +23,19 @@ }, "require": { "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "dotkernel/dot-log": "^4.1.0", - "laminas/laminas-diactoros": "^3.3", - "laminas/laminas-stratigility": "^3.11", - "mezzio/mezzio": "^3.19", + "dotkernel/dot-log": "^5.0", + "laminas/laminas-diactoros": "^3.5", + "laminas/laminas-stdlib": "^3.20", + "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", - "mikey179/vfsstream": "^1.6.7", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^6.0" + "laminas/laminas-coding-standard": "^3.0.1", + "mikey179/vfsstream": "^1.6.12", + "phpunit/phpunit": "^10.5.45", + "vimeo/psalm": "6.6.2" }, "autoload": { "psr-4": { @@ -60,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/config/error-handling.global.php.dist b/config/error-handling.global.php.dist index eb07019..ac7908a 100644 --- a/config/error-handling.global.php.dist +++ b/config/error-handling.global.php.dist @@ -1,16 +1,84 @@ [ + 'dependencies' => [ 'aliases' => [ ErrorHandlerInterface::class => LogErrorHandler::class, - ] + ], ], 'dot-errorhandler' => [ - 'loggerEnabled' => true, - 'logger' => 'dot-log.default_logger' - ] + 'loggerEnabled' => true, + 'logger' => 'dot-log.default_logger', + ExtraProvider::CONFIG_KEY => [ + CookieProvider::class => [ + 'enabled' => false, + 'processor' => [ + 'class' => CookieProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + ProcessorInterface::ALL, + ], + ], + ], + HeaderProvider::class => [ + 'enabled' => false, + 'processor' => [ + 'class' => HeaderProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + ], + ], + RequestProvider::class => [ + 'enabled' => false, + 'processor' => [ + 'class' => RequestProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + 'password', + ], + ], + ], + ServerProvider::class => [ + 'enabled' => false, + 'processor' => [ + 'class' => ServerProcessor::class, + 'replacementStrategy' => ReplacementStrategy::Full, + 'sensitiveParameters' => [ + ProcessorInterface::ALL, + ], + ], + ], + SessionProvider::class => [ + 'enabled' => false, + 'processor' => [ + 'class' => SessionProcessor::class, + ], + ], + TraceProvider::class => [ + 'enabled' => true, + '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..494cb8d --- /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..cd686e9 --- /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/introduction.md) page. 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 b95a4df..9d2f48e 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 PSR-15 compliant error 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/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 = []; 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 new file mode 100644 index 0000000..290ce91 --- /dev/null +++ b/src/Extra/ExtraProvider.php @@ -0,0 +1,122 @@ + CookieProvider::class, + 'header' => HeaderProvider::class, + 'request' => RequestProvider::class, + 'server' => ServerProvider::class, + 'session' => SessionProvider::class, + 'trace' => TraceProvider::class, + ]; + + 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']; + } + + $processor = new $options[$extraClass]['processor']['class']($sensitiveParameters, $replacementStrategy); + assert($processor instanceof ProcessorInterface); + + $this->$extraKey = new $extraClass(true, $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/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 new file mode 100644 index 0000000..b4d858f --- /dev/null +++ b/src/Extra/Processor/CookieProcessor.php @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000..cd65020 --- /dev/null +++ b/src/Extra/Processor/HeaderProcessor.php @@ -0,0 +1,42 @@ + $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 new file mode 100644 index 0000000..e6e4bf7 --- /dev/null +++ b/src/Extra/Processor/ProcessorInterface.php @@ -0,0 +1,26 @@ +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); + } 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; + } + + $return[$key] = $this->replace($this->replacementStrategy, (string) $value); + } + } + + return $return; + } +} diff --git a/src/Extra/Processor/ServerProcessor.php b/src/Extra/Processor/ServerProcessor.php new file mode 100644 index 0000000..da6d708 --- /dev/null +++ b/src/Extra/Processor/ServerProcessor.php @@ -0,0 +1,70 @@ + $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 new file mode 100644 index 0000000..f02a570 --- /dev/null +++ b/src/Extra/Processor/SessionProcessor.php @@ -0,0 +1,20 @@ + ArrayUtils::iteratorToArray($container), + $data + ); + } +} diff --git a/src/Extra/Processor/TraceProcessor.php b/src/Extra/Processor/TraceProcessor.php new file mode 100644 index 0000000..ec632c7 --- /dev/null +++ b/src/Extra/Processor/TraceProcessor.php @@ -0,0 +1,28 @@ + 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/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 new file mode 100644 index 0000000..9e68fb3 --- /dev/null +++ b/src/Extra/Provider/CookieProvider.php @@ -0,0 +1,19 @@ +processor instanceof ProcessorInterface) { + return $this->processor->process($data); + } + + return $data; + } +} diff --git a/src/Extra/Provider/HeaderProvider.php b/src/Extra/Provider/HeaderProvider.php new file mode 100644 index 0000000..5d838aa --- /dev/null +++ b/src/Extra/Provider/HeaderProvider.php @@ -0,0 +1,19 @@ +processor instanceof ProcessorInterface) { + return $this->processor->process($data); + } + + 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($data); + } + + return $data; + } +} diff --git a/src/Extra/Provider/ServerProvider.php b/src/Extra/Provider/ServerProvider.php new file mode 100644 index 0000000..2b196a4 --- /dev/null +++ b/src/Extra/Provider/ServerProvider.php @@ -0,0 +1,19 @@ +processor instanceof ProcessorInterface) { + return $this->processor->process($data); + } + + return $data; + } +} diff --git a/src/Extra/Provider/SessionProvider.php b/src/Extra/Provider/SessionProvider.php new file mode 100644 index 0000000..09b4449 --- /dev/null +++ b/src/Extra/Provider/SessionProvider.php @@ -0,0 +1,19 @@ +processor instanceof ProcessorInterface) { + return $this->processor->process($data); + } + + return $data; + } +} diff --git a/src/Extra/Provider/TraceProvider.php b/src/Extra/Provider/TraceProvider.php new file mode 100644 index 0000000..989091f --- /dev/null +++ b/src/Extra/Provider/TraceProvider.php @@ -0,0 +1,19 @@ +processor instanceof ProcessorInterface) { + return $this->processor->process($data); + } + + 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 @@ +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,14 +82,15 @@ 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 { $generator = $this->responseGenerator; if ($this->logger instanceof LoggerInterface) { - $this->logger->err($e->getMessage(), (array) $e); + $extra = $this->provideExtra($e, $request); + $this->logger->error($e->getMessage(), $extra); } $response = $generator($e, $request, ($this->responseFactory)()); @@ -126,4 +131,43 @@ public function triggerListeners( $listener($error, $request, $response); } } + + public function provideExtra(Throwable $throwable, ServerRequestInterface $request): array + { + $extra = [ + 'file' => $throwable->getFile(), + 'line' => $throwable->getLine(), + ]; + + if ($this->extraProvider?->getCookie()->isEnabled()) { + $extra['cookie'] = $this->extraProvider?->getCookie()->provide($request->getCookieParams()); + } + + if ($this->extraProvider?->getHeader()->isEnabled()) { + $extra['header'] = $this->extraProvider?->getHeader()->provide($request->getHeaders()); + } + + if ($this->extraProvider?->getRequest()->isEnabled()) { + $extra['request'] = $this->extraProvider?->getRequest()->provide((array) $request->getParsedBody()); + } + + if ($this->extraProvider?->getServer()->isEnabled()) { + $extra['server'] = $this->extraProvider?->getServer()->provide($request->getServerParams()); + } + + if ($this->extraProvider?->getSession()->isEnabled()) { + $extra['session'] = $this->extraProvider?->getSession()->provide($_SESSION ?? []); + } + + 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 4220e4a..9f89f74 100644 --- a/src/LogErrorHandlerFactory.php +++ b/src/LogErrorHandlerFactory.php @@ -4,7 +4,7 @@ namespace Dot\ErrorHandler; -use Dot\Log\LoggerInterface; +use Dot\ErrorHandler\Extra\ExtraProvider; use InvalidArgumentException; use Mezzio\Middleware\ErrorResponseGenerator; use Psr\Container\ContainerExceptionInterface; @@ -12,7 +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; @@ -51,6 +54,20 @@ public function __invoke(ContainerInterface $container): MiddlewareInterface ? $container->get(ErrorResponseGenerator::class) : null; - return new LogErrorHandler($container->get(ResponseInterface::class), $generator, $logger); + $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, + $logger, + $extraProvider + ); } } 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..ff840e0 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,115 @@ 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], + ]) + ); + + $this->serverRequest->method('getCookieParams')->willReturn([]); + $this->serverRequest->method('getHeaders')->willReturn([]); + $this->serverRequest->method('getParsedBody')->willReturn([]); + $this->serverRequest->method('getServerParams')->willReturn([]); + + $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 +353,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, ], ], ],