php-api-clients /
middleware-log
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php declare(strict_types=1); |
||
| 2 | |||
| 3 | namespace ApiClients\Middleware\Log; |
||
| 4 | |||
| 5 | use ApiClients\Foundation\Middleware\Annotation\Last; |
||
| 6 | use ApiClients\Foundation\Middleware\MiddlewareInterface; |
||
| 7 | use Psr\Http\Message\RequestInterface; |
||
| 8 | use Psr\Http\Message\ResponseInterface; |
||
| 9 | use Psr\Http\Message\UriInterface; |
||
| 10 | use Psr\Log\LoggerInterface; |
||
| 11 | use React\Promise\CancellablePromiseInterface; |
||
| 12 | use function React\Promise\reject; |
||
| 13 | use function React\Promise\resolve; |
||
| 14 | use Throwable; |
||
| 15 | use function WyriHaximus\getIn; |
||
| 16 | |||
| 17 | class LoggerMiddleware implements MiddlewareInterface |
||
| 18 | { |
||
| 19 | private const REQUEST = 'request'; |
||
| 20 | private const RESPONSE = 'response'; |
||
| 21 | private const ERROR = 'error'; |
||
| 22 | |||
| 23 | private const MESSAGE_URL = '[{{transaction_id}}] Requesting: {{request.uri}}'; |
||
| 24 | private const MESSAGE_SUCCESSFUL = '[{{transaction_id}}] Request completed with {{response.status_code}}'; |
||
| 25 | private const MESSAGE_ERROR = '[{{transaction_id}}] {{error.message}}'; |
||
| 26 | |||
| 27 | /** |
||
| 28 | * @var LoggerInterface |
||
| 29 | */ |
||
| 30 | private $logger; |
||
| 31 | |||
| 32 | private $context = []; |
||
| 33 | |||
| 34 | 6 | public function __construct(LoggerInterface $logger) |
|
| 35 | { |
||
| 36 | 6 | $this->logger = $logger; |
|
| 37 | 6 | } |
|
| 38 | |||
| 39 | /** |
||
| 40 | * @Last() |
||
| 41 | */ |
||
| 42 | 6 | public function pre( |
|
| 43 | RequestInterface $request, |
||
| 44 | string $transactionId, |
||
| 45 | array $options = [] |
||
| 46 | ): CancellablePromiseInterface { |
||
| 47 | 6 | if (!isset($options[self::class][Options::LEVEL]) && !isset($options[self::class][Options::ERROR_LEVEL])) { |
|
| 48 | 1 | return resolve($request); |
|
| 49 | } |
||
| 50 | |||
| 51 | 5 | $this->context[$transactionId] = [ |
|
| 52 | 5 | 'transaction_id' => $transactionId, |
|
| 53 | 5 | self::REQUEST => [], |
|
| 54 | ]; |
||
| 55 | 5 | $this->context[$transactionId][self::REQUEST]['method'] = $request->getMethod(); |
|
| 56 | 5 | $this->context[$transactionId][self::REQUEST]['uri'] = (string)$this->stripQueryItems( |
|
| 57 | 5 | $request->getUri(), |
|
| 58 | 5 | $options |
|
| 59 | ); |
||
| 60 | 5 | $this->context[$transactionId][self::REQUEST]['protocol_version'] = (string)$request->getProtocolVersion(); |
|
| 61 | 5 | $ignoreHeaders = $options[self::class][Options::IGNORE_HEADERS] ?? []; |
|
| 62 | 5 | $this->context[$transactionId] = $this->iterateHeaders( |
|
| 63 | 5 | $this->context[$transactionId], |
|
| 64 | 5 | self::REQUEST, |
|
| 65 | 5 | $request->getHeaders(), |
|
| 66 | 5 | $ignoreHeaders |
|
| 67 | ); |
||
| 68 | |||
| 69 | 5 | if (!isset($options[self::class][Options::URL_LEVEL])) { |
|
| 70 | 3 | return resolve($request); |
|
| 71 | } |
||
| 72 | |||
| 73 | 2 | $message = $this->renderTemplate( |
|
| 74 | 2 | $options[self::class][Options::MESSAGE_PRE] ?? self::MESSAGE_URL, |
|
| 75 | 2 | $this->context[$transactionId] |
|
| 76 | ); |
||
| 77 | 2 | $this->logger->log($options[self::class][Options::URL_LEVEL], $message, $this->context[$transactionId]); |
|
| 78 | |||
| 79 | 2 | return resolve($request); |
|
| 80 | } |
||
| 81 | |||
| 82 | /** |
||
| 83 | * @Last() |
||
| 84 | */ |
||
| 85 | 3 | public function post( |
|
| 86 | ResponseInterface $response, |
||
| 87 | string $transactionId, |
||
| 88 | array $options = [] |
||
| 89 | ): CancellablePromiseInterface { |
||
| 90 | 3 | if (!isset($this->context[$transactionId])) { |
|
| 91 | 1 | return resolve($response); |
|
| 92 | } |
||
| 93 | |||
| 94 | 2 | $context = $this->context[$transactionId]; |
|
| 95 | 2 | if (!isset($options[self::class][Options::LEVEL]) && !isset($options[self::class][Options::ERROR_LEVEL])) { |
|
| 96 | unset($this->context[$transactionId]); |
||
| 97 | } |
||
| 98 | |||
| 99 | 2 | if (!isset($options[self::class][Options::LEVEL])) { |
|
| 100 | return resolve($response); |
||
| 101 | } |
||
| 102 | |||
| 103 | 2 | $context = $this->addResponseToContext($context, $response, $options); |
|
| 104 | 2 | $message = $this->renderTemplate( |
|
| 105 | 2 | $options[self::class][Options::MESSAGE_POST] ?? self::MESSAGE_SUCCESSFUL, |
|
| 106 | 2 | $context |
|
| 107 | ); |
||
| 108 | 2 | $this->logger->log($options[self::class][Options::LEVEL], $message, $context); |
|
| 109 | |||
| 110 | 2 | return resolve($response); |
|
| 111 | } |
||
| 112 | |||
| 113 | /** |
||
| 114 | * @Last() |
||
| 115 | */ |
||
| 116 | 4 | public function error( |
|
| 117 | Throwable $throwable, |
||
| 118 | string $transactionId, |
||
| 119 | array $options = [] |
||
| 120 | ): CancellablePromiseInterface { |
||
| 121 | 4 | if (!isset($this->context[$transactionId])) { |
|
| 122 | 1 | return reject($throwable); |
|
| 123 | } |
||
| 124 | |||
| 125 | 3 | $context = $this->context[$transactionId]; |
|
| 126 | 3 | unset($this->context[$transactionId]); |
|
| 127 | |||
| 128 | 3 | if (!isset($options[self::class][Options::ERROR_LEVEL])) { |
|
| 129 | return reject($throwable); |
||
| 130 | } |
||
| 131 | |||
| 132 | 3 | $response = null; |
|
| 133 | 3 | if (\method_exists($throwable, 'getResponse')) { |
|
| 134 | 1 | $response = $throwable->getResponse(); |
|
|
0 ignored issues
–
show
|
|||
| 135 | } |
||
| 136 | 3 | if ($response instanceof ResponseInterface) { |
|
| 137 | 1 | $context = $this->addResponseToContext($context, $response, $options); |
|
| 138 | } |
||
| 139 | |||
| 140 | 3 | $context[self::ERROR]['message'] = $throwable->getMessage(); |
|
| 141 | 3 | $context[self::ERROR]['code'] = $throwable->getCode(); |
|
| 142 | 3 | $context[self::ERROR]['file'] = $throwable->getFile(); |
|
| 143 | 3 | $context[self::ERROR]['line'] = $throwable->getLine(); |
|
| 144 | 3 | $context[self::ERROR]['trace'] = $throwable->getTraceAsString(); |
|
| 145 | |||
| 146 | 3 | if (\method_exists($throwable, 'getContext')) { |
|
| 147 | $context[self::ERROR]['context'] = $throwable->getContext(); |
||
| 148 | } |
||
| 149 | |||
| 150 | 3 | $message = $this->renderTemplate( |
|
| 151 | 3 | $options[self::class][Options::MESSAGE_ERROR] ?? self::MESSAGE_ERROR, |
|
| 152 | 3 | $context |
|
| 153 | ); |
||
| 154 | 3 | $this->logger->log($options[self::class][Options::ERROR_LEVEL], $message, $context); |
|
| 155 | |||
| 156 | 3 | return reject($throwable); |
|
| 157 | } |
||
| 158 | |||
| 159 | 5 | protected function iterateHeaders( |
|
| 160 | array $context, |
||
| 161 | string $prefix, |
||
| 162 | array $headers, |
||
| 163 | array $ignoreHeaders |
||
| 164 | ): array { |
||
| 165 | 5 | foreach ($headers as $header => $value) { |
|
| 166 | 5 | if (\in_array($header, $ignoreHeaders, true)) { |
|
| 167 | 5 | continue; |
|
| 168 | } |
||
| 169 | |||
| 170 | 5 | $context[$prefix]['headers'][$header] = $value; |
|
| 171 | } |
||
| 172 | |||
| 173 | 5 | return $context; |
|
| 174 | } |
||
| 175 | |||
| 176 | 3 | private function addResponseToContext( |
|
| 177 | array $context, |
||
| 178 | ResponseInterface $response, |
||
| 179 | array $options |
||
| 180 | ): array { |
||
| 181 | 3 | $context[self::RESPONSE]['status_code'] = $response->getStatusCode(); |
|
| 182 | 3 | $context[self::RESPONSE]['status_reason'] = $response->getReasonPhrase(); |
|
| 183 | 3 | $context[self::RESPONSE]['protocol_version'] = $response->getProtocolVersion(); |
|
| 184 | 3 | $ignoreHeaders = $options[self::class][Options::IGNORE_HEADERS] ?? []; |
|
| 185 | 3 | $context = $this->iterateHeaders( |
|
| 186 | 3 | $context, |
|
| 187 | 3 | self::RESPONSE, |
|
| 188 | 3 | $response->getHeaders(), |
|
| 189 | 3 | $ignoreHeaders |
|
| 190 | ); |
||
| 191 | |||
| 192 | 3 | return $context; |
|
| 193 | } |
||
| 194 | |||
| 195 | 5 | private function stripQueryItems(UriInterface $uri, array $options): UriInterface |
|
| 196 | { |
||
| 197 | 5 | \parse_str($uri->getQuery(), $query); |
|
| 198 | 5 | foreach ($options[self::class][Options::IGNORE_URI_QUERY_ITEMS] ?? [] as $item) { |
|
| 199 | 2 | unset($query[$item], $query[$item . '[]']); |
|
| 200 | } |
||
| 201 | |||
| 202 | 5 | return $uri->withQuery(\http_build_query($query)); |
|
| 203 | } |
||
| 204 | |||
| 205 | 5 | private function renderTemplate(string $template, array $context): string |
|
| 206 | { |
||
| 207 | 5 | $keyValues = []; |
|
| 208 | 5 | \preg_match_all("|\{\{(.*)\}\}|U", $template, $out, \PREG_PATTERN_ORDER); |
|
| 209 | 5 | foreach (\array_unique(\array_values($out[1])) as $placeHolder) { |
|
| 210 | 5 | $keyValues['{{' . $placeHolder . '}}'] = getIn($context, $placeHolder, ''); |
|
| 211 | } |
||
| 212 | 5 | $template = \str_replace(\array_keys($keyValues), \array_values($keyValues), $template); |
|
| 213 | |||
| 214 | 5 | return $template; |
|
| 215 | } |
||
| 216 | } |
||
| 217 |
Let’s take a look at an example:
In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.
Available Fixes
Change the type-hint for the parameter:
Add an additional type-check:
Add the method to the interface: