├── src
├── Bridges
│ ├── HttpTracy
│ │ ├── tab.latte
│ │ ├── dist
│ │ │ ├── tab.phtml
│ │ │ └── panel.phtml
│ │ ├── SessionPanel.php
│ │ └── panel.latte
│ └── HttpDI
│ │ ├── SessionExtension.php
│ │ └── HttpExtension.php
└── Http
│ ├── Helpers.php
│ ├── Context.php
│ ├── UrlScript.php
│ ├── IRequest.php
│ ├── SessionSection.php
│ ├── Response.php
│ ├── Request.php
│ ├── FileUpload.php
│ ├── UrlImmutable.php
│ ├── RequestFactory.php
│ ├── Url.php
│ ├── IResponse.php
│ └── Session.php
├── composer.json
├── license.md
├── .phpstorm.meta.php
└── readme.md
/src/Bridges/HttpTracy/tab.latte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/Bridges/HttpTracy/dist/tab.phtml:
--------------------------------------------------------------------------------
1 |
4 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/src/Bridges/HttpTracy/SessionPanel.php:
--------------------------------------------------------------------------------
1 |
2 | #tracy-debug .nette-SessionPanel-parameters pre {
3 | background: #FDF5CE;
4 | padding: .4em .7em;
5 | border: 1px dotted silver;
6 | overflow: auto;
7 | }
8 |
9 |
10 |
Session #{substr(session_id(), 0, 10)}… (Lifetime: {ini_get('session.cookie_lifetime')})
11 |
12 |
13 | {if empty($_SESSION)}
14 |
empty
15 | {else}
16 |
17 | {foreach $_SESSION as $k => $v}
18 | {if $k === __NF}
19 |
20 | | Nette Session |
21 | {Tracy\Dumper::toHtml($v[DATA] ?? null, [Tracy\Dumper::LIVE => true])} |
22 |
23 | {elseif $k !== '_tracy'}
24 |
25 | | {$k} |
26 | {Tracy\Dumper::toHtml($v, [Tracy\Dumper::LIVE => true])} |
27 |
28 | {/if}
29 | {/foreach}
30 |
31 | {/if}
32 |
33 |
--------------------------------------------------------------------------------
/src/Bridges/HttpTracy/dist/panel.phtml:
--------------------------------------------------------------------------------
1 |
4 |
12 |
13 | Session #= Tracy\Helpers::escapeHtml(substr(session_id(), 0, 10)) ?>
14 | … (Lifetime: = Tracy\Helpers::escapeHtml(ini_get('session.cookie_lifetime')) ?>
15 | )
16 |
17 |
18 |
empty
19 |
20 | $v): ?>
21 | | Nette Session |
22 | = Tracy\Dumper::toHtml($v['DATA'] ?? null, [Tracy\Dumper::LIVE => true]) ?>
23 | |
24 |
25 |
26 | | = Tracy\Helpers::escapeHtml($k) ?>
27 | |
28 | = Tracy\Dumper::toHtml($v, [Tracy\Dumper::LIVE => true]) ?>
29 | |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nette/http",
3 | "description": "🌐 Nette Http: abstraction for HTTP request, response and session. Provides careful data sanitization and utility for URL and cookies manipulation.",
4 | "keywords": ["nette", "http", "request", "response", "session", "security", "url", "proxy", "cookies"],
5 | "homepage": "https://nette.org",
6 | "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
7 | "authors": [
8 | {
9 | "name": "David Grudl",
10 | "homepage": "https://davidgrudl.com"
11 | },
12 | {
13 | "name": "Nette Community",
14 | "homepage": "https://nette.org/contributors"
15 | }
16 | ],
17 | "require": {
18 | "php": "8.2 - 8.5",
19 | "nette/utils": "^4.0.4"
20 | },
21 | "require-dev": {
22 | "nette/di": "^3.0",
23 | "nette/tester": "^2.4",
24 | "nette/security": "^3.0",
25 | "tracy/tracy": "^2.8",
26 | "phpstan/phpstan-nette": "^2.0@stable"
27 | },
28 | "conflict": {
29 | "nette/di": "<3.0.3",
30 | "nette/schema": "<1.2"
31 | },
32 | "suggest": {
33 | "ext-fileinfo": "to detect MIME type of uploaded files by Nette\\Http\\FileUpload",
34 | "ext-gd": "to use image function in Nette\\Http\\FileUpload",
35 | "ext-session": "to use Nette\\Http\\Session",
36 | "ext-intl": "to support punycode by Nette\\Http\\Url"
37 | },
38 | "autoload": {
39 | "classmap": ["src/"],
40 | "psr-4": {
41 | "Nette\\": "src"
42 | }
43 | },
44 | "minimum-stability": "dev",
45 | "scripts": {
46 | "phpstan": "phpstan analyse",
47 | "tester": "tester tests -s"
48 | },
49 | "extra": {
50 | "branch-alias": {
51 | "dev-master": "4.0-dev"
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Http/Helpers.php:
--------------------------------------------------------------------------------
1 | setTimezone(new \DateTimeZone('GMT'));
34 | return $time->format('D, d M Y H:i:s \G\M\T');
35 | }
36 |
37 |
38 | /**
39 | * Is IP address in CIDR block?
40 | */
41 | public static function ipMatch(string $ip, string $mask): bool
42 | {
43 | [$mask, $size] = explode('/', $mask . '/');
44 | $tmp = fn(int $n): string => sprintf('%032b', $n);
45 | $ip = implode('', array_map($tmp, unpack('N*', inet_pton($ip))));
46 | $mask = implode('', array_map($tmp, unpack('N*', inet_pton($mask))));
47 | $max = strlen($ip);
48 | if (!$max || $max !== strlen($mask) || (int) $size < 0 || (int) $size > $max) {
49 | return false;
50 | }
51 |
52 | return strncmp($ip, $mask, $size === '' ? $max : (int) $size) === 0;
53 | }
54 |
55 |
56 | public static function initCookie(IRequest $request, IResponse $response)
57 | {
58 | $response->setCookie(self::StrictCookieName, '1', 0, '/', sameSite: IResponse::SameSiteStrict);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Http/Context.php:
--------------------------------------------------------------------------------
1 | response->setHeader('Last-Modified', Helpers::formatDate($lastModified));
32 | }
33 |
34 | if ($etag) {
35 | $this->response->setHeader('ETag', '"' . addslashes($etag) . '"');
36 | }
37 |
38 | $ifNoneMatch = $this->request->getHeader('If-None-Match');
39 | if ($ifNoneMatch === '*') {
40 | $match = true; // match, check if-modified-since
41 |
42 | } elseif ($ifNoneMatch !== null) {
43 | $etag = $this->response->getHeader('ETag');
44 |
45 | if ($etag === null || !str_contains(' ' . strtr($ifNoneMatch, ",\t", ' '), ' ' . $etag)) {
46 | return true;
47 |
48 | } else {
49 | $match = true; // match, check if-modified-since
50 | }
51 | }
52 |
53 | $ifModifiedSince = $this->request->getHeader('If-Modified-Since');
54 | if ($ifModifiedSince !== null) {
55 | $lastModified = $this->response->getHeader('Last-Modified');
56 | if ($lastModified !== null && strtotime($lastModified) <= strtotime($ifModifiedSince)) {
57 | $match = true;
58 |
59 | } else {
60 | return true;
61 | }
62 | }
63 |
64 | if (empty($match)) {
65 | return true;
66 | }
67 |
68 | $this->response->setCode(IResponse::S304_NotModified);
69 | return false;
70 | }
71 |
72 |
73 | public function getRequest(): IRequest
74 | {
75 | return $this->request;
76 | }
77 |
78 |
79 | public function getResponse(): IResponse
80 | {
81 | return $this->response;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | Licenses
2 | ========
3 |
4 | Good news! You may use Nette Framework under the terms of either
5 | the New BSD License or the GNU General Public License (GPL) version 2 or 3.
6 |
7 | The BSD License is recommended for most projects. It is easy to understand and it
8 | places almost no restrictions on what you can do with the framework. If the GPL
9 | fits better to your project, you can use the framework under this license.
10 |
11 | You don't have to notify anyone which license you are using. You can freely
12 | use Nette Framework in commercial projects as long as the copyright header
13 | remains intact.
14 |
15 | Please be advised that the name "Nette Framework" is a protected trademark and its
16 | usage has some limitations. So please do not use word "Nette" in the name of your
17 | project or top-level domain, and choose a name that stands on its own merits.
18 | If your stuff is good, it will not take long to establish a reputation for yourselves.
19 |
20 |
21 | New BSD License
22 | ---------------
23 |
24 | Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)
25 | All rights reserved.
26 |
27 | Redistribution and use in source and binary forms, with or without modification,
28 | are permitted provided that the following conditions are met:
29 |
30 | * Redistributions of source code must retain the above copyright notice,
31 | this list of conditions and the following disclaimer.
32 |
33 | * Redistributions in binary form must reproduce the above copyright notice,
34 | this list of conditions and the following disclaimer in the documentation
35 | and/or other materials provided with the distribution.
36 |
37 | * Neither the name of "Nette Framework" nor the names of its contributors
38 | may be used to endorse or promote products derived from this software
39 | without specific prior written permission.
40 |
41 | This software is provided by the copyright holders and contributors "as is" and
42 | any express or implied warranties, including, but not limited to, the implied
43 | warranties of merchantability and fitness for a particular purpose are
44 | disclaimed. In no event shall the copyright owner or contributors be liable for
45 | any direct, indirect, incidental, special, exemplary, or consequential damages
46 | (including, but not limited to, procurement of substitute goods or services;
47 | loss of use, data, or profits; or business interruption) however caused and on
48 | any theory of liability, whether in contract, strict liability, or tort
49 | (including negligence or otherwise) arising in any way out of the use of this
50 | software, even if advised of the possibility of such damage.
51 |
52 |
53 | GNU General Public License
54 | --------------------------
55 |
56 | GPL licenses are very very long, so instead of including them here we offer
57 | you URLs with full text:
58 |
59 | - [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html)
60 | - [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html)
61 |
--------------------------------------------------------------------------------
/src/Http/UrlScript.php:
--------------------------------------------------------------------------------
1 |
20 | * baseUrl basePath relativePath relativeUrl
21 | * | | | |
22 | * /---------------/-----\/--------\-----------------------------\
23 | * http://nette.org/admin/script.php/pathinfo/?name=param#fragment
24 | * \_______________/\________/
25 | * | |
26 | * scriptPath pathInfo
27 | *
28 | *
29 | * @property-read string $scriptPath
30 | * @property-read string $basePath
31 | * @property-read string $relativePath
32 | * @property-read string $baseUrl
33 | * @property-read string $relativeUrl
34 | * @property-read string $pathInfo
35 | */
36 | class UrlScript extends UrlImmutable
37 | {
38 | private string $scriptPath;
39 | private string $basePath;
40 |
41 |
42 | public function __construct(string|Url $url = '/', string $scriptPath = '')
43 | {
44 | parent::__construct($url);
45 | $this->setScriptPath($scriptPath);
46 | }
47 |
48 |
49 | public function withPath(string $path, string $scriptPath = ''): static
50 | {
51 | $dolly = parent::withPath($path);
52 | $dolly->setScriptPath($scriptPath);
53 | return $dolly;
54 | }
55 |
56 |
57 | private function setScriptPath(string $scriptPath): void
58 | {
59 | $path = $this->getPath();
60 | $scriptPath = $scriptPath ?: $path;
61 | $pos = strrpos($scriptPath, '/');
62 | if ($pos === false || strncmp($scriptPath, $path, $pos + 1)) {
63 | throw new Nette\InvalidArgumentException("ScriptPath '$scriptPath' doesn't match path '$path'");
64 | }
65 |
66 | $this->scriptPath = $scriptPath;
67 | $this->basePath = substr($scriptPath, 0, $pos + 1);
68 | }
69 |
70 |
71 | public function getScriptPath(): string
72 | {
73 | return $this->scriptPath;
74 | }
75 |
76 |
77 | public function getBasePath(): string
78 | {
79 | return $this->basePath;
80 | }
81 |
82 |
83 | public function getRelativePath(): string
84 | {
85 | return substr($this->getPath(), strlen($this->basePath));
86 | }
87 |
88 |
89 | public function getBaseUrl(): string
90 | {
91 | return $this->getHostUrl() . $this->basePath;
92 | }
93 |
94 |
95 | public function getRelativeUrl(): string
96 | {
97 | return substr($this->getAbsoluteUrl(), strlen($this->getBaseUrl()));
98 | }
99 |
100 |
101 | /**
102 | * Returns the additional path information.
103 | */
104 | public function getPathInfo(): string
105 | {
106 | return substr($this->getPath(), strlen($this->scriptPath));
107 | }
108 |
109 |
110 | /** @internal */
111 | protected function mergePath(string $path): string
112 | {
113 | return $this->basePath . $path;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/Bridges/HttpDI/SessionExtension.php:
--------------------------------------------------------------------------------
1 | Expect::bool(false),
33 | 'autoStart' => Expect::anyOf('smart', 'always', 'never', true, false)->firstIsDefault(),
34 | 'expiration' => Expect::string()->dynamic(),
35 | 'handler' => Expect::string()->dynamic(),
36 | 'readAndClose' => Expect::bool(),
37 | 'cookieSamesite' => Expect::anyOf(IResponse::SameSiteLax, IResponse::SameSiteStrict, IResponse::SameSiteNone)
38 | ->firstIsDefault(),
39 | ])->otherItems('mixed');
40 | }
41 |
42 |
43 | public function loadConfiguration(): void
44 | {
45 | $builder = $this->getContainerBuilder();
46 | $config = $this->config;
47 |
48 | $session = $builder->addDefinition($this->prefix('session'))
49 | ->setFactory(Nette\Http\Session::class);
50 |
51 | if ($config->expiration) {
52 | $session->addSetup('setExpiration', [$config->expiration]);
53 | }
54 |
55 | if ($config->handler) {
56 | $session->addSetup('setHandler', [$config->handler]);
57 | }
58 |
59 | if (($config->cookieDomain ?? null) === 'domain') {
60 | $config->cookieDomain = $builder::literal('$this->getByType(Nette\Http\IRequest::class)->getUrl()->getDomain(2)');
61 | }
62 |
63 | $this->compiler->addExportedType(Nette\Http\IRequest::class);
64 |
65 | if ($this->debugMode && $config->debugger) {
66 | $session->addSetup('@Tracy\Bar::addPanel', [
67 | new Nette\DI\Definitions\Statement(Nette\Bridges\HttpTracy\SessionPanel::class),
68 | ]);
69 | }
70 |
71 | $options = (array) $config;
72 | unset($options['expiration'], $options['handler'], $options['autoStart'], $options['debugger']);
73 | if ($config->autoStart === 'never') {
74 | $options['autoStart'] = false;
75 | }
76 |
77 | if ($config->readAndClose === null) {
78 | unset($options['readAndClose']);
79 | }
80 |
81 | if (!empty($options)) {
82 | $session->addSetup('setOptions', [$options]);
83 | }
84 |
85 | if ($this->name === 'session') {
86 | $builder->addAlias('session', $this->prefix('session'));
87 | }
88 |
89 | if (!$this->cliMode) {
90 | $name = $this->prefix('session');
91 |
92 | if ($config->autoStart === 'smart') {
93 | $this->initialization->addBody('$this->getService(?)->autoStart(false);', [$name]);
94 |
95 | } elseif ($config->autoStart === 'always' || $config->autoStart === true) {
96 | $this->initialization->addBody('$this->getService(?)->start();', [$name]);
97 | }
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/.phpstorm.meta.php:
--------------------------------------------------------------------------------
1 | Expect::anyOf(Expect::arrayOf('string'), Expect::string()->castTo('array'))->firstIsDefault()->dynamic(),
32 | 'headers' => Expect::arrayOf('scalar|null')->default([
33 | 'X-Powered-By' => 'Nette Framework 3',
34 | 'Content-Type' => 'text/html; charset=utf-8',
35 | ])->mergeDefaults(),
36 | 'frames' => Expect::anyOf(Expect::string(), Expect::bool(), null)->default('SAMEORIGIN'), // X-Frame-Options
37 | 'csp' => Expect::arrayOf('array|scalar|null'), // Content-Security-Policy
38 | 'cspReportOnly' => Expect::arrayOf('array|scalar|null'), // Content-Security-Policy-Report-Only
39 | 'featurePolicy' => Expect::arrayOf('array|scalar|null'), // Feature-Policy
40 | 'cookiePath' => Expect::string()->dynamic(),
41 | 'cookieDomain' => Expect::string()->dynamic(),
42 | 'cookieSecure' => Expect::anyOf('auto', null, true, false)->firstIsDefault()->dynamic(), // Whether the cookie is available only through HTTPS
43 | 'disableNetteCookie' => Expect::bool(false), // disables cookie use by Nette
44 | ]);
45 | }
46 |
47 |
48 | public function loadConfiguration(): void
49 | {
50 | $builder = $this->getContainerBuilder();
51 | $config = $this->config;
52 |
53 | $builder->addDefinition($this->prefix('requestFactory'))
54 | ->setFactory(Nette\Http\RequestFactory::class)
55 | ->addSetup('setProxy', [$config->proxy]);
56 |
57 | $request = $builder->addDefinition($this->prefix('request'))
58 | ->setFactory('@Nette\Http\RequestFactory::fromGlobals');
59 |
60 | $response = $builder->addDefinition($this->prefix('response'))
61 | ->setFactory(Nette\Http\Response::class);
62 |
63 | if ($config->cookiePath !== null) {
64 | $response->addSetup('$cookiePath', [$config->cookiePath]);
65 | }
66 |
67 | if ($config->cookieDomain !== null) {
68 | $value = $config->cookieDomain === 'domain'
69 | ? $builder::literal('$this->getService(?)->getUrl()->getDomain(2)', [$request->getName()])
70 | : $config->cookieDomain;
71 | $response->addSetup('$cookieDomain', [$value]);
72 | }
73 |
74 | if ($config->cookieSecure !== null) {
75 | $value = $config->cookieSecure === 'auto'
76 | ? $builder::literal('$this->getService(?)->isSecured()', [$request->getName()])
77 | : $config->cookieSecure;
78 | $response->addSetup('$cookieSecure', [$value]);
79 | }
80 |
81 | if ($this->name === 'http') {
82 | $builder->addAlias('nette.httpRequestFactory', $this->prefix('requestFactory'));
83 | $builder->addAlias('httpRequest', $this->prefix('request'));
84 | $builder->addAlias('httpResponse', $this->prefix('response'));
85 | }
86 |
87 | if (!$this->cliMode) {
88 | $this->sendHeaders();
89 | }
90 | }
91 |
92 |
93 | private function sendHeaders(): void
94 | {
95 | $config = $this->config;
96 | $headers = array_map('strval', $config->headers);
97 |
98 | if (isset($config->frames) && $config->frames !== true && !isset($headers['X-Frame-Options'])) {
99 | $frames = $config->frames;
100 | if ($frames === false) {
101 | $frames = 'DENY';
102 | } elseif (preg_match('#^https?:#', $frames)) {
103 | $frames = "ALLOW-FROM $frames";
104 | }
105 |
106 | $headers['X-Frame-Options'] = $frames;
107 | }
108 |
109 | foreach (['csp', 'cspReportOnly'] as $key) {
110 | if (empty($config->$key)) {
111 | continue;
112 | }
113 |
114 | $value = self::buildPolicy($config->$key);
115 | if (str_contains($value, "'nonce'")) {
116 | $this->initialization->addBody('$cspNonce = base64_encode(random_bytes(16));');
117 | $value = Nette\DI\ContainerBuilder::literal(
118 | 'str_replace(?, ? . $cspNonce, ?)',
119 | ["'nonce", "'nonce-", $value],
120 | );
121 | }
122 |
123 | $headers['Content-Security-Policy' . ($key === 'csp' ? '' : '-Report-Only')] = $value;
124 | }
125 |
126 | if (!empty($config->featurePolicy)) {
127 | $headers['Feature-Policy'] = self::buildPolicy($config->featurePolicy);
128 | }
129 |
130 | $this->initialization->addBody('$response = $this->getService(?);', [$this->prefix('response')]);
131 | foreach ($headers as $key => $value) {
132 | if ($value !== '') {
133 | $this->initialization->addBody('$response->setHeader(?, ?);', [$key, $value]);
134 | }
135 | }
136 |
137 | if (!$config->disableNetteCookie) {
138 | $this->initialization->addBody(
139 | 'Nette\Http\Helpers::initCookie($this->getService(?), $response);',
140 | [$this->prefix('request')],
141 | );
142 | }
143 | }
144 |
145 |
146 | private static function buildPolicy(array $config): string
147 | {
148 | $nonQuoted = ['require-sri-for' => 1, 'sandbox' => 1];
149 | $value = '';
150 | foreach ($config as $type => $policy) {
151 | if ($policy === false) {
152 | continue;
153 | }
154 |
155 | $policy = $policy === true ? [] : (array) $policy;
156 | $value .= $type;
157 | foreach ($policy as $item) {
158 | if (is_array($item)) {
159 | $item = key($item) . ':';
160 | }
161 |
162 | $value .= !isset($nonQuoted[$type]) && preg_match('#^[a-z-]+$#D', $item)
163 | ? " '$item'"
164 | : " $item";
165 | }
166 |
167 | $value .= '; ';
168 | }
169 |
170 | return $value;
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/Http/SessionSection.php:
--------------------------------------------------------------------------------
1 | session->autoStart(false);
37 | return new \ArrayIterator($this->getData() ?? []);
38 | }
39 |
40 |
41 | /**
42 | * Sets a variable in this session section.
43 | */
44 | public function set(string $name, mixed $value, ?string $expire = null): void
45 | {
46 | if ($value === null) {
47 | $this->remove($name);
48 | } else {
49 | $this->session->autoStart(true);
50 | $this->getData()[$name] = $value;
51 | $this->setExpiration($expire, $name);
52 | }
53 | }
54 |
55 |
56 | /**
57 | * Gets a variable from this session section.
58 | */
59 | public function get(string $name): mixed
60 | {
61 | if (func_num_args() > 1) {
62 | throw new \ArgumentCountError(__METHOD__ . '() expects 1 arguments, given more.');
63 | }
64 |
65 | $this->session->autoStart(false);
66 | return $this->getData()[$name] ?? null;
67 | }
68 |
69 |
70 | /**
71 | * Removes a variable or whole section.
72 | * @param string|string[]|null $name
73 | */
74 | public function remove(string|array|null $name = null): void
75 | {
76 | $this->session->autoStart(false);
77 | if (func_num_args() > 1) {
78 | throw new \ArgumentCountError(__METHOD__ . '() expects at most 1 arguments, given more.');
79 |
80 | } elseif (func_num_args()) {
81 | $data = &$this->getData();
82 | $meta = &$this->getMeta();
83 | foreach ((array) $name as $name) {
84 | unset($data[$name], $meta[$name]);
85 | }
86 | } else {
87 | unset($_SESSION['__NF']['DATA'][$this->name], $_SESSION['__NF']['META'][$this->name]);
88 | }
89 | }
90 |
91 |
92 | /**
93 | * Sets a variable in this session section.
94 | * @deprecated use set() instead
95 | */
96 | public function __set(string $name, $value): void
97 | {
98 | trigger_error("Writing to \$session->$name is deprecated, use \$session->set('$name', \$value) instead", E_USER_DEPRECATED);
99 | $this->session->autoStart(true);
100 | $this->getData()[$name] = $value;
101 | }
102 |
103 |
104 | /**
105 | * Gets a variable from this session section.
106 | * @deprecated use get() instead
107 | */
108 | public function &__get(string $name): mixed
109 | {
110 | trigger_error("Reading from \$session->$name is deprecated, use \$session->get('$name') instead", E_USER_DEPRECATED);
111 | $this->session->autoStart(true);
112 | $data = &$this->getData();
113 | return $data[$name];
114 | }
115 |
116 |
117 | /**
118 | * Determines whether a variable in this session section is set.
119 | * @deprecated use get() instead
120 | */
121 | public function __isset(string $name): bool
122 | {
123 | trigger_error("Using \$session->$name is deprecated, use \$session->get('$name') instead", E_USER_DEPRECATED);
124 | $this->session->autoStart(false);
125 | return isset($this->getData()[$name]);
126 | }
127 |
128 |
129 | /**
130 | * Unsets a variable in this session section.
131 | * @deprecated use remove() instead
132 | */
133 | public function __unset(string $name): void
134 | {
135 | trigger_error("Unset(\$session->$name) is deprecated, use \$session->remove('$name') instead", E_USER_DEPRECATED);
136 | $this->remove($name);
137 | }
138 |
139 |
140 | /**
141 | * Sets a variable in this session section.
142 | * @deprecated use set() instead
143 | */
144 | public function offsetSet($name, $value): void
145 | {
146 | trigger_error("Writing to \$session['$name'] is deprecated, use \$session->set('$name', \$value) instead", E_USER_DEPRECATED);
147 | $this->__set($name, $value);
148 | }
149 |
150 |
151 | /**
152 | * Gets a variable from this session section.
153 | * @deprecated use get() instead
154 | */
155 | public function offsetGet($name): mixed
156 | {
157 | trigger_error("Reading from \$session['$name'] is deprecated, use \$session->get('$name') instead", E_USER_DEPRECATED);
158 | return $this->get($name);
159 | }
160 |
161 |
162 | /**
163 | * Determines whether a variable in this session section is set.
164 | * @deprecated use get() instead
165 | */
166 | public function offsetExists($name): bool
167 | {
168 | trigger_error("Using \$session['$name'] is deprecated, use \$session->get('$name') instead", E_USER_DEPRECATED);
169 | return $this->__isset($name);
170 | }
171 |
172 |
173 | /**
174 | * Unsets a variable in this session section.
175 | * @deprecated use remove() instead
176 | */
177 | public function offsetUnset($name): void
178 | {
179 | trigger_error("Unset(\$session['$name']) is deprecated, use \$session->remove('$name') instead", E_USER_DEPRECATED);
180 | $this->remove($name);
181 | }
182 |
183 |
184 | /**
185 | * Sets the expiration of the section or specific variables.
186 | * @param string|string[]|null $variables list of variables / single variable to expire
187 | */
188 | public function setExpiration(?string $expire, string|array|null $variables = null): static
189 | {
190 | $this->session->autoStart((bool) $expire);
191 | $meta = &$this->getMeta();
192 | if ($expire) {
193 | $expire = Nette\Utils\DateTime::from($expire)->format('U');
194 | $max = (int) ini_get('session.gc_maxlifetime');
195 | if (
196 | $max !== 0 // 0 - unlimited in memcache handler
197 | && ($expire - time() > $max + 3) // 3 - bulgarian constant
198 | ) {
199 | trigger_error("The expiration time is greater than the session expiration $max seconds");
200 | }
201 | }
202 |
203 | foreach (is_array($variables) ? $variables : [$variables] as $variable) {
204 | $meta[$variable ?? '']['T'] = $expire ?: null;
205 | }
206 |
207 | return $this;
208 | }
209 |
210 |
211 | /**
212 | * Removes the expiration from the section or specific variables.
213 | * @param string|string[]|null $variables list of variables / single variable to expire
214 | */
215 | public function removeExpiration(string|array|null $variables = null): void
216 | {
217 | $this->setExpiration(null, $variables);
218 | }
219 |
220 |
221 | private function &getData()
222 | {
223 | return $_SESSION['__NF']['DATA'][$this->name];
224 | }
225 |
226 |
227 | private function &getMeta()
228 | {
229 | return $_SESSION['__NF']['META'][$this->name];
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/src/Http/Response.php:
--------------------------------------------------------------------------------
1 | code = $code;
47 | }
48 | }
49 |
50 |
51 | /**
52 | * Sets HTTP response code.
53 | * @throws Nette\InvalidArgumentException if code is invalid
54 | * @throws Nette\InvalidStateException if HTTP headers have been sent
55 | */
56 | public function setCode(int $code, ?string $reason = null): static
57 | {
58 | if ($code < 100 || $code > 599) {
59 | throw new Nette\InvalidArgumentException("Bad HTTP response '$code'.");
60 | }
61 |
62 | self::checkHeaders();
63 | $this->code = $code;
64 | $protocol = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1';
65 | $reason ??= self::ReasonPhrases[$code] ?? 'Unknown status';
66 | header("$protocol $code $reason");
67 | return $this;
68 | }
69 |
70 |
71 | /**
72 | * Returns HTTP response code.
73 | */
74 | public function getCode(): int
75 | {
76 | return $this->code;
77 | }
78 |
79 |
80 | /**
81 | * Sends an HTTP header and overwrites previously sent header of the same name.
82 | * @throws Nette\InvalidStateException if HTTP headers have been sent
83 | */
84 | public function setHeader(string $name, ?string $value): static
85 | {
86 | self::checkHeaders();
87 | if ($value === null) {
88 | header_remove($name);
89 | } elseif (strcasecmp($name, 'Content-Length') === 0 && ini_get('zlib.output_compression')) {
90 | // ignore, PHP bug #44164
91 | } else {
92 | header($name . ': ' . $value);
93 | }
94 |
95 | return $this;
96 | }
97 |
98 |
99 | /**
100 | * Sends an HTTP header and doesn't overwrite previously sent header of the same name.
101 | * @throws Nette\InvalidStateException if HTTP headers have been sent
102 | */
103 | public function addHeader(string $name, string $value): static
104 | {
105 | self::checkHeaders();
106 | header($name . ': ' . $value, replace: false);
107 | return $this;
108 | }
109 |
110 |
111 | /**
112 | * Deletes a previously sent HTTP header.
113 | * @throws Nette\InvalidStateException if HTTP headers have been sent
114 | */
115 | public function deleteHeader(string $name): static
116 | {
117 | self::checkHeaders();
118 | header_remove($name);
119 | return $this;
120 | }
121 |
122 |
123 | /**
124 | * Sends a Content-type HTTP header.
125 | * @throws Nette\InvalidStateException if HTTP headers have been sent
126 | */
127 | public function setContentType(string $type, ?string $charset = null): static
128 | {
129 | $this->setHeader('Content-Type', $type . ($charset ? '; charset=' . $charset : ''));
130 | return $this;
131 | }
132 |
133 |
134 | /**
135 | * Response should be downloaded with 'Save as' dialog.
136 | * @throws Nette\InvalidStateException if HTTP headers have been sent
137 | */
138 | public function sendAsFile(string $fileName): static
139 | {
140 | $this->setHeader(
141 | 'Content-Disposition',
142 | 'attachment; filename="' . str_replace('"', '', $fileName) . '"; '
143 | . "filename*=utf-8''" . rawurlencode($fileName),
144 | );
145 | return $this;
146 | }
147 |
148 |
149 | /**
150 | * Redirects to another URL. Don't forget to quit the script then.
151 | * @throws Nette\InvalidStateException if HTTP headers have been sent
152 | */
153 | public function redirect(string $url, int $code = self::S302_Found): void
154 | {
155 | $this->setCode($code);
156 | $this->setHeader('Location', $url);
157 | if (preg_match('#^https?:|^\s*+[a-z0-9+.-]*+[^:]#i', $url)) {
158 | $escapedUrl = htmlspecialchars($url, ENT_IGNORE | ENT_QUOTES, 'UTF-8');
159 | echo "Redirect
\n\nPlease click here to continue.
";
160 | }
161 | }
162 |
163 |
164 | /**
165 | * Sets the expiration of the HTTP document using the `Cache-Control` and `Expires` headers.
166 | * The parameter is either a time interval (as text) or `null`, which disables caching.
167 | * @throws Nette\InvalidStateException if HTTP headers have been sent
168 | */
169 | public function setExpiration(?string $expire): static
170 | {
171 | $this->setHeader('Pragma', null);
172 | if (!$expire) { // no cache
173 | $this->setHeader('Cache-Control', 's-maxage=0, max-age=0, must-revalidate');
174 | $this->setHeader('Expires', 'Mon, 23 Jan 1978 10:00:00 GMT');
175 | return $this;
176 | }
177 |
178 | $expire = DateTime::from($expire);
179 | $this->setHeader('Cache-Control', 'max-age=' . ($expire->format('U') - time()));
180 | $this->setHeader('Expires', Helpers::formatDate($expire));
181 | return $this;
182 | }
183 |
184 |
185 | /**
186 | * Returns whether headers have already been sent from the server to the browser,
187 | * so it is no longer possible to send headers or change the response code.
188 | */
189 | public function isSent(): bool
190 | {
191 | return headers_sent();
192 | }
193 |
194 |
195 | /**
196 | * Returns the sent HTTP header, or `null` if it does not exist. The parameter is case-insensitive.
197 | */
198 | public function getHeader(string $header): ?string
199 | {
200 | $header .= ':';
201 | $len = strlen($header);
202 | foreach (headers_list() as $item) {
203 | if (strncasecmp($item, $header, $len) === 0) {
204 | return ltrim(substr($item, $len));
205 | }
206 | }
207 |
208 | return null;
209 | }
210 |
211 |
212 | /**
213 | * Returns all sent HTTP headers as associative array.
214 | */
215 | public function getHeaders(): array
216 | {
217 | $headers = [];
218 | foreach (headers_list() as $header) {
219 | $a = strpos($header, ':');
220 | $headers[substr($header, 0, $a)] = substr($header, $a + 2);
221 | }
222 |
223 | return $headers;
224 | }
225 |
226 |
227 | /**
228 | * Sends a cookie.
229 | * @throws Nette\InvalidStateException if HTTP headers have been sent
230 | */
231 | public function setCookie(
232 | string $name,
233 | string $value,
234 | string|int|\DateTimeInterface|null $expire,
235 | ?string $path = null,
236 | ?string $domain = null,
237 | ?bool $secure = null,
238 | bool $httpOnly = true,
239 | string $sameSite = self::SameSiteLax,
240 | ): static
241 | {
242 | self::checkHeaders();
243 | setcookie($name, $value, [
244 | 'expires' => $expire ? (int) DateTime::from($expire)->format('U') : 0,
245 | 'path' => $path ?? ($domain ? '/' : $this->cookiePath),
246 | 'domain' => $domain ?? ($path ? '' : $this->cookieDomain),
247 | 'secure' => $secure ?? $this->cookieSecure,
248 | 'httponly' => $httpOnly,
249 | 'samesite' => $sameSite,
250 | ]);
251 | return $this;
252 | }
253 |
254 |
255 | /**
256 | * Deletes a cookie.
257 | * @throws Nette\InvalidStateException if HTTP headers have been sent
258 | */
259 | public function deleteCookie(
260 | string $name,
261 | ?string $path = null,
262 | ?string $domain = null,
263 | ?bool $secure = null,
264 | ): void
265 | {
266 | $this->setCookie($name, '', 0, $path, $domain, $secure);
267 | }
268 |
269 |
270 | private function checkHeaders(): void
271 | {
272 | if (PHP_SAPI === 'cli') {
273 | } elseif (headers_sent($file, $line)) {
274 | throw new Nette\InvalidStateException('Cannot send header after HTTP headers have been sent' . ($file ? " (output started at $file:$line)." : '.'));
275 |
276 | } elseif (
277 | $this->warnOnBuffer &&
278 | ob_get_length() &&
279 | !array_filter(ob_get_status(true), fn(array $i): bool => !$i['chunk_size'])
280 | ) {
281 | trigger_error('Possible problem: you are sending a HTTP header while already having some data in output buffer. Try Tracy\OutputDebugger or send cookies/start session earlier.');
282 | }
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/src/Http/Request.php:
--------------------------------------------------------------------------------
1 | headers = array_change_key_case($headers, CASE_LOWER);
55 | $this->rawBodyCallback = $rawBodyCallback ? $rawBodyCallback(...) : null;
56 | }
57 |
58 |
59 | /**
60 | * Returns a clone with a different URL.
61 | */
62 | public function withUrl(UrlScript $url): static
63 | {
64 | $dolly = clone $this;
65 | $dolly->url = $url;
66 | return $dolly;
67 | }
68 |
69 |
70 | /**
71 | * Returns the URL of the request.
72 | */
73 | public function getUrl(): UrlScript
74 | {
75 | return $this->url;
76 | }
77 |
78 |
79 | /********************* query, post, files & cookies ****************d*g**/
80 |
81 |
82 | /**
83 | * Returns variable provided to the script via URL query ($_GET).
84 | * If no key is passed, returns the entire array.
85 | */
86 | public function getQuery(?string $key = null): mixed
87 | {
88 | if (func_num_args() === 0) {
89 | return $this->url->getQueryParameters();
90 | }
91 |
92 | return $this->url->getQueryParameter($key);
93 | }
94 |
95 |
96 | /**
97 | * Returns variable provided to the script via POST method ($_POST).
98 | * If no key is passed, returns the entire array.
99 | */
100 | public function getPost(?string $key = null): mixed
101 | {
102 | if (func_num_args() === 0) {
103 | return $this->post;
104 | }
105 |
106 | return $this->post[$key] ?? null;
107 | }
108 |
109 |
110 | /**
111 | * Returns uploaded file.
112 | * @param string|string[] $key
113 | */
114 | public function getFile($key): ?FileUpload
115 | {
116 | $res = Nette\Utils\Arrays::get($this->files, $key, null);
117 | return $res instanceof FileUpload ? $res : null;
118 | }
119 |
120 |
121 | /**
122 | * Returns tree of upload files in a normalized structure, with each leaf an instance of Nette\Http\FileUpload.
123 | */
124 | public function getFiles(): array
125 | {
126 | return $this->files;
127 | }
128 |
129 |
130 | /**
131 | * Returns a cookie or `null` if it does not exist.
132 | */
133 | public function getCookie(string $key): mixed
134 | {
135 | return $this->cookies[$key] ?? null;
136 | }
137 |
138 |
139 | /**
140 | * Returns all cookies.
141 | */
142 | public function getCookies(): array
143 | {
144 | return $this->cookies;
145 | }
146 |
147 |
148 | /********************* method & headers ****************d*g**/
149 |
150 |
151 | /**
152 | * Returns the HTTP method with which the request was made (GET, POST, HEAD, PUT, ...).
153 | */
154 | public function getMethod(): string
155 | {
156 | return $this->method;
157 | }
158 |
159 |
160 | /**
161 | * Checks the HTTP method with which the request was made. The parameter is case-insensitive.
162 | */
163 | public function isMethod(string $method): bool
164 | {
165 | return strcasecmp($this->method, $method) === 0;
166 | }
167 |
168 |
169 | /**
170 | * Returns an HTTP header or `null` if it does not exist. The parameter is case-insensitive.
171 | */
172 | public function getHeader(string $header): ?string
173 | {
174 | $header = strtolower($header);
175 | return $this->headers[$header] ?? null;
176 | }
177 |
178 |
179 | /**
180 | * Returns all HTTP headers as associative array.
181 | */
182 | public function getHeaders(): array
183 | {
184 | return $this->headers;
185 | }
186 |
187 |
188 | /**
189 | * What URL did the user come from? Beware, it is not reliable at all.
190 | * @deprecated deprecated in favor of the getOrigin()
191 | */
192 | public function getReferer(): ?UrlImmutable
193 | {
194 | trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED);
195 | return isset($this->headers['referer'])
196 | ? new UrlImmutable($this->headers['referer'])
197 | : null;
198 | }
199 |
200 |
201 | /**
202 | * What origin did the user come from? It contains scheme, hostname and port.
203 | */
204 | public function getOrigin(): ?UrlImmutable
205 | {
206 | $header = $this->headers['origin'] ?? 'null';
207 | try {
208 | return $header === 'null'
209 | ? null
210 | : new UrlImmutable($header);
211 | } catch (Nette\InvalidArgumentException $e) {
212 | return null;
213 | }
214 | }
215 |
216 |
217 | /**
218 | * Is the request sent via secure channel (https)?
219 | */
220 | public function isSecured(): bool
221 | {
222 | return $this->url->getScheme() === 'https';
223 | }
224 |
225 |
226 | /**
227 | * Is the request coming from the same site and is initiated by clicking on a link?
228 | */
229 | public function isSameSite(): bool
230 | {
231 | return isset($this->cookies[Helpers::StrictCookieName]);
232 | }
233 |
234 |
235 | /**
236 | * Is it an AJAX request?
237 | */
238 | public function isAjax(): bool
239 | {
240 | return $this->getHeader('X-Requested-With') === 'XMLHttpRequest';
241 | }
242 |
243 |
244 | /**
245 | * Returns the IP address of the remote client.
246 | */
247 | public function getRemoteAddress(): ?string
248 | {
249 | return $this->remoteAddress;
250 | }
251 |
252 |
253 | /**
254 | * Returns the host of the remote client.
255 | */
256 | public function getRemoteHost(): ?string
257 | {
258 | return $this->remoteHost;
259 | }
260 |
261 |
262 | /**
263 | * Returns raw content of HTTP request body.
264 | */
265 | public function getRawBody(): ?string
266 | {
267 | return $this->rawBodyCallback ? ($this->rawBodyCallback)() : null;
268 | }
269 |
270 |
271 | /**
272 | * Returns decoded content of HTTP request body.
273 | */
274 | public function getDecodedBody(): mixed
275 | {
276 | $type = $this->getHeader('Content-Type');
277 | return match ($type) {
278 | 'application/json' => json_decode($this->getRawBody()),
279 | 'application/x-www-form-urlencoded' => $_POST,
280 | default => throw new \Exception("Unsupported content type: $type"),
281 | };
282 | }
283 |
284 |
285 | /**
286 | * Returns basic HTTP authentication credentials.
287 | * @return array{string, string}|null
288 | */
289 | public function getBasicCredentials(): ?array
290 | {
291 | return preg_match(
292 | '~^Basic (\S+)$~',
293 | $this->headers['authorization'] ?? '',
294 | $t,
295 | )
296 | && ($t = base64_decode($t[1], strict: true))
297 | && ($t = explode(':', $t, 2))
298 | && (count($t) === 2)
299 | ? $t
300 | : null;
301 | }
302 |
303 |
304 | /**
305 | * Returns the most preferred language by browser. Uses the `Accept-Language` header. If no match is reached, it returns `null`.
306 | * @param string[] $langs supported languages
307 | */
308 | public function detectLanguage(array $langs): ?string
309 | {
310 | $header = $this->getHeader('Accept-Language');
311 | if (!$header) {
312 | return null;
313 | }
314 |
315 | $s = strtolower($header); // case insensitive
316 | $s = strtr($s, '_', '-'); // cs_CZ means cs-CZ
317 | rsort($langs); // first more specific
318 | preg_match_all('#(' . implode('|', $langs) . ')(?:-[^\s,;=]+)?\s*(?:;\s*q=([0-9.]+))?#', $s, $matches);
319 |
320 | if (!$matches[0]) {
321 | return null;
322 | }
323 |
324 | $max = 0;
325 | $lang = null;
326 | foreach ($matches[1] as $key => $value) {
327 | $q = $matches[2][$key] === '' ? 1.0 : (float) $matches[2][$key];
328 | if ($q > $max) {
329 | $max = $q;
330 | $lang = $value;
331 | }
332 | }
333 |
334 | return $lang;
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/src/Http/FileUpload.php:
--------------------------------------------------------------------------------
1 | basename($value),
52 | 'full_path' => $value,
53 | 'size' => filesize($value),
54 | 'tmp_name' => $value,
55 | 'error' => UPLOAD_ERR_OK,
56 | ];
57 | }
58 |
59 | $this->name = $value['name'] ?? '';
60 | $this->fullPath = $value['full_path'] ?? null;
61 | $this->size = $value['size'] ?? 0;
62 | $this->tmpName = $value['tmp_name'] ?? '';
63 | $this->error = $value['error'] ?? UPLOAD_ERR_NO_FILE;
64 | }
65 |
66 |
67 | /**
68 | * @deprecated use getUntrustedName()
69 | */
70 | public function getName(): string
71 | {
72 | trigger_error(__METHOD__ . '() is deprecated, use getUntrustedName()', E_USER_DEPRECATED);
73 | return $this->name;
74 | }
75 |
76 |
77 | /**
78 | * Returns the original file name as submitted by the browser. Do not trust the value returned by this method.
79 | * A client could send a malicious filename with the intention to corrupt or hack your application.
80 | */
81 | public function getUntrustedName(): string
82 | {
83 | return $this->name;
84 | }
85 |
86 |
87 | /**
88 | * Returns the sanitized file name. The resulting name contains only ASCII characters [a-zA-Z0-9.-].
89 | * If the name does not contain such characters, it returns 'unknown'. If the file is an image supported by PHP,
90 | * it returns the correct file extension. Do not blindly trust the value returned by this method.
91 | */
92 | public function getSanitizedName(): string
93 | {
94 | $name = Nette\Utils\Strings::webalize($this->name, '.', lower: false);
95 | $name = str_replace(['-.', '.-'], '.', $name);
96 | $name = trim($name, '.-');
97 | $name = $name === '' ? 'unknown' : $name;
98 | if ($this->isImage()) {
99 | $name = preg_replace('#\.[^.]+$#D', '', $name);
100 | $name .= '.' . $this->getSuggestedExtension();
101 | }
102 |
103 | return $name;
104 | }
105 |
106 |
107 | /**
108 | * Returns the original full path as submitted by the browser during directory upload. Do not trust the value
109 | * returned by this method. A client could send a malicious directory structure with the intention to corrupt
110 | * or hack your application.
111 | */
112 | public function getUntrustedFullPath(): string
113 | {
114 | return $this->fullPath ?? $this->name;
115 | }
116 |
117 |
118 | /**
119 | * Detects the MIME content type of the uploaded file based on its signature. Requires PHP extension fileinfo.
120 | * If the upload was not successful or the detection failed, it returns null.
121 | */
122 | public function getContentType(): ?string
123 | {
124 | if ($this->isOk()) {
125 | $this->type ??= finfo_file(finfo_open(FILEINFO_MIME_TYPE), $this->tmpName);
126 | }
127 |
128 | return $this->type ?: null;
129 | }
130 |
131 |
132 | /**
133 | * Returns the appropriate file extension (without the period) corresponding to the detected MIME type. Requires the PHP extension fileinfo.
134 | */
135 | public function getSuggestedExtension(): ?string
136 | {
137 | if ($this->isOk() && $this->extension === null) {
138 | $exts = finfo_file(finfo_open(FILEINFO_EXTENSION), $this->tmpName);
139 | if ($exts && $exts !== '???') {
140 | return $this->extension = preg_replace('~[/,].*~', '', $exts);
141 | }
142 | [, , $type] = Nette\Utils\Helpers::falseToNull(@getimagesize($this->tmpName)); // @ - files smaller than 12 bytes causes read error
143 | if ($type) {
144 | return $this->extension = image_type_to_extension($type, false);
145 | }
146 | $this->extension = false;
147 | }
148 |
149 | return $this->extension ?: null;
150 | }
151 |
152 |
153 | /**
154 | * Returns the size of the uploaded file in bytes.
155 | */
156 | public function getSize(): int
157 | {
158 | return $this->size;
159 | }
160 |
161 |
162 | /**
163 | * Returns the path of the temporary location of the uploaded file.
164 | */
165 | public function getTemporaryFile(): string
166 | {
167 | return $this->tmpName;
168 | }
169 |
170 |
171 | /**
172 | * Returns the path of the temporary location of the uploaded file.
173 | */
174 | public function __toString(): string
175 | {
176 | return $this->tmpName;
177 | }
178 |
179 |
180 | /**
181 | * Returns the error code. It has to be one of UPLOAD_ERR_XXX constants.
182 | * @see http://php.net/manual/en/features.file-upload.errors.php
183 | */
184 | public function getError(): int
185 | {
186 | return $this->error;
187 | }
188 |
189 |
190 | /**
191 | * Returns true if the file was uploaded successfully.
192 | */
193 | public function isOk(): bool
194 | {
195 | return $this->error === UPLOAD_ERR_OK;
196 | }
197 |
198 |
199 | /**
200 | * Returns true if the user has uploaded a file.
201 | */
202 | public function hasFile(): bool
203 | {
204 | return $this->error !== UPLOAD_ERR_NO_FILE;
205 | }
206 |
207 |
208 | /**
209 | * Moves an uploaded file to a new location. If the destination file already exists, it will be overwritten.
210 | */
211 | public function move(string $dest): static
212 | {
213 | $dir = dirname($dest);
214 | Nette\Utils\FileSystem::createDir($dir);
215 | @unlink($dest); // @ - file may not exists
216 | Nette\Utils\Callback::invokeSafe(
217 | is_uploaded_file($this->tmpName) ? 'move_uploaded_file' : 'rename',
218 | [$this->tmpName, $dest],
219 | function (string $message) use ($dest): void {
220 | throw new Nette\InvalidStateException("Unable to move uploaded file '$this->tmpName' to '$dest'. $message");
221 | },
222 | );
223 | @chmod($dest, 0o666); // @ - possible low permission to chmod
224 | $this->tmpName = $dest;
225 | return $this;
226 | }
227 |
228 |
229 | /**
230 | * Returns true if the uploaded file is an image and the format is supported by PHP, so it can be loaded using the toImage() method.
231 | * Detection is based on its signature, the integrity of the file is not checked. Requires PHP extensions fileinfo & gd.
232 | */
233 | public function isImage(): bool
234 | {
235 | $types = array_map(fn($type) => Image::typeToMimeType($type), Image::getSupportedTypes());
236 | return in_array($this->getContentType(), $types, strict: true);
237 | }
238 |
239 |
240 | /**
241 | * Converts uploaded image to Nette\Utils\Image object.
242 | * @throws Nette\Utils\ImageException If the upload was not successful or is not a valid image
243 | */
244 | public function toImage(): Image
245 | {
246 | return Image::fromFile($this->tmpName);
247 | }
248 |
249 |
250 | /**
251 | * Returns a pair of [width, height] with dimensions of the uploaded image.
252 | */
253 | public function getImageSize(): ?array
254 | {
255 | return $this->isImage()
256 | ? array_intersect_key(getimagesize($this->tmpName), [0, 1])
257 | : null;
258 | }
259 |
260 |
261 | /**
262 | * Returns image file extension based on detected content type (without dot).
263 | * @deprecated use getSuggestedExtension()
264 | */
265 | public function getImageFileExtension(): ?string
266 | {
267 | return $this->getSuggestedExtension();
268 | }
269 |
270 |
271 | /**
272 | * Returns the contents of the uploaded file. If the upload was not successful, it returns null.
273 | */
274 | public function getContents(): ?string
275 | {
276 | // future implementation can try to work around safe_mode and open_basedir limitations
277 | return $this->isOk()
278 | ? file_get_contents($this->tmpName)
279 | : null;
280 | }
281 | }
282 |
--------------------------------------------------------------------------------
/src/Http/UrlImmutable.php:
--------------------------------------------------------------------------------
1 |
21 | * scheme user password host port path query fragment
22 | * | | | | | | | |
23 | * /--\ /--\ /------\ /-------\ /--\/------------\ /--------\ /------\
24 | * http://john:x0y17575@nette.org:8042/en/manual.php?name=param#fragment <-- absoluteUrl
25 | * \______\__________________________/
26 | * | |
27 | * hostUrl authority
28 | *
29 | *
30 | * @property-read string $scheme
31 | * @property-read string $user
32 | * @property-read string $password
33 | * @property-read string $host
34 | * @property-read int $port
35 | * @property-read string $path
36 | * @property-read string $query
37 | * @property-read string $fragment
38 | * @property-read string $absoluteUrl
39 | * @property-read string $authority
40 | * @property-read string $hostUrl
41 | * @property-read array $queryParameters
42 | */
43 | class UrlImmutable implements \JsonSerializable
44 | {
45 | use Nette\SmartObject;
46 |
47 | private string $scheme = '';
48 | private string $user = '';
49 | private string $password = '';
50 | private string $host = '';
51 | private ?int $port = null;
52 | private string $path = '';
53 | private array $query = [];
54 | private string $fragment = '';
55 | private ?string $authority = null;
56 |
57 |
58 | /**
59 | * @throws Nette\InvalidArgumentException if URL is malformed
60 | */
61 | public function __construct(string|self|Url $url)
62 | {
63 | $url = is_string($url) ? new Url($url) : $url;
64 | [$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment] = $url->export();
65 | }
66 |
67 |
68 | public function withScheme(string $scheme): static
69 | {
70 | $dolly = clone $this;
71 | $dolly->scheme = $scheme;
72 | $dolly->authority = null;
73 | return $dolly;
74 | }
75 |
76 |
77 | public function getScheme(): string
78 | {
79 | return $this->scheme;
80 | }
81 |
82 |
83 | /** @deprecated */
84 | public function withUser(string $user): static
85 | {
86 | trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED);
87 | $dolly = clone $this;
88 | $dolly->user = $user;
89 | $dolly->authority = null;
90 | return $dolly;
91 | }
92 |
93 |
94 | /** @deprecated */
95 | public function getUser(): string
96 | {
97 | trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED);
98 | return $this->user;
99 | }
100 |
101 |
102 | /** @deprecated */
103 | public function withPassword(string $password): static
104 | {
105 | trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED);
106 | $dolly = clone $this;
107 | $dolly->password = $password;
108 | $dolly->authority = null;
109 | return $dolly;
110 | }
111 |
112 |
113 | /** @deprecated */
114 | public function getPassword(): string
115 | {
116 | trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED);
117 | return $this->password;
118 | }
119 |
120 |
121 | /** @deprecated */
122 | public function withoutUserInfo(): static
123 | {
124 | trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED);
125 | $dolly = clone $this;
126 | $dolly->user = $dolly->password = '';
127 | $dolly->authority = null;
128 | return $dolly;
129 | }
130 |
131 |
132 | public function withHost(string $host): static
133 | {
134 | $dolly = clone $this;
135 | $dolly->host = $host;
136 | $dolly->authority = null;
137 | return $dolly->setPath($dolly->path);
138 | }
139 |
140 |
141 | public function getHost(): string
142 | {
143 | return $this->host;
144 | }
145 |
146 |
147 | public function getDomain(int $level = 2): string
148 | {
149 | $parts = ip2long($this->host)
150 | ? [$this->host]
151 | : explode('.', $this->host);
152 | $parts = $level >= 0
153 | ? array_slice($parts, -$level)
154 | : array_slice($parts, 0, $level);
155 | return implode('.', $parts);
156 | }
157 |
158 |
159 | public function withPort(int $port): static
160 | {
161 | $dolly = clone $this;
162 | $dolly->port = $port;
163 | $dolly->authority = null;
164 | return $dolly;
165 | }
166 |
167 |
168 | public function getPort(): ?int
169 | {
170 | return $this->port ?: $this->getDefaultPort();
171 | }
172 |
173 |
174 | public function getDefaultPort(): ?int
175 | {
176 | return Url::$defaultPorts[$this->scheme] ?? null;
177 | }
178 |
179 |
180 | public function withPath(string $path): static
181 | {
182 | return (clone $this)->setPath($path);
183 | }
184 |
185 |
186 | private function setPath(string $path): static
187 | {
188 | $this->path = $this->host && !str_starts_with($path, '/') ? '/' . $path : $path;
189 | return $this;
190 | }
191 |
192 |
193 | public function getPath(): string
194 | {
195 | return $this->path;
196 | }
197 |
198 |
199 | public function withQuery(string|array $query): static
200 | {
201 | $dolly = clone $this;
202 | $dolly->query = is_array($query) ? $query : Url::parseQuery($query);
203 | return $dolly;
204 | }
205 |
206 |
207 | public function getQuery(): string
208 | {
209 | return http_build_query($this->query, '', '&', PHP_QUERY_RFC3986);
210 | }
211 |
212 |
213 | public function withQueryParameter(string $name, mixed $value): static
214 | {
215 | $dolly = clone $this;
216 | $dolly->query[$name] = $value;
217 | return $dolly;
218 | }
219 |
220 |
221 | public function getQueryParameters(): array
222 | {
223 | return $this->query;
224 | }
225 |
226 |
227 | public function getQueryParameter(string $name): array|string|null
228 | {
229 | return $this->query[$name] ?? null;
230 | }
231 |
232 |
233 | public function withFragment(string $fragment): static
234 | {
235 | $dolly = clone $this;
236 | $dolly->fragment = $fragment;
237 | return $dolly;
238 | }
239 |
240 |
241 | public function getFragment(): string
242 | {
243 | return $this->fragment;
244 | }
245 |
246 |
247 | /**
248 | * Returns the entire URI including query string and fragment.
249 | */
250 | public function getAbsoluteUrl(): string
251 | {
252 | return $this->getHostUrl() . $this->path
253 | . (($tmp = $this->getQuery()) ? '?' . $tmp : '')
254 | . ($this->fragment === '' ? '' : '#' . $this->fragment);
255 | }
256 |
257 |
258 | /**
259 | * Returns the [user[:pass]@]host[:port] part of URI.
260 | */
261 | public function getAuthority(): string
262 | {
263 | return $this->authority ??= $this->host === ''
264 | ? ''
265 | : ($this->user !== ''
266 | ? rawurlencode($this->user) . ($this->password === '' ? '' : ':' . rawurlencode($this->password)) . '@'
267 | : '')
268 | . $this->host
269 | . ($this->port && $this->port !== $this->getDefaultPort()
270 | ? ':' . $this->port
271 | : '');
272 | }
273 |
274 |
275 | /**
276 | * Returns the scheme and authority part of URI.
277 | */
278 | public function getHostUrl(): string
279 | {
280 | return ($this->scheme === '' ? '' : $this->scheme . ':')
281 | . ($this->host === '' ? '' : '//' . $this->getAuthority());
282 | }
283 |
284 |
285 | public function __toString(): string
286 | {
287 | return $this->getAbsoluteUrl();
288 | }
289 |
290 |
291 | public function isEqual(string|Url|self $url): bool
292 | {
293 | return (new Url($this))->isEqual($url);
294 | }
295 |
296 |
297 | /**
298 | * Resolves relative URLs in the same way as browser. If path is relative, it is resolved against
299 | * base URL, if begins with /, it is resolved against the host root.
300 | */
301 | public function resolve(string $reference): self
302 | {
303 | $ref = new self($reference);
304 | if ($ref->scheme !== '') {
305 | $ref->path = Url::removeDotSegments($ref->path);
306 | return $ref;
307 | }
308 |
309 | $ref->scheme = $this->scheme;
310 |
311 | if ($ref->host !== '') {
312 | $ref->path = Url::removeDotSegments($ref->path);
313 | return $ref;
314 | }
315 |
316 | $ref->host = $this->host;
317 | $ref->port = $this->port;
318 |
319 | if ($ref->path === '') {
320 | $ref->path = $this->path;
321 | $ref->query = $ref->query ?: $this->query;
322 | } elseif (str_starts_with($ref->path, '/')) {
323 | $ref->path = Url::removeDotSegments($ref->path);
324 | } else {
325 | $ref->path = Url::removeDotSegments($this->mergePath($ref->path));
326 | }
327 | return $ref;
328 | }
329 |
330 |
331 | /** @internal */
332 | protected function mergePath(string $path): string
333 | {
334 | $pos = strrpos($this->path, '/');
335 | return $pos === false ? $path : substr($this->path, 0, $pos + 1) . $path;
336 | }
337 |
338 |
339 | public function jsonSerialize(): string
340 | {
341 | return $this->getAbsoluteUrl();
342 | }
343 |
344 |
345 | /** @internal */
346 | final public function export(): array
347 | {
348 | return [$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment];
349 | }
350 | }
351 |
--------------------------------------------------------------------------------
/src/Http/RequestFactory.php:
--------------------------------------------------------------------------------
1 | ['#//#' => '/'], // '%20' => ''
29 | 'url' => [], // '#[.,)]$#D' => ''
30 | ];
31 |
32 | private bool $binary = false;
33 |
34 | /** @var string[] */
35 | private array $proxies = [];
36 |
37 |
38 | public function setBinary(bool $binary = true): static
39 | {
40 | $this->binary = $binary;
41 | return $this;
42 | }
43 |
44 |
45 | /**
46 | * @param string|string[] $proxy
47 | */
48 | public function setProxy($proxy): static
49 | {
50 | $this->proxies = (array) $proxy;
51 | return $this;
52 | }
53 |
54 |
55 | /**
56 | * Returns new Request instance, using values from superglobals.
57 | */
58 | public function fromGlobals(): Request
59 | {
60 | $url = new Url;
61 | $this->getServer($url);
62 | $this->getPathAndQuery($url);
63 | [$post, $cookies] = $this->getGetPostCookie($url);
64 | [$remoteAddr, $remoteHost] = $this->getClient($url);
65 |
66 | return new Request(
67 | new UrlScript($url, $this->getScriptPath($url)),
68 | $post,
69 | $this->getFiles(),
70 | $cookies,
71 | $this->getHeaders(),
72 | $this->getMethod(),
73 | $remoteAddr,
74 | $remoteHost,
75 | fn(): string => file_get_contents('php://input'),
76 | );
77 | }
78 |
79 |
80 | private function getServer(Url $url): void
81 | {
82 | $url->setScheme(!empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https' : 'http');
83 |
84 | if (
85 | (isset($_SERVER[$tmp = 'HTTP_HOST']) || isset($_SERVER[$tmp = 'SERVER_NAME']))
86 | && ($pair = $this->parseHostAndPort($_SERVER[$tmp]))
87 | ) {
88 | $url->setHost($pair[0]);
89 | if (isset($pair[1])) {
90 | $url->setPort($pair[1]);
91 | } elseif ($tmp === 'SERVER_NAME' && isset($_SERVER['SERVER_PORT'])) {
92 | $url->setPort((int) $_SERVER['SERVER_PORT']);
93 | }
94 | }
95 | }
96 |
97 |
98 | private function getPathAndQuery(Url $url): void
99 | {
100 | $requestUrl = $_SERVER['REQUEST_URI'] ?? '/';
101 | $requestUrl = preg_replace('#^\w++://[^/]++#', '', $requestUrl);
102 | $requestUrl = Strings::replace($requestUrl, $this->urlFilters['url']);
103 |
104 | $tmp = explode('?', $requestUrl, 2);
105 | $path = Url::unescape($tmp[0], '%/?#');
106 | $path = Strings::fixEncoding(Strings::replace($path, $this->urlFilters['path']));
107 | $url->setPath($path);
108 | $url->setQuery($tmp[1] ?? '');
109 | }
110 |
111 |
112 | private function getScriptPath(Url $url): string
113 | {
114 | if (PHP_SAPI === 'cli-server') {
115 | return '/';
116 | }
117 |
118 | $path = $url->getPath();
119 | $lpath = strtolower($path);
120 | $script = strtolower($_SERVER['SCRIPT_NAME'] ?? '');
121 | if ($lpath !== $script) {
122 | $max = min(strlen($lpath), strlen($script));
123 | for ($i = 0; $i < $max && $lpath[$i] === $script[$i]; $i++);
124 | $path = $i
125 | ? substr($path, 0, strrpos($path, '/', $i - strlen($path) - 1) + 1)
126 | : '/';
127 | }
128 |
129 | return $path;
130 | }
131 |
132 |
133 | private function getGetPostCookie(Url $url): array
134 | {
135 | $useFilter = (!in_array((string) ini_get('filter.default'), ['', 'unsafe_raw'], true) || ini_get('filter.default_flags'));
136 |
137 | $query = $url->getQueryParameters();
138 | $post = $useFilter
139 | ? filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW)
140 | : (empty($_POST) ? [] : $_POST);
141 | $cookies = $useFilter
142 | ? filter_input_array(INPUT_COOKIE, FILTER_UNSAFE_RAW)
143 | : (empty($_COOKIE) ? [] : $_COOKIE);
144 |
145 | // remove invalid characters
146 | $reChars = '#^[' . self::ValidChars . ']*+$#Du';
147 | if (!$this->binary) {
148 | $list = [&$query, &$post, &$cookies];
149 | foreach ($list as $key => &$val) {
150 | foreach ($val as $k => $v) {
151 | if (is_string($k) && (!preg_match($reChars, $k) || preg_last_error())) {
152 | unset($list[$key][$k]);
153 |
154 | } elseif (is_array($v)) {
155 | $list[$key][$k] = $v;
156 | $list[] = &$list[$key][$k];
157 |
158 | } elseif (is_string($v)) {
159 | $list[$key][$k] = (string) preg_replace('#[^' . self::ValidChars . ']+#u', '', $v);
160 |
161 | } else {
162 | throw new Nette\InvalidStateException(sprintf('Invalid value in $_POST/$_COOKIE in key %s, expected string, %s given.', "'$k'", get_debug_type($v)));
163 | }
164 | }
165 | }
166 |
167 | unset($list, $key, $val, $k, $v);
168 | }
169 |
170 | $url->setQuery($query);
171 | return [$post, $cookies];
172 | }
173 |
174 |
175 | private function getFiles(): array
176 | {
177 | $reChars = '#^[' . self::ValidChars . ']*+$#Du';
178 | $files = [];
179 | $list = [];
180 | foreach ($_FILES ?? [] as $k => $v) {
181 | if (
182 | !is_array($v)
183 | || !isset($v['name'], $v['type'], $v['size'], $v['tmp_name'], $v['error'])
184 | || (!$this->binary && is_string($k) && (!preg_match($reChars, $k) || preg_last_error()))
185 | ) {
186 | continue;
187 | }
188 |
189 | $v['@'] = &$files[$k];
190 | $list[] = $v;
191 | }
192 |
193 | // create FileUpload objects
194 | foreach ($list as &$v) {
195 | if (!isset($v['name'])) {
196 | continue;
197 |
198 | } elseif (!is_array($v['name'])) {
199 | if (!$this->binary && (!preg_match($reChars, $v['name']) || preg_last_error())) {
200 | $v['name'] = '';
201 | }
202 |
203 | if ($v['error'] !== UPLOAD_ERR_NO_FILE) {
204 | $v['@'] = new FileUpload($v);
205 | }
206 |
207 | continue;
208 | }
209 |
210 | foreach ($v['name'] as $k => $foo) {
211 | if (!$this->binary && is_string($k) && (!preg_match($reChars, $k) || preg_last_error())) {
212 | continue;
213 | }
214 |
215 | $list[] = [
216 | 'name' => $v['name'][$k],
217 | 'type' => $v['type'][$k],
218 | 'size' => $v['size'][$k],
219 | 'full_path' => $v['full_path'][$k] ?? null,
220 | 'tmp_name' => $v['tmp_name'][$k],
221 | 'error' => $v['error'][$k],
222 | '@' => &$v['@'][$k],
223 | ];
224 | }
225 | }
226 |
227 | return $files;
228 | }
229 |
230 |
231 | private function getHeaders(): array
232 | {
233 | if (function_exists('apache_request_headers')) {
234 | $headers = apache_request_headers();
235 | } else {
236 | $headers = [];
237 | foreach ($_SERVER as $k => $v) {
238 | if (strncmp($k, 'HTTP_', 5) === 0) {
239 | $k = substr($k, 5);
240 | } elseif (strncmp($k, 'CONTENT_', 8)) {
241 | continue;
242 | }
243 |
244 | $headers[strtr($k, '_', '-')] = $v;
245 | }
246 | }
247 |
248 | if (!isset($headers['Authorization'])) {
249 | if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
250 | $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW']);
251 | } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) {
252 | $headers['Authorization'] = 'Digest ' . $_SERVER['PHP_AUTH_DIGEST'];
253 | }
254 | }
255 |
256 | return $headers;
257 | }
258 |
259 |
260 | private function getMethod(): string
261 | {
262 | $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
263 | if (
264 | $method === 'POST'
265 | && preg_match('#^[A-Z]+$#D', $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ?? '')
266 | ) {
267 | $method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
268 | }
269 |
270 | return $method;
271 | }
272 |
273 |
274 | private function getClient(Url $url): array
275 | {
276 | $remoteAddr = !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
277 |
278 | // use real client address and host if trusted proxy is used
279 | $usingTrustedProxy = $remoteAddr && Arrays::some($this->proxies, fn(string $proxy): bool => Helpers::ipMatch($remoteAddr, $proxy));
280 | if ($usingTrustedProxy) {
281 | $remoteHost = null;
282 | $remoteAddr = empty($_SERVER['HTTP_FORWARDED'])
283 | ? $this->useNonstandardProxy($url)
284 | : $this->useForwardedProxy($url);
285 |
286 | } else {
287 | $remoteHost = !empty($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : null;
288 | }
289 |
290 | return [$remoteAddr, $remoteHost];
291 | }
292 |
293 |
294 | private function useForwardedProxy(Url $url): ?string
295 | {
296 | $forwardParams = preg_split('/[,;]/', $_SERVER['HTTP_FORWARDED']);
297 | foreach ($forwardParams as $forwardParam) {
298 | [$key, $value] = explode('=', $forwardParam, 2) + [1 => ''];
299 | $proxyParams[strtolower(trim($key))][] = trim($value, " \t\"");
300 | }
301 |
302 | if (isset($proxyParams['for'])) {
303 | $address = $proxyParams['for'][0];
304 | $remoteAddr = str_contains($address, '[')
305 | ? substr($address, 1, strpos($address, ']') - 1) // IPv6
306 | : explode(':', $address)[0]; // IPv4
307 | }
308 |
309 | if (isset($proxyParams['proto']) && count($proxyParams['proto']) === 1) {
310 | $url->setScheme(strcasecmp($proxyParams['proto'][0], 'https') === 0 ? 'https' : 'http');
311 | $url->setPort($url->getScheme() === 'https' ? 443 : 80);
312 | }
313 |
314 | if (
315 | isset($proxyParams['host']) && count($proxyParams['host']) === 1
316 | && ($pair = $this->parseHostAndPort($proxyParams['host'][0]))
317 | ) {
318 | $url->setHost($pair[0]);
319 | if (isset($pair[1])) {
320 | $url->setPort($pair[1]);
321 | }
322 | }
323 | return $remoteAddr ?? null;
324 | }
325 |
326 |
327 | private function useNonstandardProxy(Url $url): ?string
328 | {
329 | if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
330 | $url->setScheme(strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0 ? 'https' : 'http');
331 | $url->setPort($url->getScheme() === 'https' ? 443 : 80);
332 | }
333 |
334 | if (!empty($_SERVER['HTTP_X_FORWARDED_PORT'])) {
335 | $url->setPort((int) $_SERVER['HTTP_X_FORWARDED_PORT']);
336 | }
337 |
338 | if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
339 | $xForwardedForWithoutProxies = array_filter(
340 | explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']),
341 | fn(string $ip): bool => filter_var($ip = trim($ip), FILTER_VALIDATE_IP) === false
342 | || !Arrays::some($this->proxies, fn(string $proxy): bool => Helpers::ipMatch($ip, $proxy)),
343 | );
344 | if ($xForwardedForWithoutProxies) {
345 | $remoteAddr = trim(end($xForwardedForWithoutProxies));
346 | $xForwardedForRealIpKey = key($xForwardedForWithoutProxies);
347 | }
348 | }
349 |
350 | if (isset($xForwardedForRealIpKey) && !empty($_SERVER['HTTP_X_FORWARDED_HOST'])) {
351 | $xForwardedHost = explode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);
352 | if (
353 | isset($xForwardedHost[$xForwardedForRealIpKey])
354 | && ($pair = $this->parseHostAndPort(trim($xForwardedHost[$xForwardedForRealIpKey])))
355 | ) {
356 | $url->setHost($pair[0]);
357 | if (isset($pair[1])) {
358 | $url->setPort($pair[1]);
359 | }
360 | }
361 | }
362 |
363 | return $remoteAddr ?? null;
364 | }
365 |
366 |
367 | /** @return array{string, ?int}|null */
368 | private function parseHostAndPort(string $s): ?array
369 | {
370 | return preg_match('#^([a-z0-9_.-]+|\[[a-f0-9:]+])(:\d+)?$#Di', $s, $matches)
371 | ? [
372 | rtrim(strtolower($matches[1]), '.'),
373 | isset($matches[2]) ? (int) substr($matches[2], 1) : null,
374 | ]
375 | : null;
376 | }
377 |
378 |
379 | /** @deprecated use fromGlobals() */
380 | public function createHttpRequest(): Request
381 | {
382 | trigger_error(__METHOD__ . '() is deprecated, use fromGlobals()', E_USER_DEPRECATED);
383 | return $this->fromGlobals();
384 | }
385 | }
386 |
--------------------------------------------------------------------------------
/src/Http/Url.php:
--------------------------------------------------------------------------------
1 |
21 | * scheme user password host port path query fragment
22 | * | | | | | | | |
23 | * /--\ /--\ /------\ /-------\ /--\/------------\ /--------\ /------\
24 | * http://john:x0y17575@nette.org:8042/en/manual.php?name=param#fragment <-- absoluteUrl
25 | * \______\__________________________/
26 | * | |
27 | * hostUrl authority
28 | *
29 | *
30 | * @property string $scheme
31 | * @property string $user
32 | * @property string $password
33 | * @property string $host
34 | * @property int $port
35 | * @property string $path
36 | * @property string $query
37 | * @property string $fragment
38 | * @property-read string $absoluteUrl
39 | * @property-read string $authority
40 | * @property-read string $hostUrl
41 | * @property-read string $basePath
42 | * @property-read string $baseUrl
43 | * @property-read string $relativeUrl
44 | * @property-read array $queryParameters
45 | */
46 | class Url implements \JsonSerializable
47 | {
48 | use Nette\SmartObject;
49 |
50 | public static array $defaultPorts = [
51 | 'http' => 80,
52 | 'https' => 443,
53 | 'ftp' => 21,
54 | ];
55 |
56 | private string $scheme = '';
57 | private string $user = '';
58 | private string $password = '';
59 | private string $host = '';
60 | private ?int $port = null;
61 | private string $path = '';
62 | private array $query = [];
63 | private string $fragment = '';
64 |
65 |
66 | /**
67 | * @throws Nette\InvalidArgumentException if URL is malformed
68 | */
69 | public function __construct(string|self|UrlImmutable|null $url = null)
70 | {
71 | if (is_string($url)) {
72 | $p = @parse_url($url); // @ - is escalated to exception
73 | if ($p === false) {
74 | throw new Nette\InvalidArgumentException("Malformed or unsupported URI '$url'.");
75 | }
76 |
77 | $this->scheme = $p['scheme'] ?? '';
78 | $this->port = $p['port'] ?? null;
79 | $this->host = rawurldecode($p['host'] ?? '');
80 | $this->user = rawurldecode($p['user'] ?? '');
81 | $this->password = rawurldecode($p['pass'] ?? '');
82 | $this->setPath($p['path'] ?? '');
83 | $this->setQuery($p['query'] ?? []);
84 | $this->fragment = rawurldecode($p['fragment'] ?? '');
85 |
86 | } elseif ($url instanceof UrlImmutable || $url instanceof self) {
87 | [$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment] = $url->export();
88 | }
89 | }
90 |
91 |
92 | public function setScheme(string $scheme): static
93 | {
94 | $this->scheme = $scheme;
95 | return $this;
96 | }
97 |
98 |
99 | public function getScheme(): string
100 | {
101 | return $this->scheme;
102 | }
103 |
104 |
105 | /** @deprecated */
106 | public function setUser(string $user): static
107 | {
108 | trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED);
109 | $this->user = $user;
110 | return $this;
111 | }
112 |
113 |
114 | /** @deprecated */
115 | public function getUser(): string
116 | {
117 | trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED);
118 | return $this->user;
119 | }
120 |
121 |
122 | /** @deprecated */
123 | public function setPassword(string $password): static
124 | {
125 | trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED);
126 | $this->password = $password;
127 | return $this;
128 | }
129 |
130 |
131 | /** @deprecated */
132 | public function getPassword(): string
133 | {
134 | trigger_error(__METHOD__ . '() is deprecated', E_USER_DEPRECATED);
135 | return $this->password;
136 | }
137 |
138 |
139 | public function setHost(string $host): static
140 | {
141 | $this->host = $host;
142 | $this->setPath($this->path);
143 | return $this;
144 | }
145 |
146 |
147 | public function getHost(): string
148 | {
149 | return $this->host;
150 | }
151 |
152 |
153 | /**
154 | * Returns the part of domain.
155 | */
156 | public function getDomain(int $level = 2): string
157 | {
158 | $parts = ip2long($this->host)
159 | ? [$this->host]
160 | : explode('.', $this->host);
161 | $parts = $level >= 0
162 | ? array_slice($parts, -$level)
163 | : array_slice($parts, 0, $level);
164 | return implode('.', $parts);
165 | }
166 |
167 |
168 | public function setPort(int $port): static
169 | {
170 | $this->port = $port;
171 | return $this;
172 | }
173 |
174 |
175 | public function getPort(): ?int
176 | {
177 | return $this->port ?: $this->getDefaultPort();
178 | }
179 |
180 |
181 | public function getDefaultPort(): ?int
182 | {
183 | return self::$defaultPorts[$this->scheme] ?? null;
184 | }
185 |
186 |
187 | public function setPath(string $path): static
188 | {
189 | $this->path = $path;
190 | if ($this->host && !str_starts_with($this->path, '/')) {
191 | $this->path = '/' . $this->path;
192 | }
193 |
194 | return $this;
195 | }
196 |
197 |
198 | public function getPath(): string
199 | {
200 | return $this->path;
201 | }
202 |
203 |
204 | public function setQuery(string|array $query): static
205 | {
206 | $this->query = is_array($query) ? $query : self::parseQuery($query);
207 | return $this;
208 | }
209 |
210 |
211 | public function appendQuery(string|array $query): static
212 | {
213 | $this->query = is_array($query)
214 | ? $query + $this->query
215 | : self::parseQuery($this->getQuery() . '&' . $query);
216 | return $this;
217 | }
218 |
219 |
220 | public function getQuery(): string
221 | {
222 | return http_build_query($this->query, '', '&', PHP_QUERY_RFC3986);
223 | }
224 |
225 |
226 | public function getQueryParameters(): array
227 | {
228 | return $this->query;
229 | }
230 |
231 |
232 | public function getQueryParameter(string $name): mixed
233 | {
234 | return $this->query[$name] ?? null;
235 | }
236 |
237 |
238 | public function setQueryParameter(string $name, mixed $value): static
239 | {
240 | $this->query[$name] = $value;
241 | return $this;
242 | }
243 |
244 |
245 | public function setFragment(string $fragment): static
246 | {
247 | $this->fragment = $fragment;
248 | return $this;
249 | }
250 |
251 |
252 | public function getFragment(): string
253 | {
254 | return $this->fragment;
255 | }
256 |
257 |
258 | public function getAbsoluteUrl(): string
259 | {
260 | return $this->getHostUrl() . $this->path
261 | . (($tmp = $this->getQuery()) ? '?' . $tmp : '')
262 | . ($this->fragment === '' ? '' : '#' . $this->fragment);
263 | }
264 |
265 |
266 | /**
267 | * Returns the [user[:pass]@]host[:port] part of URI.
268 | */
269 | public function getAuthority(): string
270 | {
271 | return $this->host === ''
272 | ? ''
273 | : ($this->user !== ''
274 | ? rawurlencode($this->user) . ($this->password === '' ? '' : ':' . rawurlencode($this->password)) . '@'
275 | : '')
276 | . $this->host
277 | . ($this->port && $this->port !== $this->getDefaultPort()
278 | ? ':' . $this->port
279 | : '');
280 | }
281 |
282 |
283 | /**
284 | * Returns the scheme and authority part of URI.
285 | */
286 | public function getHostUrl(): string
287 | {
288 | return ($this->scheme ? $this->scheme . ':' : '')
289 | . (($authority = $this->getAuthority()) !== '' ? '//' . $authority : '');
290 | }
291 |
292 |
293 | /** @deprecated use UrlScript::getBasePath() instead */
294 | public function getBasePath(): string
295 | {
296 | trigger_error(__METHOD__ . '() is deprecated, use UrlScript object', E_USER_DEPRECATED);
297 | $pos = strrpos($this->path, '/');
298 | return $pos === false ? '' : substr($this->path, 0, $pos + 1);
299 | }
300 |
301 |
302 | /** @deprecated use UrlScript::getBaseUrl() instead */
303 | public function getBaseUrl(): string
304 | {
305 | trigger_error(__METHOD__ . '() is deprecated, use UrlScript object', E_USER_DEPRECATED);
306 | return $this->getHostUrl() . $this->getBasePath();
307 | }
308 |
309 |
310 | /** @deprecated use UrlScript::getRelativeUrl() instead */
311 | public function getRelativeUrl(): string
312 | {
313 | trigger_error(__METHOD__ . '() is deprecated, use UrlScript object', E_USER_DEPRECATED);
314 | return substr($this->getAbsoluteUrl(), strlen($this->getBaseUrl()));
315 | }
316 |
317 |
318 | /**
319 | * URL comparison.
320 | */
321 | public function isEqual(string|self|UrlImmutable $url): bool
322 | {
323 | $url = new self($url);
324 | $query = $url->query;
325 | ksort($query);
326 | $query2 = $this->query;
327 | ksort($query2);
328 | $host = rtrim($url->host, '.');
329 | $host2 = rtrim($this->host, '.');
330 | return $url->scheme === $this->scheme
331 | && (!strcasecmp($host, $host2)
332 | || self::idnHostToUnicode($host) === self::idnHostToUnicode($host2))
333 | && $url->getPort() === $this->getPort()
334 | && $url->user === $this->user
335 | && $url->password === $this->password
336 | && self::unescape($url->path, '%/') === self::unescape($this->path, '%/')
337 | && $query === $query2
338 | && $url->fragment === $this->fragment;
339 | }
340 |
341 |
342 | /**
343 | * Transforms URL to canonical form.
344 | */
345 | public function canonicalize(): static
346 | {
347 | $this->path = preg_replace_callback(
348 | '#[^!$&\'()*+,/:;=@%"]+#',
349 | fn(array $m): string => rawurlencode($m[0]),
350 | self::unescape($this->path, '%/'),
351 | );
352 | $this->host = rtrim($this->host, '.');
353 | $this->host = self::idnHostToUnicode(strtolower($this->host));
354 | return $this;
355 | }
356 |
357 |
358 | public function __toString(): string
359 | {
360 | return $this->getAbsoluteUrl();
361 | }
362 |
363 |
364 | public function jsonSerialize(): string
365 | {
366 | return $this->getAbsoluteUrl();
367 | }
368 |
369 |
370 | /** @internal */
371 | final public function export(): array
372 | {
373 | return [$this->scheme, $this->user, $this->password, $this->host, $this->port, $this->path, $this->query, $this->fragment];
374 | }
375 |
376 |
377 | /**
378 | * Converts IDN ASCII host to UTF-8.
379 | */
380 | private static function idnHostToUnicode(string $host): string
381 | {
382 | if (!str_contains($host, '--')) { // host does not contain IDN
383 | return $host;
384 | }
385 |
386 | if (function_exists('idn_to_utf8') && defined('INTL_IDNA_VARIANT_UTS46')) {
387 | return idn_to_utf8($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46) ?: $host;
388 | }
389 |
390 | trigger_error('PHP extension intl is not loaded or is too old', E_USER_WARNING);
391 | }
392 |
393 |
394 | /**
395 | * Similar to rawurldecode, but preserves reserved chars encoded.
396 | */
397 | public static function unescape(string $s, string $reserved = '%;/?:@&=+$,'): string
398 | {
399 | // reserved (@see RFC 2396) = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
400 | // within a path segment, the characters "/", ";", "=", "?" are reserved
401 | // within a query component, the characters ";", "/", "?", ":", "@", "&", "=", "+", ",", "$" are reserved.
402 | if ($reserved !== '') {
403 | $s = preg_replace_callback(
404 | '#%(' . substr(chunk_split(bin2hex($reserved), 2, '|'), 0, -1) . ')#i',
405 | fn(array $m): string => '%25' . strtoupper($m[1]),
406 | $s,
407 | );
408 | }
409 |
410 | return rawurldecode($s);
411 | }
412 |
413 |
414 | /**
415 | * Parses query string. Is affected by directive arg_separator.input.
416 | */
417 | public static function parseQuery(string $s): array
418 | {
419 | $s = str_replace(['%5B', '%5b'], '[', $s);
420 | $sep = preg_quote(ini_get('arg_separator.input'));
421 | $s = preg_replace("#([$sep])([^[$sep=]+)([^$sep]*)#", '&0[$2]$3', '&' . $s);
422 | parse_str($s, $res);
423 | return $res[0] ?? [];
424 | }
425 |
426 |
427 | /**
428 | * Determines if URL is absolute, ie if it starts with a scheme followed by colon.
429 | */
430 | public static function isAbsolute(string $url): bool
431 | {
432 | return (bool) preg_match('#^[a-z][a-z0-9+.-]*:#i', $url);
433 | }
434 |
435 |
436 | /**
437 | * Normalizes a path by handling and removing relative path references like '.', '..' and directory traversal.
438 | */
439 | public static function removeDotSegments(string $path): string
440 | {
441 | $prefix = $segment = '';
442 | if (str_starts_with($path, '/')) {
443 | $prefix = '/';
444 | $path = substr($path, 1);
445 | }
446 | $segments = explode('/', $path);
447 | $res = [];
448 | foreach ($segments as $segment) {
449 | if ($segment === '..') {
450 | array_pop($res);
451 | } elseif ($segment !== '.') {
452 | $res[] = $segment;
453 | }
454 | }
455 |
456 | if ($segment === '.' || $segment === '..') {
457 | $res[] = '';
458 | }
459 | return $prefix . implode('/', $res);
460 | }
461 | }
462 |
--------------------------------------------------------------------------------
/src/Http/IResponse.php:
--------------------------------------------------------------------------------
1 | 'Continue',
83 | 101 => 'Switching Protocols',
84 | 102 => 'Processing',
85 | 200 => 'OK',
86 | 201 => 'Created',
87 | 202 => 'Accepted',
88 | 203 => 'Non-Authoritative Information',
89 | 204 => 'No Content',
90 | 205 => 'Reset Content',
91 | 206 => 'Partial Content',
92 | 207 => 'Multi-status',
93 | 208 => 'Already Reported',
94 | 226 => 'IM Used',
95 | 300 => 'Multiple Choices',
96 | 301 => 'Moved Permanently',
97 | 302 => 'Found',
98 | 303 => 'See Other',
99 | 304 => 'Not Modified',
100 | 305 => 'Use Proxy',
101 | 307 => 'Temporary Redirect',
102 | 308 => 'Permanent Redirect',
103 | 400 => 'Bad Request',
104 | 401 => 'Unauthorized',
105 | 402 => 'Payment Required',
106 | 403 => 'Forbidden',
107 | 404 => 'Not Found',
108 | 405 => 'Method Not Allowed',
109 | 406 => 'Not Acceptable',
110 | 407 => 'Proxy Authentication Required',
111 | 408 => 'Request Time-out',
112 | 409 => 'Conflict',
113 | 410 => 'Gone',
114 | 411 => 'Length Required',
115 | 412 => 'Precondition Failed',
116 | 413 => 'Request Entity Too Large',
117 | 414 => 'Request-URI Too Large',
118 | 415 => 'Unsupported Media Type',
119 | 416 => 'Requested range not satisfiable',
120 | 417 => 'Expectation Failed',
121 | 421 => 'Misdirected Request',
122 | 422 => 'Unprocessable Entity',
123 | 423 => 'Locked',
124 | 424 => 'Failed Dependency',
125 | 426 => 'Upgrade Required',
126 | 428 => 'Precondition Required',
127 | 429 => 'Too Many Requests',
128 | 431 => 'Request Header Fields Too Large',
129 | 451 => 'Unavailable For Legal Reasons',
130 | 500 => 'Internal Server Error',
131 | 501 => 'Not Implemented',
132 | 502 => 'Bad Gateway',
133 | 503 => 'Service Unavailable',
134 | 504 => 'Gateway Time-out',
135 | 505 => 'HTTP Version not supported',
136 | 506 => 'Variant Also Negotiates',
137 | 507 => 'Insufficient Storage',
138 | 508 => 'Loop Detected',
139 | 510 => 'Not Extended',
140 | 511 => 'Network Authentication Required',
141 | ];
142 |
143 | /** SameSite cookie */
144 | public const
145 | SameSiteLax = 'Lax',
146 | SameSiteStrict = 'Strict',
147 | SameSiteNone = 'None';
148 |
149 | /** @deprecated use IResponse::ReasonPhrases */
150 | public const REASON_PHRASES = self::ReasonPhrases;
151 |
152 | /** @deprecated use IResponse::SameSiteLax */
153 | public const SAME_SITE_LAX = self::SameSiteLax;
154 |
155 | /** @deprecated use IResponse::SameSiteStrict */
156 | public const SAME_SITE_STRICT = self::SameSiteStrict;
157 |
158 | /** @deprecated use IResponse::SameSiteNone */
159 | public const SAME_SITE_NONE = self::SameSiteNone;
160 |
161 | /** @deprecated use IResponse::S100_Continue */
162 | public const S100_CONTINUE = self::S100_Continue;
163 |
164 | /** @deprecated use IResponse::S101_SwitchingProtocols */
165 | public const S101_SWITCHING_PROTOCOLS = self::S101_SwitchingProtocols;
166 |
167 | /** @deprecated use IResponse::S102_Processing */
168 | public const S102_PROCESSING = self::S102_Processing;
169 |
170 | /** @deprecated use IResponse::S201_Created */
171 | public const S201_CREATED = self::S201_Created;
172 |
173 | /** @deprecated use IResponse::S202_Accepted */
174 | public const S202_ACCEPTED = self::S202_Accepted;
175 |
176 | /** @deprecated use IResponse::S203_NonAuthoritativeInformation */
177 | public const S203_NON_AUTHORITATIVE_INFORMATION = self::S203_NonAuthoritativeInformation;
178 |
179 | /** @deprecated use IResponse::S204_NoContent */
180 | public const S204_NO_CONTENT = self::S204_NoContent;
181 |
182 | /** @deprecated use IResponse::S205_ResetContent */
183 | public const S205_RESET_CONTENT = self::S205_ResetContent;
184 |
185 | /** @deprecated use IResponse::S206_PartialContent */
186 | public const S206_PARTIAL_CONTENT = self::S206_PartialContent;
187 |
188 | /** @deprecated use IResponse::S207_MultiStatus */
189 | public const S207_MULTI_STATUS = self::S207_MultiStatus;
190 |
191 | /** @deprecated use IResponse::S208_AlreadyReported */
192 | public const S208_ALREADY_REPORTED = self::S208_AlreadyReported;
193 |
194 | /** @deprecated use IResponse::S226_ImUsed */
195 | public const S226_IM_USED = self::S226_ImUsed;
196 |
197 | /** @deprecated use IResponse::S300_MultipleChoices */
198 | public const S300_MULTIPLE_CHOICES = self::S300_MultipleChoices;
199 |
200 | /** @deprecated use IResponse::S301_MovedPermanently */
201 | public const S301_MOVED_PERMANENTLY = self::S301_MovedPermanently;
202 |
203 | /** @deprecated use IResponse::S302_Found */
204 | public const S302_FOUND = self::S302_Found;
205 |
206 | /** @deprecated use IResponse::S303_PostGet */
207 | public const S303_SEE_OTHER = self::S303_PostGet;
208 |
209 | /** @deprecated use IResponse::S303_PostGet */
210 | public const S303_POST_GET = self::S303_PostGet;
211 |
212 | /** @deprecated use IResponse::S304_NotModified */
213 | public const S304_NOT_MODIFIED = self::S304_NotModified;
214 |
215 | /** @deprecated use IResponse::S305_UseProxy */
216 | public const S305_USE_PROXY = self::S305_UseProxy;
217 |
218 | /** @deprecated use IResponse::S307_TemporaryRedirect */
219 | public const S307_TEMPORARY_REDIRECT = self::S307_TemporaryRedirect;
220 |
221 | /** @deprecated use IResponse::S308_PermanentRedirect */
222 | public const S308_PERMANENT_REDIRECT = self::S308_PermanentRedirect;
223 |
224 | /** @deprecated use IResponse::S400_BadRequest */
225 | public const S400_BAD_REQUEST = self::S400_BadRequest;
226 |
227 | /** @deprecated use IResponse::S401_Unauthorized */
228 | public const S401_UNAUTHORIZED = self::S401_Unauthorized;
229 |
230 | /** @deprecated use IResponse::S402_PaymentRequired */
231 | public const S402_PAYMENT_REQUIRED = self::S402_PaymentRequired;
232 |
233 | /** @deprecated use IResponse::S403_Forbidden */
234 | public const S403_FORBIDDEN = self::S403_Forbidden;
235 |
236 | /** @deprecated use IResponse::S404_NotFound */
237 | public const S404_NOT_FOUND = self::S404_NotFound;
238 |
239 | /** @deprecated use IResponse::S405_MethodNotAllowed */
240 | public const S405_METHOD_NOT_ALLOWED = self::S405_MethodNotAllowed;
241 |
242 | /** @deprecated use IResponse::S406_NotAcceptable */
243 | public const S406_NOT_ACCEPTABLE = self::S406_NotAcceptable;
244 |
245 | /** @deprecated use IResponse::S407_ProxyAuthenticationRequired */
246 | public const S407_PROXY_AUTHENTICATION_REQUIRED = self::S407_ProxyAuthenticationRequired;
247 |
248 | /** @deprecated use IResponse::S408_RequestTimeout */
249 | public const S408_REQUEST_TIMEOUT = self::S408_RequestTimeout;
250 |
251 | /** @deprecated use IResponse::S409_Conflict */
252 | public const S409_CONFLICT = self::S409_Conflict;
253 |
254 | /** @deprecated use IResponse::S410_Gone */
255 | public const S410_GONE = self::S410_Gone;
256 |
257 | /** @deprecated use IResponse::S411_LengthRequired */
258 | public const S411_LENGTH_REQUIRED = self::S411_LengthRequired;
259 |
260 | /** @deprecated use IResponse::S412_PreconditionFailed */
261 | public const S412_PRECONDITION_FAILED = self::S412_PreconditionFailed;
262 |
263 | /** @deprecated use IResponse::S413_RequestEntityTooLarge */
264 | public const S413_REQUEST_ENTITY_TOO_LARGE = self::S413_RequestEntityTooLarge;
265 |
266 | /** @deprecated use IResponse::S414_RequestUriTooLong */
267 | public const S414_REQUEST_URI_TOO_LONG = self::S414_RequestUriTooLong;
268 |
269 | /** @deprecated use IResponse::S415_UnsupportedMediaType */
270 | public const S415_UNSUPPORTED_MEDIA_TYPE = self::S415_UnsupportedMediaType;
271 |
272 | /** @deprecated use IResponse::S416_RequestedRangeNotSatisfiable */
273 | public const S416_REQUESTED_RANGE_NOT_SATISFIABLE = self::S416_RequestedRangeNotSatisfiable;
274 |
275 | /** @deprecated use IResponse::S417_ExpectationFailed */
276 | public const S417_EXPECTATION_FAILED = self::S417_ExpectationFailed;
277 |
278 | /** @deprecated use IResponse::S421_MisdirectedRequest */
279 | public const S421_MISDIRECTED_REQUEST = self::S421_MisdirectedRequest;
280 |
281 | /** @deprecated use IResponse::S422_UnprocessableEntity */
282 | public const S422_UNPROCESSABLE_ENTITY = self::S422_UnprocessableEntity;
283 |
284 | /** @deprecated use IResponse::S423_Locked */
285 | public const S423_LOCKED = self::S423_Locked;
286 |
287 | /** @deprecated use IResponse::S424_FailedDependency */
288 | public const S424_FAILED_DEPENDENCY = self::S424_FailedDependency;
289 |
290 | /** @deprecated use IResponse::S426_UpgradeRequired */
291 | public const S426_UPGRADE_REQUIRED = self::S426_UpgradeRequired;
292 |
293 | /** @deprecated use IResponse::S428_PreconditionRequired */
294 | public const S428_PRECONDITION_REQUIRED = self::S428_PreconditionRequired;
295 |
296 | /** @deprecated use IResponse::S429_TooManyRequests */
297 | public const S429_TOO_MANY_REQUESTS = self::S429_TooManyRequests;
298 |
299 | /** @deprecated use IResponse::S431_RequestHeaderFieldsTooLarge */
300 | public const S431_REQUEST_HEADER_FIELDS_TOO_LARGE = self::S431_RequestHeaderFieldsTooLarge;
301 |
302 | /** @deprecated use IResponse::S451_UnavailableForLegalReasons */
303 | public const S451_UNAVAILABLE_FOR_LEGAL_REASONS = self::S451_UnavailableForLegalReasons;
304 |
305 | /** @deprecated use IResponse::S500_InternalServerError */
306 | public const S500_INTERNAL_SERVER_ERROR = self::S500_InternalServerError;
307 |
308 | /** @deprecated use IResponse::S501_NotImplemented */
309 | public const S501_NOT_IMPLEMENTED = self::S501_NotImplemented;
310 |
311 | /** @deprecated use IResponse::S502_BadGateway */
312 | public const S502_BAD_GATEWAY = self::S502_BadGateway;
313 |
314 | /** @deprecated use IResponse::S503_ServiceUnavailable */
315 | public const S503_SERVICE_UNAVAILABLE = self::S503_ServiceUnavailable;
316 |
317 | /** @deprecated use IResponse::S504_GatewayTimeout */
318 | public const S504_GATEWAY_TIMEOUT = self::S504_GatewayTimeout;
319 |
320 | /** @deprecated use IResponse::S505_HttpVersionNotSupported */
321 | public const S505_HTTP_VERSION_NOT_SUPPORTED = self::S505_HttpVersionNotSupported;
322 |
323 | /** @deprecated use IResponse::S506_VariantAlsoNegotiates */
324 | public const S506_VARIANT_ALSO_NEGOTIATES = self::S506_VariantAlsoNegotiates;
325 |
326 | /** @deprecated use IResponse::S507_InsufficientStorage */
327 | public const S507_INSUFFICIENT_STORAGE = self::S507_InsufficientStorage;
328 |
329 | /** @deprecated use IResponse::S508_LoopDetected */
330 | public const S508_LOOP_DETECTED = self::S508_LoopDetected;
331 |
332 | /** @deprecated use IResponse::S510_NotExtended */
333 | public const S510_NOT_EXTENDED = self::S510_NotExtended;
334 |
335 | /** @deprecated use IResponse::S511_NetworkAuthenticationRequired */
336 | public const S511_NETWORK_AUTHENTICATION_REQUIRED = self::S511_NetworkAuthenticationRequired;
337 |
338 | /**
339 | * Sets HTTP response code.
340 | */
341 | function setCode(int $code, ?string $reason = null): static;
342 |
343 | /**
344 | * Returns HTTP response code.
345 | */
346 | function getCode(): int;
347 |
348 | /**
349 | * Sends a HTTP header and replaces a previous one.
350 | */
351 | function setHeader(string $name, string $value): static;
352 |
353 | /**
354 | * Adds HTTP header.
355 | */
356 | function addHeader(string $name, string $value): static;
357 |
358 | /**
359 | * Sends a Content-type HTTP header.
360 | */
361 | function setContentType(string $type, ?string $charset = null): static;
362 |
363 | /**
364 | * Redirects to a new URL.
365 | */
366 | function redirect(string $url, int $code = self::S302_Found): void;
367 |
368 | /**
369 | * Sets the time (like '20 minutes') before a page cached on a browser expires, null means "must-revalidate".
370 | */
371 | function setExpiration(?string $expire): static;
372 |
373 | /**
374 | * Checks if headers have been sent.
375 | */
376 | function isSent(): bool;
377 |
378 | /**
379 | * Returns value of an HTTP header.
380 | */
381 | function getHeader(string $header): ?string;
382 |
383 | /**
384 | * Returns an associative array of headers to sent.
385 | */
386 | function getHeaders(): array;
387 |
388 | /**
389 | * Sends a cookie.
390 | */
391 | function setCookie(
392 | string $name,
393 | string $value,
394 | string|int|\DateTimeInterface|null $expire,
395 | ?string $path = null,
396 | ?string $domain = null,
397 | bool $secure = false,
398 | bool $httpOnly = true,
399 | string $sameSite = self::SameSiteLax,
400 | ): static;
401 |
402 | /**
403 | * Deletes a cookie.
404 | */
405 | function deleteCookie(
406 | string $name,
407 | ?string $path = null,
408 | ?string $domain = null,
409 | bool $secure = false,
410 | );
411 | }
412 |
--------------------------------------------------------------------------------
/src/Http/Session.php:
--------------------------------------------------------------------------------
1 | '', // must be disabled because PHP implementation is invalid
27 | 'use_cookies' => 1, // must be enabled to prevent Session Hijacking and Fixation
28 | 'use_only_cookies' => 1, // must be enabled to prevent Session Fixation
29 | 'use_trans_sid' => 0, // must be disabled to prevent Session Hijacking and Fixation
30 | 'use_strict_mode' => 1, // must be enabled to prevent Session Fixation
31 | 'cookie_httponly' => true, // must be enabled to prevent Session Hijacking
32 | ];
33 |
34 | /** @var array Occurs when the session is started */
35 | public array $onStart = [];
36 |
37 | /** @var array Occurs before the session is written to disk */
38 | public array $onBeforeWrite = [];
39 |
40 | private bool $regenerated = false;
41 | private bool $started = false;
42 |
43 | /** default configuration */
44 | private array $options = [
45 | 'cookie_samesite' => IResponse::SameSiteLax,
46 | 'cookie_lifetime' => 0, // for a maximum of 3 hours or until the browser is closed
47 | 'gc_maxlifetime' => self::DefaultFileLifetime, // 3 hours
48 | ];
49 |
50 | private readonly IRequest $request;
51 | private readonly IResponse $response;
52 | private ?\SessionHandlerInterface $handler = null;
53 | private bool $readAndClose = false;
54 | private bool $fileExists = true;
55 | private bool $autoStart = true;
56 |
57 |
58 | public function __construct(IRequest $request, IResponse $response)
59 | {
60 | $this->request = $request;
61 | $this->response = $response;
62 | $this->options['cookie_path'] = &$this->response->cookiePath;
63 | $this->options['cookie_domain'] = &$this->response->cookieDomain;
64 | $this->options['cookie_secure'] = &$this->response->cookieSecure;
65 | }
66 |
67 |
68 | /**
69 | * Starts and initializes session data.
70 | * @throws Nette\InvalidStateException
71 | */
72 | public function start(): void
73 | {
74 | $this->doStart();
75 | }
76 |
77 |
78 | private function doStart($mustExists = false): void
79 | {
80 | if (session_status() === PHP_SESSION_ACTIVE) { // adapt an existing session
81 | if (!$this->started) {
82 | $this->configure(self::SecurityOptions);
83 | $this->initialize();
84 | }
85 |
86 | return;
87 | }
88 |
89 | $this->configure(self::SecurityOptions + $this->options);
90 |
91 | if (!session_id()) { // session is started for first time
92 | $id = $this->request->getCookie(session_name());
93 | $id = is_string($id) && preg_match('#^[0-9a-zA-Z,-]{22,256}$#Di', $id)
94 | ? $id
95 | : session_create_id();
96 | session_id($id); // causes resend of a cookie to make sure it has the right parameters
97 | }
98 |
99 | try {
100 | // session_start returns false on failure only sometimes (even in PHP >= 7.1)
101 | Nette\Utils\Callback::invokeSafe(
102 | 'session_start',
103 | [['read_and_close' => $this->readAndClose]],
104 | function (string $message) use (&$e): void {
105 | $e = new Nette\InvalidStateException($message, previous: $e);
106 | },
107 | );
108 | } catch (\Throwable $e) {
109 | }
110 |
111 | if ($e) {
112 | @session_write_close(); // this is needed
113 | throw $e;
114 | }
115 |
116 | if ($mustExists && $this->request->getCookie(session_name()) !== session_id()) {
117 | // PHP regenerated the ID which means that the session did not exist and cookie was invalid
118 | $this->destroy();
119 | return;
120 | }
121 |
122 | $this->initialize();
123 | Nette\Utils\Arrays::invoke($this->onStart, $this);
124 | }
125 |
126 |
127 | /** @internal */
128 | public function autoStart(bool $forWrite): void
129 | {
130 | if ($this->started || (!$forWrite && !$this->exists())) {
131 | return;
132 |
133 | } elseif (!$this->autoStart) {
134 | trigger_error('Cannot auto-start session because autostarting is disabled', E_USER_WARNING);
135 | return;
136 | }
137 |
138 | $this->doStart(!$forWrite);
139 | }
140 |
141 |
142 | private function initialize(): void
143 | {
144 | $this->started = true;
145 | $this->fileExists = true;
146 |
147 | /* structure:
148 | __NF: Data, Meta, Time
149 | DATA: section->variable = data
150 | META: section->variable = Timestamp
151 | */
152 | $nf = &$_SESSION['__NF'];
153 |
154 | if (!is_array($nf)) {
155 | $nf = [];
156 | }
157 |
158 | // regenerate empty session
159 | if (empty($nf['Time']) && !$this->readAndClose) {
160 | $nf['Time'] = time();
161 | if ($this->request->getCookie(session_name()) === session_id()) {
162 | // ensures that the session was created with use_strict_mode (ie by Nette)
163 | $this->regenerateId();
164 | }
165 | }
166 |
167 | // expire section variables
168 | $now = time();
169 | foreach ($nf['META'] ?? [] as $section => $metadata) {
170 | foreach ($metadata ?? [] as $variable => $value) {
171 | if (!empty($value['T']) && $now > $value['T']) {
172 | if ($variable === '') { // expire whole section
173 | unset($nf['META'][$section], $nf['DATA'][$section]);
174 | continue 2;
175 | }
176 |
177 | unset($nf['META'][$section][$variable], $nf['DATA'][$section][$variable]);
178 | }
179 | }
180 | }
181 | }
182 |
183 |
184 | public function __destruct()
185 | {
186 | $this->clean();
187 | }
188 |
189 |
190 | /**
191 | * Has been session started?
192 | */
193 | public function isStarted(): bool
194 | {
195 | return $this->started && session_status() === PHP_SESSION_ACTIVE;
196 | }
197 |
198 |
199 | /**
200 | * Ends the current session and store session data.
201 | */
202 | public function close(): void
203 | {
204 | if (session_status() === PHP_SESSION_ACTIVE) {
205 | $this->clean();
206 | session_write_close();
207 | $this->started = false;
208 | }
209 | }
210 |
211 |
212 | /**
213 | * Destroys all data registered to a session.
214 | */
215 | public function destroy(): void
216 | {
217 | if (session_status() !== PHP_SESSION_ACTIVE) {
218 | throw new Nette\InvalidStateException('Session is not started.');
219 | }
220 |
221 | session_destroy();
222 | $_SESSION = null;
223 | $this->started = false;
224 | $this->fileExists = false;
225 | if (!$this->response->isSent()) {
226 | $params = session_get_cookie_params();
227 | $this->response->deleteCookie(session_name(), $params['path'], $params['domain'], $params['secure']);
228 | }
229 | }
230 |
231 |
232 | /**
233 | * Does session exist for the current request?
234 | */
235 | public function exists(): bool
236 | {
237 | return session_status() === PHP_SESSION_ACTIVE
238 | || ($this->fileExists && $this->request->getCookie($this->getName()));
239 | }
240 |
241 |
242 | /**
243 | * Regenerates the session ID.
244 | * @throws Nette\InvalidStateException
245 | */
246 | public function regenerateId(): void
247 | {
248 | if ($this->regenerated) {
249 | return;
250 | }
251 |
252 | if (session_status() === PHP_SESSION_ACTIVE) {
253 | if (headers_sent($file, $line)) {
254 | throw new Nette\InvalidStateException('Cannot regenerate session ID after HTTP headers have been sent' . ($file ? " (output started at $file:$line)." : '.'));
255 | }
256 |
257 | session_regenerate_id(true);
258 | } else {
259 | session_id(session_create_id());
260 | }
261 |
262 | $this->regenerated = true;
263 | }
264 |
265 |
266 | /**
267 | * Returns the current session ID. Don't make dependencies, can be changed for each request.
268 | */
269 | public function getId(): string
270 | {
271 | return session_id();
272 | }
273 |
274 |
275 | /**
276 | * Sets the session name to a specified one.
277 | */
278 | public function setName(string $name): static
279 | {
280 | if (!preg_match('#[^0-9.][^.]*$#DA', $name)) {
281 | throw new Nette\InvalidArgumentException('Session name cannot contain dot.');
282 | }
283 |
284 | session_name($name);
285 | return $this->setOptions([
286 | 'name' => $name,
287 | ]);
288 | }
289 |
290 |
291 | /**
292 | * Gets the session name.
293 | */
294 | public function getName(): string
295 | {
296 | return $this->options['name'] ?? session_name();
297 | }
298 |
299 |
300 | /********************* sections management ****************d*g**/
301 |
302 |
303 | /**
304 | * Returns specified session section.
305 | * @template T of SessionSection
306 | * @param class-string $class
307 | * @return T
308 | */
309 | public function getSection(string $section, string $class = SessionSection::class): SessionSection
310 | {
311 | return new $class($this, $section);
312 | }
313 |
314 |
315 | /**
316 | * Checks if a session section exist and is not empty.
317 | */
318 | public function hasSection(string $section): bool
319 | {
320 | if ($this->exists() && !$this->started) {
321 | $this->autoStart(false);
322 | }
323 |
324 | return !empty($_SESSION['__NF']['DATA'][$section]);
325 | }
326 |
327 |
328 | /** @return string[] */
329 | public function getSectionNames(): array
330 | {
331 | if ($this->exists() && !$this->started) {
332 | $this->autoStart(false);
333 | }
334 |
335 | return array_keys($_SESSION['__NF']['DATA'] ?? []);
336 | }
337 |
338 |
339 | /**
340 | * Cleans and minimizes meta structures.
341 | */
342 | private function clean(): void
343 | {
344 | if (!$this->isStarted()) {
345 | return;
346 | }
347 |
348 | Nette\Utils\Arrays::invoke($this->onBeforeWrite, $this);
349 |
350 | $nf = &$_SESSION['__NF'];
351 | foreach ($nf['DATA'] ?? [] as $name => $data) {
352 | foreach ($data ?? [] as $k => $v) {
353 | if ($v === null) {
354 | unset($nf['DATA'][$name][$k], $nf['META'][$name][$k]);
355 | }
356 | }
357 |
358 | if (empty($nf['DATA'][$name])) {
359 | unset($nf['DATA'][$name], $nf['META'][$name]);
360 | }
361 | }
362 |
363 | foreach ($nf['META'] ?? [] as $name => $data) {
364 | if (empty($nf['META'][$name])) {
365 | unset($nf['META'][$name]);
366 | }
367 | }
368 | }
369 |
370 |
371 | /********************* configuration ****************d*g**/
372 |
373 |
374 | /**
375 | * Sets session options.
376 | * @throws Nette\NotSupportedException
377 | * @throws Nette\InvalidStateException
378 | */
379 | public function setOptions(array $options): static
380 | {
381 | $normalized = [];
382 | $allowed = ini_get_all('session', details: false) + ['session.read_and_close' => 1];
383 |
384 | foreach ($options as $key => $value) {
385 | if (!strncmp($key, 'session.', 8)) { // back compatibility
386 | $key = substr($key, 8);
387 | }
388 |
389 | $normKey = strtolower(preg_replace('#(.)(?=[A-Z])#', '$1_', $key)); // camelCase -> snake_case
390 |
391 | if (!isset($allowed["session.$normKey"])) {
392 | $hint = substr((string) Nette\Utils\Helpers::getSuggestion(array_keys($allowed), "session.$normKey"), 8);
393 | if ($key !== $normKey) {
394 | $hint = preg_replace_callback('#_(.)#', fn($m) => strtoupper($m[1]), $hint); // snake_case -> camelCase
395 | }
396 |
397 | throw new Nette\InvalidStateException("Invalid session configuration option '$key'" . ($hint ? ", did you mean '$hint'?" : '.'));
398 | }
399 |
400 | $normalized[$normKey] = $value;
401 | }
402 |
403 | if (isset($normalized['read_and_close'])) {
404 | if (session_status() === PHP_SESSION_ACTIVE) {
405 | throw new Nette\InvalidStateException('Cannot configure "read_and_close" for already started session.');
406 | }
407 |
408 | $this->readAndClose = (bool) $normalized['read_and_close'];
409 | unset($normalized['read_and_close']);
410 | }
411 |
412 | $this->autoStart = $normalized['auto_start'] ?? true;
413 | unset($normalized['auto_start']);
414 |
415 | if (session_status() === PHP_SESSION_ACTIVE) {
416 | $this->configure($normalized);
417 | }
418 |
419 | $this->options = $normalized + $this->options;
420 | return $this;
421 | }
422 |
423 |
424 | /**
425 | * Returns all session options.
426 | */
427 | public function getOptions(): array
428 | {
429 | return $this->options;
430 | }
431 |
432 |
433 | /**
434 | * Configures session environment.
435 | */
436 | private function configure(array $config): void
437 | {
438 | $special = ['cache_expire' => 1, 'cache_limiter' => 1, 'save_path' => 1, 'name' => 1];
439 | $cookie = $origCookie = session_get_cookie_params();
440 |
441 | foreach ($config as $key => $value) {
442 | if ($value === null || ini_get("session.$key") == $value) { // intentionally ==
443 | continue;
444 |
445 | } elseif (strncmp($key, 'cookie_', 7) === 0) {
446 | $cookie[substr($key, 7)] = $value;
447 |
448 | } else {
449 | if (session_status() === PHP_SESSION_ACTIVE) {
450 | throw new Nette\InvalidStateException("Unable to set 'session.$key' to value '$value' when session has been started" . ($this->started ? '.' : ' by session.auto_start or session_start().'));
451 | }
452 |
453 | if (isset($special[$key])) {
454 | ("session_$key")($value);
455 |
456 | } elseif (function_exists('ini_set')) {
457 | ini_set("session.$key", (string) $value);
458 |
459 | } else {
460 | throw new Nette\NotSupportedException("Unable to set 'session.$key' to '$value' because function ini_set() is disabled.");
461 | }
462 | }
463 | }
464 |
465 | if ($cookie !== $origCookie) {
466 | @session_set_cookie_params($cookie); // @ may trigger warning when session is active since PHP 7.2
467 |
468 | if (session_status() === PHP_SESSION_ACTIVE) {
469 | $this->sendCookie();
470 | }
471 | }
472 |
473 | if ($this->handler) {
474 | session_set_save_handler($this->handler);
475 | }
476 | }
477 |
478 |
479 | /**
480 | * Sets the amount of time (like '20 minutes') allowed between requests before the session will be terminated,
481 | * null means "for a maximum of 3 hours or until the browser is closed".
482 | */
483 | public function setExpiration(?string $expire): static
484 | {
485 | if ($expire === null) {
486 | return $this->setOptions([
487 | 'gc_maxlifetime' => self::DefaultFileLifetime,
488 | 'cookie_lifetime' => 0,
489 | ]);
490 |
491 | } else {
492 | $expire = Nette\Utils\DateTime::from($expire)->format('U') - time();
493 | return $this->setOptions([
494 | 'gc_maxlifetime' => $expire,
495 | 'cookie_lifetime' => $expire,
496 | ]);
497 | }
498 | }
499 |
500 |
501 | /**
502 | * Sets the session cookie parameters.
503 | */
504 | public function setCookieParameters(
505 | string $path,
506 | ?string $domain = null,
507 | ?bool $secure = null,
508 | ?string $sameSite = null,
509 | ): static
510 | {
511 | return $this->setOptions([
512 | 'cookie_path' => $path,
513 | 'cookie_domain' => $domain,
514 | 'cookie_secure' => $secure,
515 | 'cookie_samesite' => $sameSite,
516 | ]);
517 | }
518 |
519 |
520 | /**
521 | * Sets path of the directory used to save session data.
522 | */
523 | public function setSavePath(string $path): static
524 | {
525 | return $this->setOptions([
526 | 'save_path' => $path,
527 | ]);
528 | }
529 |
530 |
531 | /**
532 | * Sets user session handler.
533 | */
534 | public function setHandler(\SessionHandlerInterface $handler): static
535 | {
536 | if ($this->started) {
537 | throw new Nette\InvalidStateException('Unable to set handler when session has been started.');
538 | }
539 |
540 | $this->handler = $handler;
541 | return $this;
542 | }
543 |
544 |
545 | /**
546 | * Sends the session cookies.
547 | */
548 | private function sendCookie(): void
549 | {
550 | $cookie = session_get_cookie_params();
551 | $this->response->setCookie(
552 | session_name(),
553 | session_id(),
554 | $cookie['lifetime'] ? $cookie['lifetime'] + time() : 0,
555 | $cookie['path'],
556 | $cookie['domain'],
557 | $cookie['secure'],
558 | $cookie['httponly'],
559 | $cookie['samesite'] ?? null,
560 | );
561 | }
562 | }
563 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | Nette HTTP Component
2 | ====================
3 |
4 | [](https://packagist.org/packages/nette/http)
5 | [](https://github.com/nette/http/actions)
6 | [](https://ci.appveyor.com/project/dg/http/branch/master)
7 | [](https://coveralls.io/github/nette/http?branch=master)
8 | [](https://github.com/nette/http/releases)
9 | [](https://github.com/nette/http/blob/master/license.md)
10 |
11 |
12 | Introduction
13 | ------------
14 |
15 | HTTP request and response are encapsulated in `Nette\Http\Request` and `Nette\Http\Response` objects which offer comfortable API and also act as
16 | sanitization filter.
17 |
18 | Documentation can be found on the [website](https://doc.nette.org/http-request-response).
19 |
20 |
21 | [Support Me](https://github.com/sponsors/dg)
22 | --------------------------------------------
23 |
24 | Do you like Nette DI? Are you looking forward to the new features?
25 |
26 | [](https://github.com/sponsors/dg)
27 |
28 | Thank you!
29 |
30 |
31 | Installation
32 | ------------
33 |
34 | ```shell
35 | composer require nette/http
36 | ```
37 |
38 | It requires PHP version 8.2 and supports PHP up to 8.5.
39 |
40 |
41 | HTTP Request
42 | ============
43 |
44 | An HTTP request is an [Nette\Http\Request](https://api.nette.org/3.0/Nette/Http/Request.html) object. What is important is that Nette when [creating](#RequestFactory) this object, it clears all GET, POST and COOKIE input parameters as well as URLs of control characters and invalid UTF-8 sequences. So you can safely continue working with the data. The cleaned data is then used in presenters and forms.
45 |
46 | Class `Request` is immutable. It has no setters, it has only one so-called wither `withUrl()`, which does not change the object, but returns a new instance with a modified value.
47 |
48 |
49 | withUrl(Nette\Http\UrlScript $url): Nette\Http\Request
50 | ------------------------------------------------------
51 | Returns a clone with a different URL.
52 |
53 | getUrl(): Nette\Http\UrlScript
54 | ------------------------------
55 | Returns the URL of the request as object [UrlScript|urls#UrlScript].
56 |
57 | ```php
58 | $url = $httpRequest->getUrl();
59 | echo $url; // https://nette.org/en/documentation?action=edit
60 | echo $url->getHost(); // nette.org
61 | ```
62 |
63 | Browsers do not send a fragment to the server, so `$url->getFragment()` will return an empty string.
64 |
65 | getQuery(string $key = null): string|array|null
66 | -----------------------------------------------
67 | Returns GET request parameters:
68 |
69 | ```php
70 | $all = $httpRequest->getQuery(); // array of all URL parameters
71 | $id = $httpRequest->getQuery('id'); // returns GET parameter 'id' (or null)
72 | ```
73 |
74 | getPost(string $key = null): string|array|null
75 | ----------------------------------------------
76 | Returns POST request parameters:
77 |
78 | ```php
79 | $all = $httpRequest->getPost(); // array of all POST parameters
80 | $id = $httpRequest->getPost('id'); // returns POST parameter 'id' (or null)
81 | ```
82 |
83 | getFile(string $key): Nette\Http\FileUpload|array|null
84 | ------------------------------------------------------
85 | Returns [upload](#Uploaded-Files) as object [Nette\Http\FileUpload](https://api.nette.org/3.0/Nette/Http/FileUpload.html):
86 |
87 | ```php
88 | $file = $httpRequest->getFile('avatar');
89 | if ($file->hasFile()) { // was any file uploaded?
90 | $file->getName(); // name of the file sent by user
91 | $file->getSanitizedName(); // the name without dangerous characters
92 | }
93 | ```
94 |
95 | getFiles(): array
96 | -----------------
97 | Returns tree of [upload files](#Uploaded-Files) in a normalized structure, with each leaf an instance of [Nette\Http\FileUpload](https://api.nette.org/3.0/Nette/Http/FileUpload.html):
98 |
99 | ```php
100 | $files = $httpRequest->getFiles();
101 | ```
102 |
103 | getCookie(string $key): string|array|null
104 | -----------------------------------------
105 | Returns a cookie or `null` if it does not exist.
106 |
107 | ```php
108 | $sessId = $httpRequest->getCookie('sess_id');
109 | ```
110 |
111 | getCookies(): array
112 | -------------------
113 | Returns all cookies:
114 |
115 | ```php
116 | $cookies = $httpRequest->getCookies();
117 | ```
118 |
119 | getMethod(): string
120 | -------------------
121 | Returns the HTTP method with which the request was made.
122 |
123 | ```php
124 | echo $httpRequest->getMethod(); // GET, POST, HEAD, PUT
125 | ```
126 |
127 | isMethod(string $method): bool
128 | ------------------------------
129 | Checks the HTTP method with which the request was made. The parameter is case-insensitive.
130 |
131 | ```php
132 | if ($httpRequest->isMethod('GET')) ...
133 | ```
134 |
135 | getHeader(string $header): ?string
136 | ----------------------------------
137 | Returns an HTTP header or `null` if it does not exist. The parameter is case-insensitive:
138 |
139 | ```php
140 | $userAgent = $httpRequest->getHeader('User-Agent');
141 | ```
142 |
143 | getHeaders(): array
144 | -------------------
145 | Returns all HTTP headers as associative array:
146 |
147 | ```php
148 | $headers = $httpRequest->getHeaders();
149 | echo $headers['Content-Type'];
150 | ```
151 |
152 | getReferer(): ?Nette\Http\UrlImmutable
153 | --------------------------------------
154 | What URL did the user come from? Beware, it is not reliable at all.
155 |
156 | isSecured(): bool
157 | -----------------
158 | Is the connection encrypted (HTTPS)? You may need to [set up a proxy|configuring#HTTP proxy] for proper functionality.
159 |
160 | isSameSite(): bool
161 | ------------------
162 | Is the request coming from the same (sub) domain and is initiated by clicking on a link?
163 |
164 | isAjax(): bool
165 | --------------
166 | Is it an AJAX request?
167 |
168 | getRemoteAddress(): ?string
169 | ---------------------------
170 | Returns the user's IP address. You may need to [set up a proxy|configuring#HTTP proxy] for proper functionality.
171 |
172 | getRemoteHost(): ?string
173 | ------------------------
174 | Returns DNS translation of the user's IP address. You may need to [set up a proxy|configuring#HTTP proxy] for proper functionality.
175 |
176 | getRawBody(): ?string
177 | ---------------------
178 | Returns the body of the HTTP request:
179 |
180 | ```php
181 | $body = $httpRequest->getRawBody();
182 | ```
183 |
184 | detectLanguage(array $langs): ?string
185 | -------------------------------------
186 | Detects language. As a parameter `$lang`, we pass an array of languages that the application supports, and it returns the one preferred by browser. It is not magic, the method just uses the `Accept-Language` header. If no match is reached, it returns `null`.
187 |
188 | ```php
189 | // Header sent by browser: Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3
190 |
191 | $langs = ['hu', 'pl', 'en']; // languages supported in application
192 | echo $httpRequest->detectLanguage($langs); // en
193 | ```
194 |
195 |
196 |
197 | RequestFactory
198 | --------------
199 |
200 | The object of the current HTTP request is created by [Nette\Http\RequestFactory](https://api.nette.org/3.0/Nette/Http/RequestFactory.html). If you are writing an application that does not use a DI container, you create a request as follows:
201 |
202 | ```php
203 | $factory = new Nette\Http\RequestFactory;
204 | $httpRequest = $factory->fromGlobals();
205 | ```
206 |
207 | RequestFactory can be configured before calling `fromGlobals()`. We can disable all sanitization of input parameters from invalid UTF-8 sequences using `$factory->setBinary()`. And also set up a proxy server, which is important for the correct detection of the user's IP address using `$factory->setProxy(...)`.
208 |
209 | It's possible to clean up URLs from characters that can get into them because of poorly implemented comment systems on various other websites by using filters:
210 |
211 | ```php
212 | // remove spaces from path
213 | $requestFactory->urlFilters['path']['%20'] = '';
214 |
215 | // remove dot, comma or right parenthesis form the end of the URL
216 | $requestFactory->urlFilters['url']['[.,)]$'] = '';
217 |
218 | // clean the path from duplicated slashes (default filter)
219 | $requestFactory->urlFilters['path']['/{2,}'] = '/';
220 | ```
221 |
222 |
223 |
224 | HTTP Response
225 | =============
226 |
227 | An HTTP response is an [Nette\Http\Response](https://api.nette.org/3.0/Nette/Http/Response.html) object. Unlike the [Request](#HTTP-Request), the object is mutable, so you can use setters to change the state, ie to send headers. Remember that all setters **must be called before any actual output is sent.** The `isSent()` method tells if output have been sent. If it returns `true`, each attempt to send a header throws an `Nette\InvalidStateException` exception.
228 |
229 |
230 | setCode(int $code, string $reason = null)
231 | -----------------------------------------
232 | Changes a status [response code](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10). For better source code readability it is recommended to use [predefined constants](https://api.nette.org/3.0/Nette/Http/IResponse.html) instead of actual numbers.
233 |
234 | ```php
235 | $httpResponse->setCode(Nette\Http\Response::S404_NotFound);
236 | ```
237 |
238 | getCode(): int
239 | --------------
240 | Returns the status code of the response.
241 |
242 | isSent(): bool
243 | --------------
244 | Returns whether headers have already been sent from the server to the browser, so it is no longer possible to send headers or change the status code.
245 |
246 | setHeader(string $name, string $value)
247 | --------------------------------------
248 | Sends an HTTP header and **overwrites** previously sent header of the same name.
249 |
250 | ```php
251 | $httpResponse->setHeader('Pragma', 'no-cache');
252 | ```
253 |
254 | addHeader(string $name, string $value)
255 | --------------------------------------
256 | Sends an HTTP header and **doesn't overwrite** previously sent header of the same name.
257 |
258 | ```php
259 | $httpResponse->addHeader('Accept', 'application/json');
260 | $httpResponse->addHeader('Accept', 'application/xml');
261 | ```
262 |
263 | deleteHeader(string $name)
264 | --------------------------
265 | Deletes a previously sent HTTP header.
266 |
267 | getHeader(string $header): ?string
268 | ----------------------------------
269 | Returns the sent HTTP header, or `null` if it does not exist. The parameter is case-insensitive.
270 |
271 | ```php
272 | $pragma = $httpResponse->getHeader('Pragma');
273 | ```
274 |
275 | getHeaders(): array
276 | -------------------
277 | Returns all sent HTTP headers as associative array.
278 |
279 | ```php
280 | $headers = $httpResponse->getHeaders();
281 | echo $headers['Pragma'];
282 | ```
283 |
284 | setContentType(string $type, string $charset = null)
285 | ----------------------------------------------------
286 | Sends the header `Content-Type`.
287 |
288 | ```php
289 | $httpResponse->setContentType('text/plain', 'UTF-8');
290 | ```
291 |
292 | redirect(string $url, int $code = self::S302_FOUND): void
293 | ---------------------------------------------------------
294 | Redirects to another URL. Don't forget to quit the script then.
295 |
296 | ```php
297 | $httpResponse->redirect('http://example.com');
298 | exit;
299 | ```
300 |
301 | setExpiration(?string $time)
302 | ----------------------------
303 | Sets the expiration of the HTTP document using the `Cache-Control` and `Expires` headers. The parameter is either a time interval (as text) or `null`, which disables caching.
304 |
305 | ```php
306 | // browser cache expires in one hour
307 | $httpResponse->setExpiration('1 hour');
308 | ```
309 |
310 | setCookie(string $name, string $value, string|int|\DateTimeInterface|null $expire, string $path = null, string $domain = null, bool $secure = null, bool $httpOnly = null, string $sameSite = null)
311 | --------------------------------------------------------------------------------------------------------------------------------------------------------------
312 | Sends a cookie. The default values of the parameters are:
313 | - `$path` with scope to all directories (`'/'`)
314 | - `$domain` with scope of the current (sub)domain, but not its subdomains
315 | - `$secure` defaults to false
316 | - `$httpOnly` is true, so the cookie is inaccessible to JavaScript
317 | - `$sameSite` is null, so the flag is not specified
318 |
319 | The `$expire` parameter can be specified as a string, an object implementing `DateTimeInterface`, or the number of seconds.
320 |
321 | ```php
322 | $httpResponse->setCookie('lang', 'en', '100 days');
323 | ```
324 |
325 | deleteCookie(string $name, string $path = null, string $domain = null, bool $secure = null): void
326 | -------------------------------------------------------------------------------------------------
327 | Deletes a cookie. The default values of the parameters are:
328 | - `$path` with scope to all directories (`'/'`)
329 | - `$domain` with scope of the current (sub)domain, but not its subdomains
330 | - `$secure` defaults to false
331 |
332 | ```php
333 | $httpResponse->deleteCookie('lang');
334 | ```
335 |
336 |
337 | Uploaded Files
338 | ==============
339 |
340 | Method `Nette\Http\Request::getFiles()` return a tree of upload files in a normalized structure, with each leaf an instance of `Nette\Http\FileUpload`. These objects encapsulate the data submitted by the `` form element.
341 |
342 | The structure reflects the naming of elements in HTML. In the simplest example, this might be a single named form element submitted as:
343 |
344 | ```html
345 |
346 | ```
347 |
348 | In this case, the `$request->getFiles()` returns array:
349 |
350 | ```php
351 | [
352 | 'avatar' => /* FileUpload instance */
353 | ]
354 | ```
355 |
356 | The `FileUpload` object is created even if the user did not upload any file or the upload failed. Method `hasFile()` returns true if a file has been sent:
357 |
358 | ```php
359 | $request->getFile('avatar')->hasFile();
360 | ```
361 |
362 | In the case of an input using array notation for the name:
363 |
364 | ```html
365 |
366 | ```
367 |
368 | returned tree ends up looking like this:
369 |
370 | ```php
371 | [
372 | 'my-form' => [
373 | 'details' => [
374 | 'avatar' => /* FileUpload instance */
375 | ],
376 | ],
377 | ]
378 | ```
379 |
380 | You can also create arrays of files:
381 |
382 | ```html
383 |
384 | ```
385 |
386 | In such a case structure looks like:
387 |
388 | ```php
389 | [
390 | 'my-form' => [
391 | 'details' => [
392 | 'avatars' => [
393 | 0 => /* FileUpload instance */,
394 | 1 => /* FileUpload instance */,
395 | 2 => /* FileUpload instance */,
396 | ],
397 | ],
398 | ],
399 | ]
400 | ```
401 |
402 | The best way to access index 1 of a nested array is as follows:
403 |
404 | ```php
405 | $file = Nette\Utils\Arrays::get(
406 | $request->getFiles(),
407 | ['my-form', 'details', 'avatars', 1],
408 | null
409 | );
410 | if ($file instanceof FileUpload) {
411 | ...
412 | }
413 | ```
414 |
415 | Because you can't trust data from the outside and therefore don't rely on the form of the file structure, it's safer to use the `Arrays::get()` than the `$request->getFiles()['my-form']['details']['avatars'][1]`, which may fail.
416 |
417 |
418 | Overview of `FileUpload` Methods .{toc: FileUpload}
419 | ---------------------------------------------------
420 |
421 | hasFile(): bool
422 | ---------------
423 | Returns `true` if the user has uploaded a file.
424 |
425 | isOk(): bool
426 | ------------
427 | Returns `true` if the file was uploaded successfully.
428 |
429 | getError(): int
430 | ---------------
431 | Returns the error code associated with the uploaded file. It is be one of [UPLOAD_ERR_XXX](http://php.net/manual/en/features.file-upload.errors.php) constants. If the file was uploaded successfully, it returns `UPLOAD_ERR_OK`.
432 |
433 | move(string $dest)
434 | ------------------
435 | Moves an uploaded file to a new location. If the destination file already exists, it will be overwritten.
436 |
437 | ```php
438 | $file->move('/path/to/files/name.ext');
439 | ```
440 |
441 | getContents(): ?string
442 | ----------------------
443 | Returns the contents of the uploaded file. If the upload was not successful, it returns `null`.
444 |
445 | getContentType(): ?string
446 | -------------------------
447 | Detects the MIME content type of the uploaded file based on its signature. If the upload was not successful or the detection failed, it returns `null`.
448 |
449 | Requires PHP extension `fileinfo`.
450 |
451 | getName(): string
452 | -----------------
453 | Returns the original file name as submitted by the browser.
454 |
455 | Do not trust the value returned by this method. A client could send a malicious filename with the intention to corrupt or hack your application.
456 |
457 | getSanitizedName(): string
458 | --------------------------
459 | Returns the sanitized file name. It contains only ASCII characters `[a-zA-Z0-9.-]`. If the name does not contain such characters, it returns 'unknown'. If the file is JPEG, PNG, GIF, or WebP image, it returns the correct file extension.
460 |
461 | getSize(): int
462 | --------------
463 | Returns the size of the uploaded file. If the upload was not successful, it returns `0`.
464 |
465 | getTemporaryFile(): string
466 | --------------------------
467 | Returns the path of the temporary location of the uploaded file. If the upload was not successful, it returns `''`.
468 |
469 | isImage(): bool
470 | ---------------
471 | Returns `true` if the uploaded file is a JPEG, PNG, GIF, or WebP image. Detection is based on its signature. The integrity of the entire file is not checked. You can find out if an image is not corrupted for example by trying to [load it](#toImage).
472 |
473 | Requires PHP extension `fileinfo`.
474 |
475 | getImageSize(): ?array
476 | ----------------------
477 | Returns a pair of `[width, height]` with dimensions of the uploaded image. If the upload was not successful or is not a valid image, it returns `null`.
478 |
479 | toImage(): Nette\Utils\Image
480 | ----------------------------
481 | Loads an image as an `Image` object. If the upload was not successful or is not a valid image, it throws an `Nette\Utils\ImageException` exception.
482 |
483 |
484 |
485 | Sessions
486 | ========
487 |
488 | When using sessions, each user receives a unique identifier called session ID, which is passed in a cookie. This serves as the key to the session data. Unlike cookies, which are stored on the browser side, session data is stored on the server side.
489 |
490 | The session is managed by the [Nette\Http\Session](https://api.nette.org/3.0/Nette/Http/Session.html) object.
491 |
492 |
493 | Starting Session
494 | ----------------
495 |
496 | By default, Nette automatically starts a session if the HTTP request contains a cookie with a session ID. It also starts automatically when we start reading from or writing data to it. Manually is session started by `$session->start()`.
497 |
498 | PHP sends HTTP headers affecting caching when starting the session, see `session_cache_limiter`, and possibly a cookie with the session ID. Therefore, it is always necessary to start the session before sending any output to the browser, otherwise an exception will be thrown. So if you know that a session will be used during page rendering, start it manually before, for example in the presenter.
499 |
500 | In developer mode, Tracy starts the session because it uses it to display redirection and AJAX requests bars in the Tracy Bar.
501 |
502 |
503 | Section
504 | -------
505 |
506 | In pure PHP, the session data store is implemented as an array accessible via a global variable `$_SESSION`. The problem is that applications normally consist of a number of independent parts, and if all have only one same array available, sooner or later a name collision will occur.
507 |
508 | Nette Framework solves the problem by dividing the entire space into sections (objects [Nette\Http\SessionSection](https://api.nette.org/3.0/Nette/Http/SessionSection.html)). Each unit then uses its own section with a unique name and no collisions can occur.
509 |
510 | We get the section from the session manager:
511 |
512 | ```php
513 | $section = $session->getSession('unique name');
514 | ```
515 |
516 | In the presenter it is enough to call `getSession()` with the parameter:
517 |
518 | ```php
519 | // $this is Presenter
520 | $section = $this->getSession('unique name');
521 | ```
522 |
523 | The existence of the section can be checked by the method `$session->hasSection('unique name')`.
524 |
525 | And then it's really simple to work with that section:
526 |
527 | ```php
528 | // variable writing
529 | $section->userName = 'john'; // nebo $section['userName'] = 'john';
530 |
531 | // variable reading
532 | echo $section->userName; // nebo echo $section['userName'];
533 |
534 | // variable removing
535 | unset($section->userName); // unset($section['userName']);
536 | ```
537 |
538 | It's possible to use `foreach` cycle to obtain all variables from section:
539 |
540 | ```php
541 | foreach ($section as $key => $val) {
542 | echo "$key = $val";
543 | }
544 | ```
545 |
546 | Accessing a non-existent variable does not generate any error (the returned value is null). It could be undesirable behavior in some cases and that's why there is a possibility to change it:
547 |
548 | ```php
549 | $section->warnOnUndefined = true;
550 | ```
551 |
552 |
553 | How to Set Expiration
554 | ---------------------
555 |
556 | Expiration can be set for individual sections or even individual variables. We can let the user's login expire in 20 minutes, but still remember the contents of a shopping cart.
557 |
558 | ```php
559 | // section will expire after 20 minutes
560 | $section->setExpiration('20 minutes');
561 |
562 | // variable $section->flash will expire after 30 seconds
563 | $section->setExpiration('30 seconds', 'flash');
564 | ```
565 |
566 | The cancellation of the previously set expiration can be achieved by the method `removeExpiration()`. Immediate deletion of the whole section will be ensured by the method `remove()`.
567 |
568 |
569 |
570 | Session Management
571 | ------------------
572 |
573 | Overview of methods of the `Nette\Http\Session` class for session management:
574 |
575 | start(): void
576 | -------------
577 | Starts a session.
578 |
579 | isStarted(): bool
580 | -----------------
581 | Is the session started?
582 |
583 | close(): void
584 | -------------
585 | Ends the session. The session ends automatically at the end of the script.
586 |
587 | destroy(): void
588 | ---------------
589 | Ends and deletes the session.
590 |
591 | exists(): bool
592 | --------------
593 | Does the HTTP request contain a cookie with a session ID?
594 |
595 | regenerateId(): void
596 | --------------------
597 | Generates a new random session ID. Data remain unchanged.
598 |
599 | getId(): string
600 | ---------------
601 | Returns the session ID.
602 |
603 |
604 | Configuration
605 | -------------
606 |
607 | Methods must be called before starting a session.
608 |
609 | setName(string $name): static
610 | -----------------------------
611 | Changes the session name. It is possible to run several different sessions at the same time within one website, each under a different name.
612 |
613 | getName(): string
614 | -----------------
615 | Returns the session name.
616 |
617 | setOptions(array $options): static
618 | ----------------------------------
619 | Configures the session. It is possible to set all PHP [session directives](https://www.php.net/manual/en/session.configuration.php) (in camelCase format, eg write `savePath` instead of `session.save_path`) and also [readAndClose](https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters).
620 |
621 | setExpiration(?string $time): static
622 | ------------------------------------
623 | Sets the time of inactivity after which the session expires.
624 |
625 | setCookieParameters(string $path, string $domain = null, bool $secure = null, string $samesite = null): static
626 | --------------------------------------------------------------------------------------------------------------
627 | Sets parameters for cookies.
628 |
629 | setSavePath(string $path): static
630 | ---------------------------------
631 | Sets the directory where session files are stored.
632 |
633 | setHandler(\SessionHandlerInterface $handler): static
634 | -----------------------------------------------------
635 | Sets custom handler, see [PHP documentation](https://www.php.net/manual/en/class.sessionhandlerinterface.php).
636 |
637 |
638 | Safety First
639 | ------------
640 |
641 | The server assumes that it communicates with the same user as long as requests contain the same session ID. The task of security mechanisms is to ensure that this behavior really works and that there is no possibility to substitute or steal an identifier.
642 |
643 | That's why Nette Framework properly configures PHP directives to transfer session ID only in cookies, to avoid access from JavaScript and to ignore the identifiers in the URL. Moreover in critical moments, such as user login, it generates a new Session ID.
644 |
645 | Function ini_set is used for configuring PHP, but unfortunately, its use is prohibited at some web hosting services. If it's your case, try to ask your hosting provider to allow this function for you, or at least to configure his server properly. .[note]
646 |
647 |
648 |
649 | Url
650 | ===
651 |
652 | The [Nette\Http\Url](https://api.nette.org/3.0/Nette/Http/Url.html) class makes it easy to work with the URL and its individual components, which are outlined in this diagram:
653 |
654 | ```
655 | scheme user password host port path query fragment
656 | | | | | | | | |
657 | /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\
658 | http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer
659 | \______\__________________________/
660 | | |
661 | hostUrl authority
662 | ```
663 |
664 | URL generation is intuitive:
665 |
666 | ```php
667 | use Nette\Http\Url;
668 |
669 | $url = new Url;
670 | $url->setScheme('https')
671 | ->setHost('localhost')
672 | ->setPath('/edit')
673 | ->setQueryParameter('foo', 'bar');
674 |
675 | echo $url; // 'https://localhost/edit?foo=bar'
676 | ```
677 |
678 | You can also parse the URL and then manipulate it:
679 |
680 | ```php
681 | $url = new Url(
682 | 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer'
683 | );
684 | ```
685 |
686 | The following methods are available to get or change individual URL components:
687 |
688 | Setter | Getter | Returned value
689 | ----------------------------------------|-------------------------------|------------------
690 | `setScheme(string $scheme)` | `getScheme(): string` | `'http'`
691 | `setUser(string $user)` | `getUser(): string` | `'john'`
692 | `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'`
693 | `setHost(string $host)` | `getHost(): string` | `'nette.org'`
694 | `setPort(int $port)` | `getPort(): ?int` | `8080`
695 | `setPath(string $path)` | `getPath(): string` | `'/en/download'`
696 | `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'`
697 | `setFragment(string $fragment)` | `getFragment(): string` | `'footer'`
698 | -- | `getAuthority(): string` | `'nette.org:8080'`
699 | -- | `getHostUrl(): string` | `'http://nette.org:8080'`
700 | -- | `getAbsoluteUrl(): string` | full URL
701 |
702 | We can also operate with individual query parameters using:
703 |
704 | Setter | Getter
705 | ----------------------------------------|---------
706 | `setQuery(string\|array $query)` | `getQueryParameters(): array`
707 | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)`
708 |
709 | Method `getDomain(int $level = 2)` returns the right or left part of the host. This is how it works if the host is `www.nette.org`:
710 |
711 | Usage | Result
712 | ----------------------------------------|---------
713 | `getDomain(1)` | `'org'`
714 | `getDomain(2)` | `'nette.org'`
715 | `getDomain(3)` | `'www.nette.org'`
716 | `getDomain(0)` | `'www.nette.org'`
717 | `getDomain(-1)` | `'www.nette'`
718 | `getDomain(-2)` | `'www'`
719 | `getDomain(-3)` | `''`
720 |
721 |
722 | The `Url` class implements the `JsonSerializable` interface and has a `__toString()` method so that the object can be printed or used in data passed to `json_encode()`.
723 |
724 | ```php
725 | echo $url;
726 | echo json_encode([$url]);
727 | ```
728 |
729 | Method `isEqual(string|Url $anotherUrl): bool` tests whether the two URLs are identical.
730 |
731 | ```php
732 | $url->isEqual('https://nette.org');
733 | ```
734 |
735 |
736 | UrlImmutable
737 | ============
738 |
739 | The class [Nette\Http\UrlImmutable](https://api.nette.org/3.0/Nette/Http/UrlImmutable.html) is an immutable alternative to class `Url` (just as in PHP `DateTimeImmutable` is immutable alternative to `DateTime`). Instead of setters, it has so-called withers, which do not change the object, but return new instances with a modified value:
740 |
741 | ```php
742 | use Nette\Http\UrlImmutable;
743 |
744 | $url = new UrlImmutable(
745 | 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer'
746 | );
747 |
748 | $newUrl = $url
749 | ->withUser('')
750 | ->withPassword('')
751 | ->withPath('/cs/');
752 |
753 | echo $newUrl; // 'http://nette.org:8080/cs/?name=param#footer'
754 | ```
755 |
756 | The following methods are available to get or change individual URL components:
757 |
758 | Wither | Getter | Returned value
759 | ----------------------------------------|-------------------------------|------------------
760 | `withScheme(string $scheme)` | `getScheme(): string` | `'http'`
761 | `withUser(string $user)` | `getUser(): string` | `'john'`
762 | `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'`
763 | `withHost(string $host)` | `getHost(): string` | `'nette.org'`
764 | `withPort(int $port)` | `getPort(): ?int` | `8080`
765 | `withPath(string $path)` | `getPath(): string` | `'/en/download'`
766 | `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'`
767 | `withFragment(string $fragment)` | `getFragment(): string` | `'footer'`
768 | -- | `getAuthority(): string` | `'nette.org:8080'`
769 | -- | `getHostUrl(): string` | `'http://nette.org:8080'`
770 | -- | `getAbsoluteUrl(): string` | full URL
771 |
772 | We can also operate with individual query parameters using:
773 |
774 | Wither | Getter
775 | ------------------------------------|---------
776 | `withQuery(string\|array $query)` | `getQueryParameters(): array`
777 | -- | `getQueryParameter(string $name)`
778 |
779 | The `getDomain(int $level = 2)` method works the same as the method in `Url`. Method `withoutUserInfo()` removes `user` and `password`.
780 |
781 | The `UrlImmutable` class implements the `JsonSerializable` interface and has a `__toString()` method so that the object can be printed or used in data passed to `json_encode()`.
782 |
783 | ```php
784 | echo $url;
785 | echo json_encode([$url]);
786 | ```
787 |
788 | Method `isEqual(string|Url $anotherUrl): bool` tests whether the two URLs are identical.
789 |
790 |
791 | If you like Nette, **[please make a donation now](https://github.com/sponsors/dg)**. Thank you!
792 |
--------------------------------------------------------------------------------