Completed
Push — v3 ( 4ecdc9...abfb2c )
by Mihail
06:27
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
    protected string $serverSoftware = '';
25
    protected array  $attributes     = [];
26
    protected array  $queryParams    = [];
27
28
    protected object|array|null $parsedBody = null;
29
30
    /**
31
     * ServerRequest constructor.
32
     *
33
     * @param array $attributes
34
     */
35
    public function __construct(array $attributes = [])
36
    {
37
        parent::__construct($_SERVER['REQUEST_METHOD'] ?? Request::GET, $this->buildUri());
38
        $this->attributes = $attributes;
39
        $this->extractHttpHeaders($_SERVER);
40
        $this->extractServerData($_SERVER);
41
    }
42
43
    public function getServerParams(): array
44
    {
45
        return $_SERVER;
46
    }
47
48
    public function getQueryParams(): array
49
    {
50
        return $this->queryParams;
51
    }
52
53
    public function withQueryParams(array $query): ServerRequest
54
    {
55
        $instance              = clone $this;
56
        $instance->queryParams = array_merge($instance->queryParams, $query);
57
        return $instance;
58
    }
59
60
    public function getParsedBody()
61
    {
62
        if ($this->useOnlyPost()) {
63
            return $_POST;
64
        }
65
        if (false === empty($_POST)) {
66
            return $_POST;
67
        }
68
        return $this->parsedBody;
69
    }
70
71
    public function withParsedBody($data): ServerRequest
72
    {
73
        $instance = clone $this;
74
        if ($this->useOnlyPost()) {
75
            $instance->parsedBody = $_POST;
76
            return $instance;
77
        }
78
        // If nothing is available for the body
79
        if (null === $data) {
80
            $instance->parsedBody = null;
81
            return $instance;
82
        }
83
        // Supports array or iterable object
84
        if (is_iterable($data)) {
85
            $instance->parsedBody = is_array($data) ? $data : iterator_to_array($data);
86
            return $instance;
87
        }
88
        if (is_object($data)) {
89
            $instance->parsedBody = $data;
90
            return $instance;
91
        }
92
        throw new InvalidArgumentException(
93
            sprintf('Unsupported data provided (%s), Expects NULL, array or iterable', gettype($data))
94
        );
95
    }
96
97
    public function getAttributes(): array
98
    {
99
        return $this->attributes;
100
    }
101
102
    public function getAttribute($name, $default = null)
103
    {
104
        return $this->attributes[$name] ?? $default;
105
    }
106
107
    public function withAttribute($name, $value): ServerRequest
108
    {
109
        $instance                    = clone $this;
110
        $instance->attributes[$name] = $value;
111
        return $instance;
112
    }
113
114
    public function withoutAttribute($name): ServerRequest
115
    {
116
        $instance = clone $this;
117
        unset($instance->attributes[$name]);
118
        return $instance;
119
    }
120
121
    public function withAttributes(array $attributes): Request
122
    {
123
        $instance = clone $this;
124
        foreach ($attributes as $name => $value) {
125
            $instance->attributes[$name] = $value;
126
        }
127
        return $instance;
128
    }
129
130
    public function isXHR(): bool
131
    {
132
        return 'XMLHTTPREQUEST' === strtoupper($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '');
133
    }
134
135
    protected function buildUri(): Uri
136
    {
137
        if (strpos($_SERVER['REQUEST_URI'] ?? '', '://')) {
138
            return new Uri($_SERVER['REQUEST_URI']);
139
        }
140
        if ($host = $_SERVER['SERVER_NAME'] ?? $_SERVER['SERVER_ADDR'] ?? '') {
141
            return new Uri('http' . ($_SERVER['HTTPS'] ?? $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? false ? 's' : '')
142
                . '://' . $host
143
                . ':' . ($_SERVER['SERVER_PORT'] ?? 80)
144
                . ($_SERVER['REQUEST_URI'] ?? '')
145
            );
146
        }
147
        return new Uri($_SERVER['REQUEST_URI'] ?? '');
148
    }
149
150
    protected function extractHttpHeaders(array $server): void
151
    {
152
        foreach ($server as $k => $v) {
153
            // Calisthenics :)
154
            0 === strpos($k, 'HTTP_', 0) && $this->normalizeHeader(str_replace('HTTP_', '', $k), $v, false);
155
        }
156
        unset($this->headers['X-Forwarded-For'], $this->headers['X-Forwarded-Proto']);
157
        unset($this->headersMap['x-forwarded-for'], $this->headersMap['x-forwarded-proto']);
158
        if (isset($server['HTTP_IF_NONE_MATCH'])) {
159
            // ETag workaround for various broken Apache2 versions
160
            $this->headers['ETag']    = str_replace('-gzip', '', $server['HTTP_IF_NONE_MATCH']);
161
            $this->headersMap['etag'] = 'ETag';
162
        }
163
        if (isset($server['CONTENT_TYPE'])) {
164
            $this->headers['Content-Type']    = strtolower($server['CONTENT_TYPE']);
165
            $this->headersMap['content-type'] = 'Content-Type';
166
        }
167
        $this->setHost();
168
    }
169
170
    protected function extractServerData(array $server): void
171
    {
172
        $this->protocolVersion = str_ireplace('HTTP/', '', $server['SERVER_PROTOCOL'] ?? $this->protocolVersion);
0 ignored issues
show
Documentation Bug introduced by
It seems like str_ireplace('HTTP/', ''...$this->protocolVersion) can also be of type array. However, the property $protocolVersion is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
173
        $this->serverSoftware  = $server['SERVER_SOFTWARE'] ?? '';
174
        $this->queryParams     = $_GET;
175
        $this->cookieParams    = $_COOKIE;
176
        if (false === $this->isSafeMethod()) {
177
            $this->parseInput();
178
        }
179
        if ($_FILES) {
180
            $this->uploadedFiles = $this->parseUploadedFiles($_FILES);
181
        }
182
    }
183
184
    /**
185
     * Per recommendation:
186
     *
187
     * @return bool If the request Content-Type is either
188
     * application/x-www-form-urlencoded or multipart/form-data
189
     * and the request method is POST,
190
     * then it MUST return the contents of $_POST
191
     * @see ServerRequestInterface::withParsedBody()
192
     *
193
     * @see ServerRequestInterface::getParsedBody()
194
     */
195
    protected function useOnlyPost(): bool
196
    {
197
        if (empty($contentType = $this->getHeaderLine('Content-Type'))) {
198
            return false;
199
        }
200
        return $this->method === self::POST && (
201
                false !== strpos('application/x-www-form-urlencoded', $contentType) ||
202
                false !== strpos('multipart/form-data', $contentType));
203
    }
204
205
    /**
206
     * Try to unserialize a JSON string or form encoded request body.
207
     * Very useful if JavaScript app stringify objects in AJAX requests.
208
     */
209
    protected function parseInput(): void
210
    {
211
        if (empty($input = $this->getRawInput())) {
212
            return;
213
        }
214
        // Try JSON deserialization
215
        $this->parsedBody = json_decode($input, true, 512, JSON_BIGINT_AS_STRING);
216
        if (null === $this->parsedBody) {
217
            parse_str($input, $this->parsedBody);
218
        }
219
    }
220
221
    protected function getRawInput(): string
222
    {
223
        return file_get_contents('php://input') ?: '';
224
    }
225
}
226