Issues (48)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Request.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2014 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden;
9
10
use JsonSerializable;
11
12
/**
13
 * A class that contains the information in an http request.
14
 */
15
class Request implements JsonSerializable {
16
17
    /// Constants ///
18
    const METHOD_HEAD = 'HEAD';
19
    const METHOD_GET = 'GET';
20
    const METHOD_POST = 'POST';
21
    const METHOD_PUT = 'PUT';
22
    const METHOD_PATCH = 'PATCH';
23
    const METHOD_DELETE = 'DELETE';
24
    const METHOD_OPTIONS = 'OPTIONS';
25
26
    /// Properties ///
27
28
    /**
29
     *
30
     * @var array The data in this request.
31
     */
32
    protected $env;
33
34
    /**
35
     * @var Request The currently dispatched request.
36
     */
37
    protected static $current;
38
39
    /**
40
     * @var array The default environment for constructed requests.
41
     */
42
    protected static $defaultEnv;
43
44
    protected static $knownExtensions = [
45
        '.html' => 'text/html',
46
        '.json' => 'application/json',
47
        '.txt' => 'text/plain',
48
        '.xml' => 'application/xml'
49
    ];
50
51
    /**
52
     * @var array The global environment for the request.
53
     */
54
    protected static $globalEnv;
55
56
    /**
57
     * Special-case HTTP headers that are otherwise unidentifiable as HTTP headers.
58
     * Typically, HTTP headers in the $_SERVER array will be prefixed with
59
     * `HTTP_` or `X_`. These are not so we list them here for later reference.
60
     *
61
     * @var array
62
     */
63
    protected static $specialHeaders = array(
64
        'CONTENT_TYPE',
65
        'CONTENT_LENGTH',
66
        'PHP_AUTH_USER',
67
        'PHP_AUTH_PW',
68
        'PHP_AUTH_DIGEST',
69
        'AUTH_TYPE'
70
    );
71
72
    /// Methods ///
73
74
    /**
75
     * Initialize a new instance of the {@link Request} class.
76
     *
77
     * @param string $url The url of the request or blank to use the current environment.
78
     * @param string $method The request method.
79
     * @param mixed $data The request data. This is the query string for GET requests or the body for other requests.
80
     */
81 79
    public function __construct($url = '', $method = '', $data = null) {
82 79
        if ($url) {
83 70
            $this->env = (array)static::defaultEnvironment();
84
            // Instantiate the request from the url.
85 70
            $this->setUrl($url);
86 70
            if ($method) {
87 56
                $this->setMethod($method);
88 56
            }
89 70
            if (is_array($data)) {
90 2
                $this->setData($data);
91 2
            }
92 70
        } else {
93
            // Instantiate the request from the global environment.
94 9
            $this->env = static::globalEnvironment();
0 ignored issues
show
Documentation Bug introduced by
It seems like static::globalEnvironment() can also be of type string. However, the property $env is declared as type array. 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...
95 9
            if ($method) {
96
                $this->setMethod($method);
97
            }
98 9
            if (is_array($data)) {
99
                $this->setData($data);
100
            }
101
        }
102
103 79
        static::overrideEnvironment($this->env);
104 79
    }
105
106
    /**
107
     * Convert a request to a string.
108
     *
109
     * @return string Returns the url of the request.
110
     */
111 1
    public function __toString() {
112 1
        return $this->getUrl();
113
    }
114
115
    /**
116
     * Gets or sets the current request.
117
     *
118
     * @param Request $request Pass a request object to set the current request.
119
     * @return Request Returns the current request if {@link Request} is null or the previous request otherwise.
120
     */
121 44
    public static function current(Request $request = null) {
122 44
        if ($request !== null) {
123 44
            $bak = self::$current;
124 44
            self::$current = $request;
125 44
            return $bak;
126
        }
127 1
        return self::$current;
128
    }
129
130
    /**
131
     * Gets or updates the default environment.
132
     *
133
     * @param string|array|null $key Specifies a specific key in the environment array.
134
     * If you pass an array for this parameter then you can set the default environment.
135
     * @param bool $merge Whether or not to merge the new value.
136
     * @return array|mixed Returns the value at {@link $key} or the entire environment array.
137
     * @throws \InvalidArgumentException Throws an exception when {@link $key} is not valid.
138
     */
139 71
    public static function defaultEnvironment($key = null, $merge = false) {
140 71
        if (self::$defaultEnv === null) {
141 1
            self::$defaultEnv = array(
142 1
                'REQUEST_METHOD' => 'GET',
143 1
                'X_REWRITE' => true,
144 1
                'SCRIPT_NAME' => '',
145 1
                'PATH_INFO' => '/',
146 1
                'EXT' => '',
147 1
                'QUERY' => [],
148 1
                'SERVER_NAME' => 'localhost',
149 1
                'SERVER_PORT' => 80,
150 1
                'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
151 1
                'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8',
152 1
                'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
153 1
                'HTTP_USER_AGENT' => 'Garden/0.1 (Howdy stranger)',
154 1
                'REMOTE_ADDR' => '127.0.0.1',
155 1
                'URL_SCHEME' => 'http',
156 1
                'INPUT' => [],
157 1
            );
158 1
        }
159
160 71
        if ($key === null) {
161 71
            return self::$defaultEnv;
162 45
        } elseif (is_array($key)) {
163 44
            if ($merge) {
164 44
                self::$defaultEnv = array_merge(self::$defaultEnv, $key);
165 44
            } else {
166
                self::$defaultEnv = $key;
167
            }
168 44
            return self::$defaultEnv;
169 1
        } elseif (is_string($key)) {
170 1
            return val($key, self::$defaultEnv);
171
        } else {
172
            throw new \InvalidArgumentException("Argument #1 for Request::globalEnvironment() is invalid.", 422);
173
        }
174
    }
175
176
    /**
177
     * Parse request information from the current environment.
178
     *
179
     * The environment contains keys based on the Rack protocol (see http://rack.rubyforge.org/doc/SPEC.html).
180
     *
181
     * @param mixed $key The environment variable to look at.
182
     * - null: Return the entire environment.
183
     * - true: Force a re-parse of the environment and return the entire environment.
184
     * - string: One of the environment variables.
185
     * @return array|string Returns the global environment or the value at {@link $key}.
186
     */
187 9
    public static function globalEnvironment($key = null) {
188
        // Check to parse the environment.
189 9
        if ($key === true || !isset(self::$globalEnv)) {
190 1
            self::$globalEnv = static::parseServerVariables();
191 1
        }
192
193 9
        if ($key) {
194
            return val($key, self::$globalEnv);
195
        }
196 9
        return self::$globalEnv;
197
    }
198
199
    /**
200
     * Parse the various server variables to build the global environment.
201
     *
202
     * @return array Returns an array suitable to be used as the {@see Request::$globalEnv}.
203
     * @see Request::globalEnvironment().
204
     */
205 1
    protected static function parseServerVariables() {
206 1
        $env = static::defaultEnvironment();
207
208
        // REQUEST_METHOD.
209 1
        $env['REQUEST_METHOD'] = val('REQUEST_METHOD', $_SERVER) ?: 'CONSOLE';
210
211
        // SCRIPT_NAME: This is the root directory of the application.
212 1
        $script_name = rtrim_substr($_SERVER['SCRIPT_NAME'], 'index.php');
213 1
        $env['SCRIPT_NAME'] = rtrim($script_name, '/');
214
215
        // PATH_INFO.
216 1
        $path = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
217
218
        // Strip the extension from the path.
219 1
        list($path, $ext) = static::splitPathExt($path);
220 1
        $env['PATH_INFO'] = '/' . ltrim($path, '/');
221 1
        $env['EXT'] = $ext;
222
223
        // QUERY.
224 1
        $get = $_GET;
225 1
        $env['QUERY'] = $get;
226
227
        // SERVER_NAME.
228 1
        $host = array_select(
229 1
            ['HTTP_X_FORWARDED_HOST', 'HTTP_HOST', 'SERVER_NAME'],
230
            $_SERVER
231 1
        );
232 1
        list($host) = explode(':', $host, 2);
233 1
        $env['SERVER_NAME'] = $host;
234
235
        // HTTP_* headers.
236 1
        $env = array_replace($env, static::extractHeaders($_SERVER));
237
238
        // URL_SCHEME.
239 1
        $url_scheme = 'http';
240
        // Web server-originated SSL.
241 1
        if (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on') {
242
            $url_scheme = 'https';
243
        }
244 1
        $url_scheme = array_select([
245 1
            'HTTP_X_ORIGINALLY_FORWARDED_PROTO', // varnish modifies the scheme
246
            'HTTP_X_FORWARDED_PROTO' // load balancer-originated (and terminated) ssl
247 1
        ], $_SERVER, $url_scheme);
248 1
        $env['URL_SCHEME'] = $url_scheme;
249
250
        // SERVER_PORT.
251 1
        $server_port = (int)val('SERVER_PORT', $_SERVER, $url_scheme === 'https' ? 443 :80);
252 1
        $env['SERVER_PORT'] = $server_port;
253
254
        // INPUT: The entire input.
255
        // Input stream (readable one time only; not available for multipart/form-data requests)
256 1
        switch (val('CONTENT_TYPE', $env)) {
257 1
            case 'application/json':
258
                $input_raw = @file_get_contents('php://input');
259
                $input = @json_decode($input_raw, true);
260
                break;
261 1
            default:
262 1
                $input = $_POST;
263 1
                $input_raw = null;
264 1
                break;
265 1
        }
266 1
        $env['INPUT'] = $input;
267 1
        $env['INPUT_RAW'] = $input_raw;
268
269
        // IP Address.
270
        // Load balancers set a different ip address.
271 1
        $ip = array_select(
272 1
            ['HTTP_X_ORIGINALLY_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'],
273 1
            $_SERVER,
274
            '127.0.0.1'
275 1
        );
276 1
        $env['REMOTE_ADDR'] = force_ipv4($ip);
277 1
        return $env;
278
    }
279
280
    /**
281
     * Check for specific environment overrides.
282
     *
283
     * @param array &$env The environment to override.
284
     */
285 79
    protected static function overrideEnvironment(&$env) {
286 79
        $get =& $env['QUERY'];
287
288
        // Check to override the method.
289 79
        if (isset($get['x-method'])) {
290 7
            $method = strtoupper($get['x-method']);
291
292 7
            $getMethods = array(self::METHOD_GET, self::METHOD_HEAD, self::METHOD_OPTIONS);
293
294
            // Don't allow get style methods to be overridden to post style methods.
295 7
            if (!in_array($env['REQUEST_METHOD'], $getMethods) || in_array($method, $getMethods)) {
296 7
                static::replaceEnv($env, 'REQUEST_METHOD', $method);
297 7
            } else {
298 4
                $env['X_METHOD_BLOCKED'] = true;
299
            }
300 7
            unset($get['x-method']);
301 7
        }
302
303
        // Force the path and extension to lowercase.
304 79
        $path = strtolower($env['PATH_INFO']);
305 79
        if ($path !== $env['PATH_INFO']) {
306 1
            static::replaceEnv($env, 'PATH_INFO', $path);
307 1
        }
308
309 79
        $ext = strtolower($env['EXT']);
310 79
        if ($ext !== $env['EXT']) {
311
            static::replaceEnv($env, 'EXT', $ext);
312
        }
313
314
        // Check to override the accepts header.
315 79
        if (isset(self::$knownExtensions[$ext]) && $env['HTTP_ACCEPT'] !== 'application/internal') {
316 7
            static::replaceEnv($env, 'HTTP_ACCEPT', self::$knownExtensions[$ext]);
317 7
        }
318 79
    }
319
320
    /**
321
     * Get a value from the environment or the entire environment.
322
     *
323
     * @param string|null $key The key to get or null to get the entire environment.
324
     * @param mixed $default The default value if {@link $key} is not found.
325
     * @return mixed|array Returns the value at {@link $key}, {$link $default} or the entire environment array.
326
     * @see Request::setEnv()
327
     */
328 51
    public function getEnv($key = null, $default = null) {
329 51
        if ($key === null) {
330
            return $this->env;
331
        }
332 51
        return val(strtoupper($key), $this->env, $default);
333
    }
334
335
    /**
336
     * Set a value from the environment or the entire environment.
337
     *
338
     * @param string|array $key The key to set or an array to set the entire environment.
339
     * @param mixed $value The value to set.
340
     * @return Request Returns $this for fluent calls.
341
     * @throws \InvalidArgumentException Throws an exception when {@link $key} is invalid.
342
     * @see Request::getEnv()
343
     */
344 1
    public function setEnv($key, $value = null) {
345 1
        if (is_string($key)) {
346
            $this->env[strtoupper($key)] = $value;
347 1
        } elseif (is_array($key)) {
348 1
            $this->env = $key;
349 1
        } else {
350
            throw new \InvalidArgumentException("Argument 1 must be either a string or array.", 422);
351
        }
352 1
        return $this;
353
    }
354
355
    /**
356
     * Replace an environment variable with another one and back up the old one in a *_RAW key.
357
     *
358
     * @param array &$env The environment array.
359
     * @param string $key The environment key.
360
     * @param mixed $value The new environment value.
361
     * @return mixed Returns the old value or null if there was no old value.
362
     */
363 15
    public static function replaceEnv(&$env, $key, $value) {
364 15
        $key = strtoupper($key);
365
366 15
        $result = null;
367 15
        if (isset($env[$key])) {
368 15
            $result = $env[$key];
369 15
            $env[$key.'_RAW'] = $result;
370 15
        }
371 15
        $env[$key] = $value;
372 15
        return $result;
373
    }
374
375
    /**
376
     * Restore an environment variable that was replaced with {@link Request::replaceEnv()}.
377
     *
378
     * @param array &$env The environment array.
379
     * @param string $key The environment key.
380
     * @return mixed Returns the current environment value.
381
     */
382
    public static function restoreEnv(&$env, $key) {
383
        $key = strtoupper($key);
384
385
        if (isset($env[$key.'_RAW'])) {
386
            $env[$key] = $env[$key.'_RAW'];
387
            unset($env[$key.'_RAW']);
388
            return $env[$key];
389
        } elseif (isset($env[$key])) {
390
            return $env[$key];
391
        }
392
        return null;
393
    }
394
395
    /**
396
     * Extract the headers from an array such as $_SERVER or the request's own $env.
397
     *
398
     * @param array $arr The array to extract.
399
     * @return array The extracted headers.
400
     */
401 1
    public static function extractHeaders($arr) {
402 1
        $result = [];
403
404 1
        foreach ($arr as $key => $value) {
405 1
            $key = strtoupper($key);
406 1
            if (strpos($key, 'X_') === 0 || strpos($key, 'HTTP_') === 0 || in_array($key, static::$specialHeaders)) {
407
                if ($key === 'HTTP_CONTENT_TYPE' || $key === 'HTTP_CONTENT_LENGTH') {
408
                    continue;
409
                }
410
                $result[$key] = $value;
411
            }
412 1
        }
413 1
        return $result;
414
    }
415
416
    /**
417
     * Retrieves all message headers.
418
     *
419
     * @return array Returns an associative array of the message's headers.
420
     * Each key represents a header name, and each value is an array of strings.
421
     */
422
    public function getHeaders() {
423
        $result = [];
424
425
        foreach ($this->env as $key => $value) {
426
            if (stripos($key, 'HTTP_') === 0 && !str_ends($key, '_RAW')) {
427
                $headerKey = static::normalizeHeaderName(substr($key, 5));
428
                $result[$headerKey][] = $value;
429
            }
430
        }
431
        return $result;
432
    }
433
434
    /**
435
     * Get the hostname of the request.
436
     *
437
     * @return string Returns the host.
438
     */
439 4
    public function getHost() {
440 4
        return (string)$this->env['SERVER_NAME'];
441
    }
442
443
    /**
444
     * Set the hostname of the request.
445
     *
446
     * @param string $host The hostname.
447
     * @return Request Returns $this for fluent calls.
448
     */
449 18
    public function setHost($host) {
450 18
        $this->env['SERVER_NAME'] = $host;
451 18
        return $this;
452
    }
453
454
    /**
455
     * Get the host and port, but only if the port is not the standard port for the request scheme.
456
     *
457
     * @return string Returns the host and port or just the host if this is the standard port.
458
     * @see Request::getHost()
459
     * @see Request::getPort()
460
     */
461 3
    public function getHostAndPort() {
462 3
        $host = $this->getHost();
463 3
        $port = $this->getPort();
464
465
        // Only append the port if it is non-standard.
466 3
        if (($port == 80 && $this->getScheme() === 'http') || ($port == 443 && $this->getScheme() === 'https')) {
467 1
            $port = '';
468 1
        } else {
469 2
            $port = ':'.$port;
470
        }
471 3
        return $host.$port;
472
    }
473
474
    /**
475
     * Get the ip address of the request.
476
     *
477
     * @return string Returns the current ip address.
478
     */
479 1
    public function getIP() {
480 1
        return (string)$this->env['REMOTE_ADDR'];
481
    }
482
483
    /**
484
     * Set the ip address of the request.
485
     *
486
     * @param string $ip The new ip address.
487
     * @return Request Returns $this for fluent calls.
488
     */
489 1
    public function setIP($ip) {
490 1
        $this->env['REMOTE_ADDR'] = $ip;
491 1
        return $this;
492
    }
493
494
    /**
495
     * Gets whether or not this is a DELETE request.
496
     *
497
     * @return bool Returns true if this is a DELETE request, false otherwise.
498
     */
499 1
    public function isDelete() {
500 1
        return $this->getMethod() === self::METHOD_DELETE;
501
    }
502
503
    /**
504
     * Gets whether or not this is a GET request.
505
     *
506
     * @return bool Returns true if this is a GET request, false otherwise.
507
     */
508 1
    public function isGet() {
509 1
        return $this->getMethod() === self::METHOD_GET;
510
    }
511
512
    /**
513
     * Gets whether or not this is a HEAD request.
514
     *
515
     * @return bool Returns true if this is a HEAD request, false otherwise.
516
     */
517 1
    public function isHead() {
518 1
        return $this->getMethod() === self::METHOD_HEAD;
519
    }
520
521
    /**
522
     * Gets whether or not this is an OPTIONS request.
523
     *
524
     * @return bool Returns true if this is an OPTIONS request, false otherwise.
525
     */
526 1
    public function isOptions() {
527 1
        return $this->getMethod() === self::METHOD_OPTIONS;
528
    }
529
530
    /**
531
     * Gets whether or not this is a PATCH request.
532
     *
533
     * @return bool Returns true if this is a PATCH request, false otherwise.
534
     */
535 1
    public function isPatch() {
536 1
        return $this->getMethod() === self::METHOD_PATCH;
537
    }
538
539
    /**
540
     * Gets whether or not this is a POST request.
541
     *
542
     * @return bool Returns true if this is a POST request, false otherwise.
543
     */
544 1
    public function isPost() {
545 1
        return $this->getMethod() === self::METHOD_POST;
546
    }
547
548
    /**
549
     * Gets whether or not this is a PUT request.
550
     *
551
     * @return bool Returns true if this is a PUT request, false otherwise.
552
     */
553 1
    public function isPut() {
554 1
        return $this->getMethod() === self::METHOD_PUT;
555
    }
556
557
    /**
558
     * Get the http method of the request.
559
     *
560
     * @return string Returns the http method of the request.
561
     */
562 61
    public function getMethod() {
563 61
        return (string)$this->env['REQUEST_METHOD'];
564
    }
565
566
    /**
567
     * Set the http method of the request.
568
     *
569
     * @param string $method The new request method.
570
     * @return Request Returns $this for fluent calls.
571
     */
572 57
    public function setMethod($method) {
573 57
        $this->env['REQUEST_METHOD'] = strtoupper($method);
574 57
        return $this;
575
    }
576
577
    /**
578
     * Gets the request path.
579
     *
580
     * Note that this returns the path without the file extension.
581
     * If you want the full path use {@link Request::getFullPath()}.
582
     *
583
     * @return string Returns the request path.
584
     */
585 48
    public function getPath() {
586 48
        return (string)$this->env['PATH_INFO'];
587
    }
588
589
    /**
590
     * Sets the request path.
591
     *
592
     * @param string $path The path to set.
593
     * @return Request Returns $this for fluent calls.
594
     */
595 1
    public function setPath($path) {
596 1
        $this->env['PATH_INFO'] = (string)$path;
597 1
        return $this;
598
    }
599
600
    /**
601
     * Get the file extension of the request.
602
     *
603
     * @return string Returns the file extension.
604
     */
605 2
    public function getExt() {
606 2
        return (string)$this->env['EXT'];
607
    }
608
609
    /**
610
     * Sets the file extension of the request.
611
     *
612
     * @param string $ext The file extension to set.
613
     * @return Request Returns $this for fluent calls.
614
     */
615 2
    public function setExt($ext) {
616 2
        if ($ext) {
617 1
            $this->env['EXT'] = '.'.ltrim($ext, '.');
618 1
        } else {
619 1
            $this->env['EXT'] = '';
620
        }
621 2
        return $this;
622
    }
623
624
    /**
625
     * Get the path and file extenstion.
626
     *
627
     * @return string Returns the path and file extension.
628
     * @see Request::setPathExt()
629
     */
630 5
    public function getPathExt() {
631 5
        return $this->env['PATH_INFO'].$this->env['EXT'];
632
    }
633
634
    /**
635
     * Set the path with file extension.
636
     *
637
     * @param string $path The path to set.
638
     * @return Request Returns $this for fluent calls.
639
     * @see Request::getPathExt()
640
     */
641 65
    public function setPathExt($path) {
642
        // Strip the extension from the path.
643 65
        if (substr($path, -1) !== '/' && ($pos = strrpos($path, '.')) !== false) {
644 9
            $ext = substr($path, $pos);
645 9
            $path = substr($path, 0, $pos);
646 9
            $this->env['EXT'] = $ext;
647 9
        } else {
648 58
            $this->env['EXT'] = '';
649
        }
650 65
        $this->env['PATH_INFO'] = $path;
651 65
        return $this;
652
    }
653
654
    /**
655
     * Get the full path of the request.
656
     *
657
     * The full path consists of the root + path + extension.
658
     *
659
     * @return string Returns the full path.
660
     */
661 3
    public function getFullPath() {
662 3
        return $this->getRoot().$this->getPathExt();
663
    }
664
665
    /**
666
     * Set the full path of the request.
667
     *
668
     * The full path consists of the root + path + extension.
669
     * This method examins the current root to see if the root can still be used or should be removed.
670
     * Special care must be taken when calling this method to make sure you don't remove the root unintentionally.
671
     *
672
     * @param string $fullPath The full path to set.
673
     * @return Request Returns $this for fluent calls.
674
     */
675 64
    public function setFullPath($fullPath) {
676 64
        $fullPath = '/'.ltrim($fullPath, '/');
677
678
        // Try stripping the root out of the path first.
679 64
        $root = (string)$this->getRoot();
680
681 64
        if ($root &&
682 64
            strpos($fullPath, $root) === 0 &&
683 64
            (strlen($fullPath) === strlen($root) || substr($fullPath, strlen($root), 1) === '/')) {
684
685 1
            $this->setPathExt(substr($fullPath, strlen($root)));
686 1
        } else {
687 64
            $this->setRoot('');
688 64
            $this->setPathExt($fullPath);
689
        }
690
691 64
        return $this;
692
    }
693
694
    /**
695
     * Normalize a header field name to follow the general HTTP header `Capital-Dash-Separated` convention.
696
     *
697
     * @param string $name The header name to normalize.
698
     * @return string Returns the normalized header name.
699
     */
700
    public static function normalizeHeaderName($name) {
701
        $result = str_replace(' ', '-', ucwords(str_replace(['-', '_'], ' ', strtolower($name))));
702
        return $result;
703
    }
704
705
    /**
706
     * Gets the port.
707
     *
708
     * @return int Returns the port.
709
     */
710 3
    public function getPort() {
711 3
        return (int)$this->env['SERVER_PORT'];
712
    }
713
714
    /**
715
     * Sets the port.
716
     *
717
     * Setting the port to 80 or 443 will also set the scheme to http or https respectively.
718
     *
719
     * @param int $port The port to set.
720
     * @return Request Returns $this for fluent calls.
721
     */
722 17
    public function setPort($port) {
723 17
        $this->env['SERVER_PORT'] = $port;
724
725
        // Override the scheme for standard ports.
726 17
        if ($port === 80) {
727 15
            $this->setScheme('http');
728 17
        } elseif ($port === 443) {
729 1
            $this->setScheme('https');
730 1
        }
731
732 17
        return $this;
733
    }
734
735
    /**
736
     * Get an item from the query string array.
737
     *
738
     * @param string|null $key Either a string key or null to get the entire array.
739
     * @param mixed|null $default The default to return if {@link $key} is not found.
740
     * @return string|array|null Returns the query string value or the query string itself.
741
     * @see Request::setQuery()
742
     */
743 47
    public function getQuery($key = null, $default = null) {
744 47
        if ($key === null) {
745 39
            return $this->env['QUERY'];
746
        }
747 10
        return isset($this->env['QUERY'][$key]) ? $this->env['QUERY'][$key] : $default;
748
    }
749
750
    /**
751
     * Set an item from the query string array.
752
     *
753
     * @param string|array $key Either a string key or an array to set the entire query string.
754
     * @param mixed|null $value The value to set.
755
     * @return Request Returns $this for fluent call.
756
     * @throws \InvalidArgumentException Throws an exception when {@link $key is invalid}.
757
     * @see Request::getQuery()
758
     */
759 14 View Code Duplication
    public function setQuery($key, $value = null) {
760 14
        if (is_string($key)) {
761
            $this->env['QUERY'][$key] = $value;
762 14
        } elseif (is_array($key)) {
763 13
            $this->env['QUERY'] = $key;
764 13
        } else {
765 1
            throw new \InvalidArgumentException("Argument 1 must be a string or array.", 422);
766
        }
767 13
        return $this;
768
    }
769
770
    /**
771
     * Get an item from the input array.
772
     *
773
     * @param string|null $key Either a string key or null to get the entire array.
774
     * @param mixed|null $default The default to return if {@link $key} is not found.
775
     * @return string|array|null Returns the query string value or the input array itself.
776
     */
777 4
    public function getInput($key = null, $default = null) {
778 4
        if ($key === null) {
779 4
            return $this->env['INPUT'];
780
        }
781 1
        return isset($this->env['INPUT'][$key]) ? $this->env['INPUT'][$key] : $default;
782
    }
783
784
    /**
785
     * Set an item from the input array.
786
     *
787
     * @param string|array $key Either a string key or an array to set the entire input.
788
     * @param mixed|null $value The value to set.
789
     * @return Request Returns $this for fluent call.
790
     * @throws \InvalidArgumentException Throws an exception when {@link $key is invalid}.
791
     */
792 4 View Code Duplication
    public function setInput($key, $value = null) {
793 4
        if (is_string($key)) {
794
            $this->env['INPUT'][$key] = $value;
795 4
        } elseif (is_array($key)) {
796 3
            $this->env['INPUT'] = $key;
797 3
        } else {
798 1
            throw new \InvalidArgumentException("Argument 1 must be a string or array.", 422);
799
        }
800 3
        return $this;
801
    }
802
803
    /**
804
     * Gets the query on input depending on the http method.
805
     *
806
     * @param string|null $key Either a string key or null to get the entire array.
807
     * @param mixed|null $default The default to return if {@link $key} is not found.
808
     * @return mixed|array Returns the value at {@link $key} or the entire array.
809
     * @see Request::setData()
810
     * @see Request::getInput()
811
     * @see Request::getQuery()
812
     * @see Request::hasInput()
813
     */
814 1
    public function getData($key = null, $default = null) {
815 1
        if ($this->hasInput()) {
816 1
            return $this->getInput($key, $default);
817
        } else {
818 1
            return $this->getQuery($key, $default);
819
        }
820
    }
821
822
    /**
823
     * Sets the query on input depending on the http method.
824
     *
825
     * @param string|array $key Either a string key or an array to set the entire data.
826
     * @param mixed|null $value The value to set.
827
     * @return Request Returns $this for fluent call.
828
     * @see Request::getData()
829
     * @see Request::setInput()
830
     * @see Request::setQuery()
831
     * @see Request::hasInput()
832
     */
833 2
    public function setData($key, $value = null) {
834 2
        if ($this->hasInput()) {
835 2
            $this->setInput($key, $value);
836 2
        } else {
837 1
            $this->setQuery($key, $value);
838
        }
839 2
        return $this;
840
    }
841
842
    /**
843
     * Returns true if an http method has input (a post body).
844
     *
845
     * @param string $method The http method to test.
846
     * @return bool Returns true if the http method has input, false otherwise.
847
     */
848 2
    public function hasInput($method = '') {
849 2
        if (!$method) {
850 2
            $method = $this->getMethod();
851 2
        }
852
853 2
        switch (strtoupper($method)) {
854 2
            case self::METHOD_GET:
855 2
            case self::METHOD_DELETE:
856 2
            case self::METHOD_HEAD:
857 2
            case self::METHOD_OPTIONS:
858 1
                return false;
859 2
        }
860 2
        return true;
861
    }
862
863
    /**
864
     * Get the root directory (SCRIPT_NAME) of the request.
865
     *
866
     * @return Returns the root directory of the request as a string.
867
     * @see Request::setRoot()
868
     */
869 65
    public function getRoot() {
870 65
        return (string)$this->env['SCRIPT_NAME'];
871
    }
872
873
    /**
874
     * Set the root directory (SCRIPT_NAME) of the request.
875
     *
876
     * This method will modify the set root to include a leading slash if it does not have one.
877
     * A root of just "/" will be coerced to an empty string.
878
     *
879
     * @param string $root The new root directory.
880
     * @return Request Returns $this for fluent calls.
881
     * @see Request::getRoot()
882
     */
883 65
    public function setRoot($root) {
884 65
        $value = trim($root, '/');
885 65
        if ($value) {
886 4
            $value = '/'.$value;
887 4
        }
888 65
        $this->env['SCRIPT_NAME'] = $value;
889 65
        return $this;
890
    }
891
892
    /**
893
     * Get the request scheme.
894
     *
895
     * The request scheme is usually http or https.
896
     *
897
     * @return string Retuns the scheme.
898
     * @see Request::setScheme()
899
     */
900 18
    public function getScheme() {
901 18
        return (string)$this->env['URL_SCHEME'];
902
    }
903
904
    /**
905
     * Set the request scheme.
906
     *
907
     * The request scheme is usually http or https.
908
     *
909
     * @param string $scheme The new scheme to set.
910
     * @return Request Returns $this for fluent calls.
911
     * @see Request::getScheme()
912
     */
913 18
    public function setScheme($scheme) {
914 18
        $this->env['URL_SCHEME'] = $scheme;
915 18
        return $this;
916
    }
917
918
    /**
919
     * Get the full url of the request.
920
     *
921
     * @return string Returns the full url of the request.
922
     * @see Request::setUrl()
923
     */
924 2
    public function getUrl() {
925 2
        $query = $this->getQuery();
926
        return
927 2
            $this->getScheme().
928 2
            '://'.
929 2
            $this->getHostAndPort().
930 2
            $this->getRoot().
931 2
            $this->getPath().
932 2
            (!empty($query) ? '?'.http_build_query($query) : '');
933
    }
934
935
    /**
936
     * Set the full url of the request.
937
     *
938
     * @param string $url The new url.
939
     * @return Request $this Returns $this for fluent calls.
940
     * @see Request::getUrl()
941
     */
942 70
    public function setUrl($url) {
943
        // Parse the url and set the individual components.
944 70
        $url_parts = parse_url($url);
945
946 70
        if (isset($url_parts['scheme'])) {
947 17
            $this->setScheme($url_parts['scheme']);
948 17
        }
949
950 70
        if (isset($url_parts['host'])) {
951 17
            $this->setHost($url_parts['host']);
952 17
        }
953
954 70
        if (isset($url_parts['port'])) {
955 2
            $this->setPort($url_parts['port']);
956 70
        } elseif (isset($url_parts['scheme'])) {
957 15
            $this->setPort($this->getScheme() === 'https' ? 443 : 80);
958 15
        }
959
960 70
        if (isset($url_parts['path'])) {
961 63
            $this->setFullPath($url_parts['path']);
962 63
        }
963
964 70
        if (isset($url_parts['query'])) {
965 11
            parse_str($url_parts['query'], $query);
966 11
            if (is_array($query)) {
967 11
                $this->setQuery($query);
968 11
            }
969 11
        }
970 70
        return $this;
971
    }
972
973
    /**
974
     * Construct a url on the current site.
975
     *
976
     * @param string $path The path of the url.
977
     * @param mixed $domain Whether or not to include the domain. This can be one of the following.
978
     * - false: The domain will not be included.
979
     * - true: The domain will be included.
980
     * - http: Force http.
981
     * - https: Force https.
982
     * - //: A schemeless domain will be included.
983
     * - /: Just the path will be returned.
984
     * @return string Returns the url.
985
     */
986 1
    public function makeUrl($path, $domain = false) {
987 1
        if (!$path) {
988
            $path = $this->getPath();
989
        }
990
991
        // Check for a specific scheme.
992 1
        $scheme = $this->getScheme();
993 1
        if ($domain === 'http' || $domain === 'https') {
994 1
            $scheme = $domain;
995 1
            $domain = true;
996 1
        }
997
998 1
        if ($domain === true) {
999 1
            $prefix = $scheme.'://'.$this->getHostAndPort().$this->getRoot();
1000 1
        } elseif ($domain === false) {
1001 1
            $prefix = $this->getRoot();
1002 1
        } elseif ($domain === '//') {
1003 1
            $prefix = '//'.$this->getHostAndPort().$this->getRoot();
1004 1
        } else {
1005 1
            $prefix = '';
1006
        }
1007
1008 1
        return $prefix.'/'.ltrim($path, '/');
1009
    }
1010
1011
    /**
1012
     * Split the file extension off a path.
1013
     *
1014
     * @param string $path The path to split.
1015
     * @return array Returns an array in the form `['path', 'ext']`.
1016
     */
1017 1
    protected static function splitPathExt($path) {
1018 1
        if (substr($path, -1) !== '/' && ($pos = strrpos($path, '.')) !== false) {
1019
            $ext = substr($path, $pos);
1020
            $path = substr($path, 0, $pos);
1021
            return [$path, $ext];
1022
        } else {
1023 1
            return [$path, ''];
1024
        }
1025
    }
1026
1027
    /**
1028
     * Specify data which should be serialized to JSON.
1029
     *
1030
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
1031
     * @return mixed data which can be serialized by <b>json_encode</b>,
1032
     * which is a value of any type other than a resource.
1033
     */
1034 1
    public function jsonSerialize() {
1035 1
        return $this->env;
1036
    }
1037
}
1038