Request::normalizeQueryString()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
1
<?php
2
/*
3
 * This file is part of the Borobudur-Http package.
4
 *
5
 * (c) Hexacodelabs <http://hexacodelabs.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Borobudur\Http;
12
13
use Borobudur\Http\Header\Accept\AbstractAcceptHeader;
14
use Borobudur\Http\Header\CacheControlHeader;
15
use Borobudur\Http\Session;
16
use Borobudur\Http\Session\Storage\SaveHandler\NativeSessionSaveHandler;
17
use SessionHandlerInterface;
18
19
/**
20
 * @author      Iqbal Maulana <[email protected]>
21
 * @created     7/19/15
22
 */
23
class Request extends AbstractRequest
24
{
25
    /**
26
     * @const string
27
     */
28
    const HTTP_METHOD_GET = 'GET';
29
    const HTTP_METHOD_POST = 'POST';
30
    const HTTP_METHOD_PATCH = 'PATCH';
31
    const HTTP_METHOD_PUT = 'PUT';
32
    const HTTP_METHOD_DELETE = 'DELETE';
33
    const HTTP_METHOD_OPTIONS = 'OPTIONS';
34
    const HTTP_METHOD_HEAD = 'HEAD';
35
    const HTTP_METHOD_CONNECT = 'CONNECT';
36
    const HTTP_METHOD_TRACE = 'TRACE';
37
    const HTTP_METHOD_PURGE = 'PURGE';
38
39
    /**
40
     * HTTP request method (GET is default).
41
     *
42
     * @var string
43
     */
44
    protected $method = Request::HTTP_METHOD_GET;
45
46
    /**
47
     * @var array
48
     */
49
    private $charsets;
50
51
    /**
52
     * @var array
53
     */
54
    private $encodings;
55
56
    /**
57
     * @var array
58
     */
59
    private $languages;
60
61
    /**
62
     * @var array
63
     */
64
    private $eTags;
65
66
    /**
67
     * Constructor.
68
     *
69
     * @param array                        $server
70
     * @param array                        $query
71
     * @param array                        $request
72
     * @param array                        $cookies
73
     * @param array                        $files
74
     * @param SessionHandlerInterface|null $sessionHandler
75
     * @param string|null                  $content
76
     */
77
    public function __construct(
78
        array $server = array(),
79
        array $query = array(),
80
        array $request = array(),
81
        array $cookies = array(),
82
        array $files = array(),
83
        SessionHandlerInterface $sessionHandler = null,
84
        $content = null
85
    ) {
86
        $this->server = new ServerBag($server);
87
        $this->query = new ParameterBag($query);
88
        $this->request = new ParameterBag($request);
89
        $this->cookies = new ParameterBag($cookies);
90
        $this->headers = HeaderBag::fromServer($this->server->getHeaders());
91
        $this->schemaHost = new SchemaHost($this->server);
92
        $this->uri = new Uri($this->server);
93
        $this->url = new Url($this->uri, $this->schemaHost, $this->getQueryString());
94
        $this->files = new UploadFileBag($files);
95
        $this->session = new Session($sessionHandler ?: new NativeSessionSaveHandler());
96
        $this->content = $content;
97
    }
98
99
    /**
100
     * Factory create Request.
101
     *
102
     * @param string $uri
103
     * @param string $method
104
     * @param array  $parameters
105
     * @param array  $cookies
106
     * @param array  $server
107
     * @param array  $files
108
     * @param string $content
109
     *
110
     * @return $this
111
     */
112
    public static function create(
113
        $uri,
114
        $method = Request::HTTP_METHOD_GET,
115
        array $parameters = array(),
116
        array $cookies = array(),
117
        array $server = array(),
118
        array $files = array(),
119
        $content = null
120
    ) {
121
        $server = array_replace(array(
122
            'SERVER_PROTOCOL'      => 'HTTP/1.1',
123
            'SERVER_NAME'          => 'localhost',
124
            'SERVER_PORT'          => 80,
125
            'REQUEST_URI'          => '/',
126
            'SCRIPT_NAME'          => '',
127
            'SCRIPT_FILENAME'      => '',
128
            'HTTP_HOST'            => 'localhost',
129
            'HTTP_ACCEPT'          => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
130
            'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8',
131
            'HTTP_ACCEPT_CHARSET'  => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
132
            'HTTP_USER_AGENT'      => 'Borobudur/0.X',
133
            'REQUEST_TIME'         => time(),
134
        ), $server);
135
136
        $server['PATH_INFO'] = '';
137
        $server['REQUEST_METHOD'] = strtoupper($method);
138
139
        $components = array_merge(array('path' => '/'), parse_url($uri));
140
        self::fixServerComponent($server, $components);
141
142
        switch ($server['REQUEST_METHOD']) {
143
            case Request::HTTP_METHOD_POST:
144
                // break intentionally omitted
145
            case Request::HTTP_METHOD_PUT:
146
                // break intentionally omitted
147
            case Request::HTTP_METHOD_DELETE:
148
                if (!isset($server['CONTENT_TYPE'])) {
149
                    $server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
150
                }
151
            // break intentionally omitted
152
            case Request::HTTP_METHOD_PATCH:
153
                // $parameters as $_POST parameters.
154
                // disable $_GET parameters.
155
                $request = $parameters;
156
                $query = array();
157
                break;
158
            default:
159
                // $parameters as $_GET parameters.
160
                // disabled $_POST parameters.
161
                $request = array();
162
                $query = $parameters;
163
        }
164
165
        $queryString = '';
166
        if (isset($components['query'])) {
167
            $queryString = $components['query'];
168
        }
169
170
        $query = self::fixQueryString($server, $query, $queryString, $components['path']);
171
172
        return new static($server, $query, $request, $cookies, $files, null, $content);
173
    }
174
175
    /**
176
     * Factory create Request from $_SERVER variables.
177
     *
178
     * @return Request
179
     */
180
    public static function createFromServer()
181
    {
182
        $server = $_SERVER;
183
        if ('cli-server' === php_sapi_name()) {
184
            if (array_key_exists('HTTP_CONTENT_LENGTH', $_SERVER)) {
185
                $server['CONTENT_LENGTH'] = $_SERVER['HTTP_CONTENT_LENGTH'];
186
            }
187
            if (array_key_exists('HTTP_CONTENT_TYPE', $_SERVER)) {
188
                $server['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE'];
189
            }
190
        }
191
192
        return self::fixCreateServerRequest($server);
193
    }
194
195
    /**
196
     * Normalize query string.
197
     *
198
     * @param string $queryString
199
     *
200
     * @return string
201
     */
202
    public static function normalizeQueryString($queryString)
203
    {
204
        $queryString = trim((string) $queryString);
205
        if (empty($queryString)) {
206
            return '';
207
        }
208
209
        list($parts, $order) = self::computePairValue($queryString);
210
        array_multisort($order, SORT_ASC, $parts);
211
212
        return implode('&', $parts);
213
    }
214
215
    /**
216
     * Pair value.
217
     *
218
     * @param string $queryString
219
     *
220
     * @return array
221
     */
222
    private static function computePairValue($queryString)
223
    {
224
        $parts = array();
225
        $order = array();
226
227
        foreach (explode('&', $queryString) as $param) {
228
            $param = trim($param);
229
            if (true === empty($param) || '=' === $param[0]) {
230
                continue;
231
            }
232
233
            $keyValuePair = explode('=', $param, 2);
234
            $order[] = urldecode($keyValuePair[0]);
235
236
            if (isset($keyValuePair[1])) {
237
                $parts[] = sprintf(
238
                    '%s=%s',
239
                    rawurlencode(urldecode($keyValuePair[0])),
240
                    rawurlencode(urldecode($keyValuePair[1]))
241
                );
242
                continue;
243
            }
244
245
            $parts[] = rawurlencode(urldecode($keyValuePair[0]));
246
        }
247
248
        return array($parts, $order);
249
    }
250
251
    /**
252
     * Create fixed server request
253
     *
254
     * @param array $server
255
     *
256
     * @return $this
257
     */
258
    private static function fixCreateServerRequest(array &$server)
259
    {
260
        $request = new self($server, $_GET, $_POST, $_COOKIE, $_FILES);
261
262
        // parse parameter from body content from method PUT, DELETE and PATCH.
263
        if ($request->getHeaderBag()->has('Content-Type')
264
            && 0 === strpos(
265
                $request->getHeaderBag()->first('Content-Type')->getFieldValue(),
266
                'application/x-www-form-urlencoded'
267
            )
268
            && in_array(
269
                strtoupper($request->getServerBag()->get('REQUEST_METHOD', Request::HTTP_METHOD_GET)),
270
                array(Request::HTTP_METHOD_PUT, Request::HTTP_METHOD_DELETE, Request::HTTP_METHOD_PATCH)
271
            )
272
        ) {
273
            parse_str($request->getContent(), $data);
274
            $request->request = new ParameterBag($data ?: array());
275
        }
276
277
        return $request;
278
    }
279
280
    /**
281
     * Fixing server component.
282
     *
283
     * @param array $server
284
     * @param array $components
285
     */
286
    private static function fixServerComponent(array &$server, array $components)
287
    {
288
        if (isset($components['host'])) {
289
            $server['SERVER_NAME'] = $server['HTTP_HOST'] = $components['host'];
290
        }
291
292
        if (isset($components['scheme'])) {
293
            self::fixSchema($server, $components['scheme']);
294
        }
295
296
        if (isset($components['port'])) {
297
            $server['SERVER_PORT'] = $components['port'];
298
            $server['HTTP_HOST'] = sprintf('%s:%s', $server['HTTP_HOST'], $server['SERVER_PORT']);
299
        }
300
    }
301
302
    /**
303
     * Fix schema.
304
     *
305
     * @param array  $server
306
     * @param string $schema
307
     */
308
    private static function fixSchema(array &$server, $schema)
309
    {
310
        if ($schema) {
311
            if ('http' === $schema) {
312
                unset($server['HTTPS']);
313
                $server['SERVER_PORT'] = 80;
314
            } elseif ('https' === $schema) {
315
                $server['HTTPS'] = 'on';
316
                $server['SERVER_PORT'] = 443;
317
            }
318
        }
319
    }
320
321
    /**
322
     * Fix server query string
323
     *
324
     * @param array $server
325
     * @param array $query
326
     *
327
     * @return array
328
     */
329
    private static function fixQueryString(array &$server, array $query, $queryString, $path)
330
    {
331
        $str = '';
332
        if ($queryString) {
333
            parse_str(html_entity_decode($queryString), $queryParts);
334
335
            if (!empty($query)) {
336
                $query = array_merge($query, $queryParts);
337
            } else {
338
                $query = $queryParts ?: array();
339
            }
340
        }
341
342
        if (!empty($query)) {
343
            $str = http_build_query($query, '', '&');
344
        }
345
346
        $server['REQUEST_URI'] = $path . (!empty($str) ? sprintf('?%s', $str) : '');
347
        $server['QUERY_STRING'] = $str;
348
349
        return $query;
350
    }
351
352
    /**
353
     * Get parameter value or return default value if not exist.
354
     *
355
     * Order of precedence: GET, POST
356
     *
357
     * @param string $key
358
     * @param mixed  $default
359
     *
360
     * @return mixed
361
     */
362
    public function get($key, $default = null)
363
    {
364
        if ($this->query->has($key)) {
365
            return $this->query->get($key, $default);
366
        }
367
368
        if ($this->request->has($key)) {
369
            return $this->request->get($key, $default);
370
        }
371
372
        return $default;
373
    }
374
375
    /**
376
     * Check if parameter defined by key.
377
     *
378
     * @param string $key
379
     *
380
     * @return bool
381
     */
382
    public function has($key)
383
    {
384
        if ($this->query->has($key)) {
385
            return true;
386
        }
387
388
        if ($this->request->has($key)) {
389
            return true;
390
        }
391
392
        return false;
393
    }
394
395
    /**
396
     * Get all parameters.
397
     *
398
     * @return array
399
     */
400
    public function all()
401
    {
402
        return array_merge($this->request->all(), $this->query->all());
403
    }
404
405
    /**
406
     * Get query string.
407
     *
408
     * @return string|null
409
     */
410
    public function getQueryString()
411
    {
412
        $queryString = static::normalizeQueryString($this->server->get('QUERY_STRING'));
413
414
        return false === empty($queryString) ? $queryString : null;
415
    }
416
417
    /**
418
     * Get list of charsets that accepted by client browser.
419
     *
420
     * @return array
421
     */
422
    public function getCharsets()
423
    {
424
        if (null === $this->charsets) {
425
            $this->charsets = $this->getAcceptedHeaderValues('Accept-Charset');
426
        }
427
428
        return $this->charsets;
429
    }
430
431
    /**
432
     * Get list of encodings that accepted by client browser.
433
     *
434
     * @return array
435
     */
436
    public function getEncodings()
437
    {
438
        if (null === $this->encodings) {
439
            $this->encodings = $this->getAcceptedHeaderValues('Accept-Encoding');
440
        }
441
442
        return $this->encodings;
443
    }
444
445
    /**
446
     * Get list of languages that accepted by client browser.
447
     *
448
     * @return array
449
     */
450
    public function getLanguages()
451
    {
452
        if (null === $this->languages) {
453
            $this->languages = $this->getAcceptedHeaderValues('Accept-Language');
454
        }
455
456
        return $this->languages;
457
    }
458
459
    /**
460
     * Get ETags.
461
     *
462
     * @return array
463
     */
464
    public function getETags()
465
    {
466
        if (null === $this->eTags) {
467
            $eTags = array();
468
            if ($this->headers->has('ETag')) {
469
                $header = $this->headers->first('ETag');
470
                $eTags = array_map('trim', explode(',', $header->getFieldValue()));
471
                $eTags = array_filter($eTags);
472
            }
473
474
            $this->eTags = $eTags;
475
        }
476
477
        return $this->eTags;
478
    }
479
480
    /**
481
     * Get client ip addresses.
482
     *
483
     * @return array
484
     */
485
    public function getClientIps()
486
    {
487
        $ips = array();
488
        foreach (
489
            array(
490
                'HTTP_CLIENT_IP',
491
                'HTTP_X_FORWARDED_FOR',
492
                'HTTP_X_FORWARDED',
493
                'HTTP_FORWARDED_FOR',
494
                'HTTP_FORWARDED',
495
                'REMOTE_ADDR',
496
            ) as $ip
497
        ) {
498
            if ($this->server->has($ip)) {
499
                $ips[] = $this->server->get($ip);
500
            }
501
        }
502
503
        return array_reverse($ips);
504
    }
505
506
    /**
507
     * Get client ip address.
508
     *
509
     * @return string
510
     */
511
    public function getClientIp()
512
    {
513
        $ips = $this->getClientIps();
514
515
        return count($ips) ? $ips[0] : null;
516
    }
517
518
    /**
519
     * Check request is no cache.
520
     *
521
     * @return bool
522
     */
523
    public function isNoCache()
524
    {
525
        if ($this->headers->has('Cache-Control')) {
526
            /**
527
             * @var CacheControlHeader $cacheControlHeader
528
             */
529
            $cacheControlHeader = $this->headers->first('Cache-Control');
530
            if ($cacheControlHeader->isNoCache()) {
531
                return true;
532
            }
533
        }
534
535
        return $this->headers->has('Pragma') && 'no-cache' === $this->headers->first('Pragma')->getFieldValue();
536
    }
537
538
    /**
539
     * Cast Request to string representation.
540
     *
541
     * @return string
542
     */
543
    public function __toString()
544
    {
545
        return
546
            sprintf(
547
                '%s %s %s' . "\r\n",
548
                $this->getMethod(),
549
                $this->uri->getRequestUri(),
550
                $this->server->get('SERVER_PROTOCOL')
551
            ) .
552
            $this->headers . "\r\n" .
553
            $this->getContent();
554
    }
555
556
    /**
557
     * Get accept header values by header name.
558
     *
559
     * @param string $headerName
560
     *
561
     * @return array
562
     */
563
    private function getAcceptedHeaderValues($headerName)
564
    {
565
        $values = array();
566
        if ($this->headers->has($headerName)) {
567
            /**
568
             * @var AbstractAcceptHeader $header
569
             */
570
            $header = $this->headers->first($headerName);
571
            foreach ($header->all() as $item) {
572
                $values[] = $item->getValue();
573
            }
574
        }
575
576
        return $values;
577
    }
578
}
579