• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IlyasDeckers / ody-core / 13532154862

25 Feb 2025 10:24PM UTC coverage: 30.374% (+1.7%) from 28.706%
13532154862

push

github

web-flow
Update php.yml

544 of 1791 relevant lines covered (30.37%)

9.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

21.74
/src/Middleware/BodyParsingMiddleware.php
1
<?php
2
declare(strict_types=1);
3

4
namespace Ody\Core\Middleware;
5

6
use Psr\Http\Message\ResponseInterface;
7
use Psr\Http\Message\ServerRequestInterface;
8
use Psr\Http\Server\MiddlewareInterface;
9
use Psr\Http\Server\RequestHandlerInterface;
10
use RuntimeException;
11

12
use function count;
13
use function explode;
14
use function is_array;
15
use function is_null;
16
use function is_object;
17
use function is_string;
18
use function json_decode;
19
use function libxml_clear_errors;
20
use function libxml_disable_entity_loader;
21
use function libxml_use_internal_errors;
22
use function parse_str;
23
use function simplexml_load_string;
24
use function strtolower;
25
use function trim;
26

27
use const LIBXML_VERSION;
28

29
/** @api */
30
class BodyParsingMiddleware implements MiddlewareInterface
31
{
32
    /**
33
     * @var callable[]
34
     */
35
    protected array $bodyParsers;
36

37
    /**
38
     * @param callable[] $bodyParsers list of body parsers as an associative array of mediaType => callable
39
     */
40
    public function __construct(array $bodyParsers = [])
1✔
41
    {
42
        $this->registerDefaultBodyParsers();
1✔
43

44
        foreach ($bodyParsers as $mediaType => $parser) {
1✔
45
            $this->registerBodyParser($mediaType, $parser);
×
46
        }
47
    }
48

49
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
×
50
    {
51
        $parsedBody = $request->getParsedBody();
×
52

53
        if (empty($parsedBody)) {
×
54
            $parsedBody = $this->parseBody($request);
×
55
            $request = $request->withParsedBody($parsedBody);
×
56
        }
57

58
        return $handler->handle($request);
×
59
    }
60

61
    /**
62
     * @param string   $mediaType A HTTP media type (excluding content-type params).
63
     * @param callable $callable  A callable that returns parsed contents for media type.
64
     */
65
    public function registerBodyParser(string $mediaType, callable $callable): self
1✔
66
    {
67
        $this->bodyParsers[$mediaType] = $callable;
1✔
68
        return $this;
1✔
69
    }
70

71
    /**
72
     * @param string   $mediaType A HTTP media type (excluding content-type params).
73
     */
74
    public function hasBodyParser(string $mediaType): bool
×
75
    {
76
        return isset($this->bodyParsers[$mediaType]);
×
77
    }
78

79
    /**
80
     * @param string    $mediaType A HTTP media type (excluding content-type params).
81
     * @throws RuntimeException
82
     */
83
    public function getBodyParser(string $mediaType): callable
×
84
    {
85
        if (!isset($this->bodyParsers[$mediaType])) {
×
86
            throw new RuntimeException('No parser for type ' . $mediaType);
×
87
        }
88
        return $this->bodyParsers[$mediaType];
×
89
    }
90

91
    protected function registerDefaultBodyParsers(): void
1✔
92
    {
93
        $this->registerBodyParser('application/json', static function ($input) {
1✔
94
            $result = json_decode($input, true);
×
95

96
            if (!is_array($result)) {
×
97
                return null;
×
98
            }
99

100
            return $result;
×
101
        });
1✔
102

103
        $this->registerBodyParser('application/x-www-form-urlencoded', static function ($input) {
1✔
104
            parse_str($input, $data);
×
105
            return $data;
×
106
        });
1✔
107

108
        $xmlCallable = static function ($input): \SimpleXMLElement|null {
1✔
109
            $backup = self::disableXmlEntityLoader(true);
×
110
            $backup_errors = libxml_use_internal_errors(true);
×
111
            $result = simplexml_load_string($input);
×
112

113
            self::disableXmlEntityLoader($backup);
×
114
            libxml_clear_errors();
×
115
            libxml_use_internal_errors($backup_errors);
×
116

117
            if ($result === false) {
×
118
                return null;
×
119
            }
120

121
            return $result;
×
122
        };
1✔
123

124
        $this->registerBodyParser('application/xml', $xmlCallable);
1✔
125
        $this->registerBodyParser('text/xml', $xmlCallable);
1✔
126
    }
127

128
    /**
129
     * @return null|array<mixed>|object
130
     */
131
    protected function parseBody(ServerRequestInterface $request)
×
132
    {
133
        $mediaType = $this->getMediaType($request);
×
134
        if ($mediaType === null) {
×
135
            return null;
×
136
        }
137

138
        // Check if this specific media type has a parser registered first
139
        if (!isset($this->bodyParsers[$mediaType])) {
×
140
            // If not, look for a media type with a structured syntax suffix (RFC 6839)
141
            $parts = explode('+', $mediaType);
×
142
            if (count($parts) >= 2) {
×
143
                $mediaType = 'application/' . $parts[count($parts) - 1];
×
144
            }
145
        }
146

147
        if (isset($this->bodyParsers[$mediaType])) {
×
148
            $body = (string)$request->getBody();
×
149
            $parsed = $this->bodyParsers[$mediaType]($body);
×
150

151
            if ($parsed !== null && !is_object($parsed) && !is_array($parsed)) {
×
152
                throw new RuntimeException(
×
153
                    'Request body media type parser return value must be an array, an object, or null'
×
154
                );
×
155
            }
156

157
            return $parsed;
×
158
        }
159

160
        return null;
×
161
    }
162

163
    /**
164
     * @return string|null The serverRequest media type, minus content-type params
165
     */
166
    protected function getMediaType(ServerRequestInterface $request): ?string
×
167
    {
168
        $contentType = $request->getHeader('Content-Type')[0] ?? null;
×
169

170
        if (is_string($contentType) && trim($contentType) !== '') {
×
171
            $contentTypeParts = explode(';', $contentType);
×
172
            return strtolower(trim($contentTypeParts[0]));
×
173
        }
174

175
        return null;
×
176
    }
177

178
    protected static function disableXmlEntityLoader(bool $disable): bool
×
179
    {
180
        if (LIBXML_VERSION >= 20900) {
×
181
            // libxml >= 2.9.0 disables entity loading by default, so it is
182
            // safe to skip the real call (deprecated in PHP 8).
183
            return true;
×
184
        }
185

186
        // @codeCoverageIgnoreStart
187
        return libxml_disable_entity_loader($disable);
188
        // @codeCoverageIgnoreEnd
189
    }
190
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc