Completed
Push — master ( 3e15a8...ec1159 )
by Mihail
09:16
created

ServerRequest.php (1 issue)

1
<?php
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded\Http;
14
15
use InvalidArgumentException;
16
use Koded\Http\Interfaces\Request;
17
use Psr\Http\Message\ServerRequestInterface;
18
19
20
class ServerRequest extends ClientRequest implements Request
21
{
22
    use CookieTrait, FilesTrait, ValidatableTrait;
23
24
    /** @var string */
25
    protected $serverSoftware = '';
26
27
    /** @var array */
28
    protected $attributes = [];
29
30
    /** @var array */
31
    protected $queryParams = [];
32
33
    /** @var null|array */
34
    protected $parsedBody;
35
36
    /**
37
     * ServerRequest constructor.
38
     *
39
     * @param array $attributes
40
     */
41
    public function __construct(array $attributes = [])
42
    {
43
        parent::__construct($_SERVER['REQUEST_METHOD'] ?? Request::GET, $this->buildUri());
44
        $this->attributes = $attributes;
45
        $this->extractHttpHeaders($_SERVER);
46
        $this->extractServerData($_SERVER);
47
    }
48
49
    public function getServerParams(): array
50
    {
51
        return $_SERVER;
52
    }
53
54
    public function getQueryParams(): array
55
    {
56
        return $this->queryParams;
57
    }
58
59
    public function withQueryParams(array $query): ServerRequest
60
    {
61
        $instance              = clone $this;
62
        $instance->queryParams = array_merge($instance->queryParams, $query);
63
64
        return $instance;
65
    }
66
67
    public function getParsedBody()
68
    {
69
        if ($this->useOnlyPost()) {
70
            return $_POST;
71
        }
72
73
        if (false === empty($_POST)) {
74
            return $_POST;
75
        }
76
77
        return $this->parsedBody;
78
    }
79
80
    public function withParsedBody($data): ServerRequest
81
    {
82
        $instance = clone $this;
83
84
        if ($this->useOnlyPost()) {
85
            $instance->parsedBody = $_POST;
86
87
            return $instance;
88
        }
89
90
        // If nothing is available for the body
91
        if (null === $data) {
92
            $instance->parsedBody = null;
93
94
            return $instance;
95
        }
96
97
        // Supports array or iterable object
98
        if (is_iterable($data)) {
99
            $instance->parsedBody = is_array($data) ? $data : iterator_to_array($data);
100
101
            return $instance;
102
        }
103
104
        if (is_object($data)) {
105
            $instance->parsedBody = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type object is incompatible with the declared type array|null of property $parsedBody.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
106
107
            return $instance;
108
        }
109
110
        throw new InvalidArgumentException(
111
            sprintf('Unsupported data provided (%s), Expects NULL, array or iterable', gettype($data))
112
        );
113
    }
114
115
    public function getAttributes(): array
116
    {
117
        return $this->attributes;
118
    }
119
120
    public function getAttribute($name, $default = null)
121
    {
122
        return $this->attributes[$name] ?? $default;
123
    }
124
125
    public function withAttribute($name, $value): ServerRequest
126
    {
127
        $instance                    = clone $this;
128
        $instance->attributes[$name] = $value;
129
130
        return $instance;
131
    }
132
133
    public function withoutAttribute($name): ServerRequest
134
    {
135
        $instance = clone $this;
136
        unset($instance->attributes[$name]);
137
138
        return $instance;
139
    }
140
141
    public function withAttributes(array $attributes): Request
142
    {
143
        $instance = clone $this;
144
145
        foreach ($attributes as $name => $value) {
146
            $instance->attributes[$name] = $value;
147
        }
148
149
        return $instance;
150
    }
151
152
    public function isXHR(): bool
153
    {
154
        return 'XMLHTTPREQUEST' === strtoupper($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '');
155
    }
156
157
    protected function buildUri(): Uri
158
    {
159
        if (strpos($_SERVER['REQUEST_URI'] ?? '', '://')) {
160
            return new Uri($_SERVER['REQUEST_URI']);
161
        }
162
163
        if ($host = $_SERVER['SERVER_NAME'] ?? $_SERVER['SERVER_ADDR'] ?? '') {
164
            return new Uri('http' . ($_SERVER['HTTPS'] ?? $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? false ? 's' : '')
165
                . '://' . $host
166
                . ':' . ($_SERVER['SERVER_PORT'] ?? 80)
167
                . ($_SERVER['REQUEST_URI'] ?? '')
168
            );
169
        }
170
171
        return new Uri($_SERVER['REQUEST_URI'] ?? '');
172
    }
173
174
    protected function extractHttpHeaders(array $server): void
175
    {
176
        foreach ($server as $k => $v) {
177
            // Calisthenics :)
178
            0 === strpos($k, 'HTTP_', 0) and $this->normalizeHeader(str_replace('HTTP_', '', $k), $v, false);
179
        }
180
181
        unset($this->headers['X-Forwarded-For'], $this->headers['X-Forwarded-Proto']);
182
        unset($this->headersMap['x-forwarded-for'], $this->headersMap['x-forwarded-proto']);
183
184
        if (isset($server['HTTP_IF_NONE_MATCH'])) {
185
            // ETag workaround for various broken Apache2 versions
186
            $this->headers['ETag']    = str_replace('-gzip', '', $server['HTTP_IF_NONE_MATCH']);
187
            $this->headersMap['etag'] = 'ETag';
188
        }
189
190
        if (isset($server['CONTENT_TYPE'])) {
191
            $this->headers['Content-Type']    = strtolower($server['CONTENT_TYPE']);
192
            $this->headersMap['content-type'] = 'Content-Type';
193
        }
194
195
        $this->setHost();
196
    }
197
198
    protected function extractServerData(array $server): void
199
    {
200
        $this->protocolVersion = str_ireplace('HTTP/', '', $server['SERVER_PROTOCOL'] ?? $this->protocolVersion);
201
        $this->serverSoftware  = $server['SERVER_SOFTWARE'] ?? '';
202
        $this->queryParams     = $_GET;
203
        $this->cookieParams    = $_COOKIE;
204
205
        if (false === $this->isSafeMethod()) {
206
            $this->parseInput();
207
        }
208
209
        if ($_FILES) {
210
            $this->uploadedFiles = $this->parseUploadedFiles($_FILES);
211
        }
212
    }
213
214
    /**
215
     * Per recommendation:
216
     *
217
     * @return bool If the request Content-Type is either
218
     * application/x-www-form-urlencoded or multipart/form-data
219
     * and the request method is POST,
220
     * then it MUST return the contents of $_POST
221
     * @see ServerRequestInterface::withParsedBody()
222
     *
223
     * @see ServerRequestInterface::getParsedBody()
224
     */
225
    protected function useOnlyPost(): bool
226
    {
227
        if (empty($contentType = $this->getHeaderLine('Content-Type'))) {
228
            return false;
229
        }
230
231
        return $this->method === self::POST && (
232
                false !== strpos('application/x-www-form-urlencoded', $contentType) ||
233
                false !== strpos('multipart/form-data', $contentType));
234
    }
235
236
    /**
237
     * Try to unserialize a JSON string or form encoded request body.
238
     * Very useful if JavaScript app stringify objects in AJAX requests.
239
     */
240
    protected function parseInput(): void
241
    {
242
        if (empty($input = $this->getRawInput())) {
243
            return;
244
        }
245
246
        // Try JSON deserialization
247
        $this->parsedBody = json_decode($input, true, 512, JSON_BIGINT_AS_STRING);
248
249
        if (null === $this->parsedBody) {
250
            parse_str($input, $this->parsedBody);
251
        }
252
    }
253
254
    protected function getRawInput(): string
255
    {
256
        return file_get_contents('php://input') ?: '';
257
    }
258
}
259