Lines 57.40% 31 / 54
Methods 25.00% 2 / 8
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 85.71% 6 / 7 0.00% 0 / 1 3.03
 process 100.00% 5 / 5 100.00% 1 / 1 2
 sanitizeRequest 63.63% 7 / 11 0.00% 0 / 1 3.43
 sanitizeData 100.00% 1 / 1 100.00% 1 / 1 1
 sanitizeDataWithDepth 70.00% 7 / 10 0.00% 0 / 1 5.68
 sanitizeObject 0.00% 0 / 1 0.00% 0 / 1 2
 sanitizeObjectWithDepth 0.00% 0 / 10 0.00% 0 / 1 42
 sanitizeString 55.55% 5 / 9 0.00% 0 / 1 5.40
13class RequestSanitizerMiddleware implements MiddlewareInterface
14{
15    private int $maxRecursionDepth;
16    private int $maxStringLength;
17    private LoggerService $logger;
18
19    public function __construct(LoggerService $logger, int $maxRecursionDepth = 10, int $maxStringLength = 32768)
20    {
21        if ($maxRecursionDepth < 1) {
22            throw new \InvalidArgumentException('maxRecursionDepth must be greater than 0');
23        }
24        if ($maxStringLength < 1) {
25            throw new \InvalidArgumentException('maxStringLength must be greater than 0');
26        }
27
28        $this->logger = $logger;
29        $this->maxRecursionDepth = $maxRecursionDepth;
30        $this->maxStringLength = $maxStringLength;
31    }
32
33    #[\Override]
34    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
35    {
36        try {
37            $request = $this->sanitizeRequest($request);
38
39            return $handler->handle($request);
40        } catch (\Throwable $e) {
41            $this->logger->logError($e, $request);
42            throw $e;
43        }
44    }
45
46    private function sanitizeRequest(ServerRequestInterface $request): ServerRequestInterface
47    {
48        $queryParams = $request->getQueryParams();
49        $sanitizedQueryParams = $this->sanitizeData($queryParams);
50        $request = $request->withQueryParams($sanitizedQueryParams);
51
52        $parsedBody = $request->getParsedBody();
53        if (is_array($parsedBody)) {
54            $sanitizedParsedBody = $this->sanitizeData($parsedBody);
55            $request = $request->withParsedBody($sanitizedParsedBody);
56        } elseif (is_object($parsedBody)) {
57            $sanitizedParsedBody = $this->sanitizeObject($parsedBody);
58            $request = $request->withParsedBody($sanitizedParsedBody);
59        }
60
61        return $request;
62    }
63
64    private function sanitizeData(array $data): array
65    {
66        return $this->sanitizeDataWithDepth($data, 0);
67    }
68
69    private function sanitizeDataWithDepth(array $data, int $depth): array
70    {
71        if ($depth >= $this->maxRecursionDepth) {
72            throw new \RuntimeException('Maximum recursion depth exceeded');
73        }
74
75        $sanitized = [];
76        foreach ($data as $key => $value) {
77            if (is_array($value)) {
78                $sanitized[$key] = $this->sanitizeDataWithDepth($value, $depth + 1);
79            } elseif (is_string($value)) {
80                $sanitized[$key] = $this->sanitizeString($value);
81            } else {
82                $sanitized[$key] = $value;
83            }
84        }
85        return $sanitized;
86    }
87
88    private function sanitizeObject(object $data): object
89    {
90        return $this->sanitizeObjectWithDepth($data, 0);
91    }
92
93    private function sanitizeObjectWithDepth(object $data, int $depth): object
94    {
95        if ($depth >= $this->maxRecursionDepth) {
96            throw new \RuntimeException('Maximum recursion depth exceeded');
97        }
98
99        foreach ($data as $key => $value) {
100            if (is_array($value)) {
101                $data->$key = $this->sanitizeDataWithDepth($value, $depth + 1);
102            } elseif (is_object($value)) {
103                $data->$key = $this->sanitizeObjectWithDepth($value, $depth + 1);
104            } elseif (is_string($value)) {
105                $data->$key = $this->sanitizeString($value);
106            }
107        }
108        return $data;
109    }
110
111    private function sanitizeString(string $value): string
112    {
113        if (strlen($value) > $this->maxStringLength) {
114            throw new \RuntimeException('String exceeds maximum length');
115        }
116
117        $value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value) ?? '';
118        $value = trim($value);
119        if (!mb_check_encoding($value, 'UTF-8')) {
120            $this->logger->logWarning('Invalid string encoding detected.', ['value' => $value]);
121            $encoded = mb_convert_encoding($value, 'UTF-8', 'auto');
122            $value = is_string($encoded) ? $encoded : $value;
123        }
124        return $value;
125    }
126}