├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
├── rector.php
└── src
├── CorsAllowAll.php
├── Event
└── SetLocaleEvent.php
├── Exception
├── BadUriPrefixException.php
└── InvalidLocalesFormatException.php
├── ForceSecureConnection.php
├── HttpCache.php
├── IpFilter.php
├── Locale.php
├── Redirect.php
├── Subfolder.php
└── TagRequest.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii Middleware Change Log
2 |
3 | ## 1.0.5 under development
4 |
5 | - Chg #129: Bump PHP minimal version to 8.1 and refactor code to use new features (@dagpro)
6 | - Chg #129: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@dagpro)
7 | - Chg #129: Bump `yiisoft/router` version to `^4.0` (@dagpro)
8 | - Chg #129: Bump `yiisoft/session` version to `^3.0` (@dagpro)
9 | - Bug #129: Explicitly mark nullable parameters (@dagpro)
10 | - Chg #130: Bump minimal version of `yiisoft/cookie` to `^1.2.3` (@vjik)
11 | - Enh #130: Allow to use PSR-20 clock interface to get current time into `Locale` middleware (@vjik)
12 |
13 | ## 1.0.4 September 03, 2024
14 |
15 | - Enh #121: Add `network-utilities` dependency and use it instead of `validator` for `IpFilter` (@arogachev)
16 | - Enh #121: Add support for `validator` of version 2.0, mark it as deprecated (@arogachev)
17 |
18 | ## 1.0.3 June 06, 2024
19 |
20 | - Enh #117: Add support for `psr/http-message` version `^2.0` (@bautrukevich)
21 |
22 | ## 1.0.2 October 06, 2023
23 |
24 | - Enh #103: Add `Access-Control-Expose-Headers: *` to `CorsAllowAll` (@xepozz)
25 | - Bug #105: Fire `SetLocaleEvent` and prepare URL generator in `Locale` before handle request (@vjik)
26 | - Bug #112: Check ignored requests earlier and do not set default locale (@g-rodigy)
27 |
28 | ## 1.0.1 June 04, 2023
29 |
30 | - Chg #95: Remove unused network utilities dependency (@arogachev)
31 | - Bug #96: Fix unexpected redirects from `Locale` middleware on GET requests (@vjik)
32 | - Bug #97: Don't search locale in cookies when `$cookieDuration` is null (@vjik)
33 |
34 | ## 1.0.0 May 22, 2023
35 |
36 | - Initial release.
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software ()
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Yii Middleware
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/yii-middleware)
10 | [](https://packagist.org/packages/yiisoft/yii-middleware)
11 | [](https://github.com/yiisoft/yii-middleware/actions/workflows/build.yml)
12 | [](https://codecov.io/gh/yiisoft/yii-middleware)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/yii-middleware/master)
14 | [](https://github.com/yiisoft/yii-middleware/actions/workflows/static.yml?query=branch%3Amaster)
15 | [](https://shepherd.dev/github/yiisoft/yii-middleware)
16 | [](https://shepherd.dev/github/yiisoft/yii-middleware)
17 |
18 | The package provides middleware classes that implement [PSR-15](https://www.php-fig.org/psr/psr-15/#12-middleware):
19 |
20 | - [`ForceSecureConnection`](#forcesecureconnection).
21 | - [`HttpCache`](#httpcache).
22 | - [`IpFilter`](#ipfilter).
23 | - [`Redirect`](#redirect).
24 | - [`Subfolder`](#subfolder).
25 | - [`TagRequest`](#tagrequest).
26 | - [`Locale`](#locale).
27 | - [`CorsAllowAll`](#corsallowall).
28 |
29 | For proxy related middleware, there is a separate package -
30 | [Yii Proxy Middleware](https://github.com/yiisoft/proxy-middleware).
31 |
32 | For more information on how to use middleware in the [Yii Framework](https://www.yiiframework.com/), see the
33 | [Yii middleware guide](https://github.com/yiisoft/docs/blob/master/guide/en/structure/middleware.md).
34 |
35 | ## Requirements
36 |
37 | - PHP 8.1 or higher.
38 |
39 | ## Installation
40 |
41 | The package could be installed with [Composer](https://getcomposer.org):
42 |
43 | ```shell
44 | composer require yiisoft/yii-middleware
45 | ```
46 |
47 | ## General usage
48 |
49 | All classes are separate implementations of [PSR 15](https://github.com/php-fig/http-server-middleware)
50 | middleware and don't interact with each other in any way.
51 |
52 | ### `ForceSecureConnection`
53 |
54 | Redirects insecure requests from HTTP to HTTPS, and adds headers necessary to enhance the security policy.
55 |
56 | ```php
57 | use Yiisoft\Yii\Middleware\ForceSecureConnection;
58 |
59 | /**
60 | * @var Psr\Http\Message\ResponseFactoryInterface $responseFactory
61 | * @var Psr\Http\Message\ServerRequestInterface $request
62 | * @var Psr\Http\Server\RequestHandlerInterface $handler
63 | */
64 |
65 | $middleware = new ForceSecureConnection($responseFactory);
66 |
67 | // Enables redirection from HTTP to HTTPS:
68 | $middleware = $middleware->withRedirection(301);
69 | // Disables redirection from HTTP to HTTPS:
70 | $middleware = $middleware->withoutRedirection();
71 |
72 | $response = $middleware->process($request, $handler);
73 | ```
74 |
75 | The `Content-Security-Policy` (CSP) header can force the browser to load page resources only through
76 | a secure connection, even if links in the page layout are specified with an unprotected protocol.
77 |
78 | ```php
79 | $middleware = $middleware->withCSP('upgrade-insecure-requests; default-src https:');
80 | // Or without the `Content-Security-Policy` header in response:
81 | $middleware = $middleware->withoutCSP();
82 | ```
83 |
84 | Middleware adds HTTP Strict-Transport-Security (HSTS) header to each response.
85 | The header tells the browser that your site works with HTTPS only.
86 |
87 | ```php
88 | $maxAge = 3600; // Default is 31_536_000 (12 months).
89 | $subDomains = false; // Whether to add the `includeSubDomains` option to the header value.
90 |
91 | $middleware = $middleware->withHSTS($maxAge, $subDomains);
92 | // Or without the `Strict-Transport-Security` header in response:
93 | $middleware = $middleware->withoutHSTS();
94 | ```
95 |
96 | ### `HttpCache`
97 |
98 | Implements client-side caching by utilizing the `Last-Modified` and `ETag` HTTP headers.
99 |
100 | ```php
101 | use Yiisoft\Yii\Middleware\HttpCache;
102 |
103 | /**
104 | * @var Psr\Http\Message\ServerRequestInterface $request
105 | * @var Psr\Http\Server\RequestHandlerInterface $handler
106 | */
107 |
108 | $middleware = new HttpCache();
109 |
110 | // Specify callable that generates the last modified:
111 | $middleware = $middleware->withLastModified(function (ServerRequestInterface $request, mixed $params): int {
112 | $defaultLastModified = 3600;
113 | // Some actions.
114 | return $defaultLastModified;
115 | });
116 | // Specify callable that generates the ETag seed string:
117 | $middleware = $middleware->withEtagSeed(function (ServerRequestInterface $request, mixed $params): string {
118 | $defaultEtagSeed = '33a64df551425fcc55e4d42a148795d9f25f89d4';
119 | // Some actions.
120 | return $defaultEtagSeed;
121 | });
122 |
123 | $response = $middleware->process($request, $handler);
124 | ```
125 |
126 | Additionally, you can specify the following options:
127 |
128 | ```php
129 | // Extra parameters for ETag seed string generation:
130 | $middleware = $middleware->withParams(['parameter' => 'value']);
131 |
132 | // The value of the `Cache-Control` HTTP header:
133 | $middleware = $middleware->withCacheControlHeader('public, max-age=31536000');
134 | // Default is `public, max-age=3600`. If null, the header won't be sent.
135 |
136 | // Enable weak ETags generation (disabled by default):
137 | $middleware = $middleware->withWeakTag();
138 | // You should use weak ETags if the content is semantically equal, but not byte-equal.
139 | ```
140 |
141 | ### `IpFilter`
142 |
143 | `IpFilter` allows access from specified IP ranges only and responds with 403 for all other IPs.
144 |
145 | ```php
146 | use Yiisoft\Yii\Middleware\IpFilter;
147 |
148 | /**
149 | * @var Psr\Http\Message\ResponseFactoryInterface $responseFactory
150 | * @var Psr\Http\Message\ServerRequestInterface $request
151 | * @var Psr\Http\Server\RequestHandlerInterface $handler
152 | * @var Yiisoft\Validator\Rule\ValidatorInterface $validator
153 | */
154 |
155 | // Name of the request attribute holding client IP:
156 | $clientIpAttribute = 'client-ip';
157 | // If there is no such attribute, or it has no value, then the middleware will respond with 403 forbidden.
158 | // If the name of the request attribute is `null`, then `REMOTE_ADDR` server parameter is used to determine client IP.
159 |
160 | $middleware = new IpFilter($validator, $responseFactory, $clientIpAttribute);
161 |
162 | // Change client IP validator:
163 | $middleware = $middleware->withValidator($validator);
164 |
165 | $response = $middleware->process($request, $handler);
166 | ```
167 |
168 | ### `Redirect`
169 |
170 | Generates and adds a `Location` header to the response.
171 |
172 | ```php
173 | use Yiisoft\Yii\Middleware\Redirect;
174 |
175 | /**
176 | * @var Psr\Http\Message\ResponseFactoryInterface $responseFactory
177 | * @var Psr\Http\Message\ServerRequestInterface $request
178 | * @var Psr\Http\Server\RequestHandlerInterface $handler
179 | * @var Yiisoft\Router\UrlGeneratorInterface $urlGenerator
180 | */
181 |
182 | $middleware = new Redirect($ipValidator, $urlGenerator);
183 |
184 | // Specify URL for redirection:
185 | $middleware = $middleware->toUrl('/login');
186 | // Or specify route data for redirection:
187 | $middleware = $middleware->toRoute('auth/login', ['parameter' => 'value']);
188 | // If you have set a redirect URL with "toUrl()" method, the middleware ignores the route data, since the URL is a
189 | // priority.
190 |
191 | $response = $middleware->process($request, $handler);
192 | ```
193 |
194 | You can also set the status of the response code for redirection.
195 |
196 | ```php
197 | // For permanent redirection (301):
198 | $middleware = $middleware->permanent();
199 |
200 | // For temporary redirection (302):
201 | $middleware = $middleware->permanent();
202 |
203 | // Or specify the status code yourself:
204 | $middleware = $middleware->withStatus(303);
205 | ```
206 |
207 | ### `Subfolder`
208 |
209 | Supports routing when the entry point of the application isn't directly at the webroot.
210 | By default, it determines webroot based on server parameters.
211 |
212 | > Info: You should place this middleware before `Route` middleware in the middleware list.
213 |
214 | If you want the application to run on the specified path, use the prefix instead:
215 |
216 | ```php
217 | use Yiisoft\Yii\Middleware\Subfolder;
218 |
219 | /**
220 | * @var Psr\Http\Message\ServerRequestInterface $request
221 | * @var Psr\Http\Server\RequestHandlerInterface $handler
222 | * @var Yiisoft\Aliases\Aliases $aliases
223 | * @var Yiisoft\Router\UrlGeneratorInterface $urlGenerator
224 | */
225 |
226 | // URI prefix the specified immediately after the domain part (default is `null`):
227 | $prefix = '/blog';
228 | // The prefix value usually begins with a slash and must not end with a slash.
229 |
230 | $middleware = new Subfolder($urlGenerator, $aliases, $prefix);
231 |
232 | $response = $middleware->process($request, $handler);
233 | ```
234 |
235 | ### `TagRequest`
236 |
237 | Tags request with a random value that could be later used for identifying it.
238 |
239 | ```php
240 | use Yiisoft\Yii\Middleware\TagRequest;
241 |
242 | /**
243 | * @var Psr\Http\Message\ServerRequestInterface $request
244 | * @var Psr\Http\Server\RequestHandlerInterface $handler
245 | */
246 |
247 | $middleware = new TagRequest();
248 | // In the process, a request attribute with the name `requestTag`
249 | // and the generated value by the function `uniqid()` will be added.
250 | $response = $middleware->process($request, $handler);
251 | ```
252 |
253 | ### `Locale`
254 |
255 | Supports locale-based routing and configures URL generator.
256 |
257 | > Info: You should place this middleware before `Route` middleware in the middleware list.
258 |
259 | ```php
260 | use Yiisoft\Yii\Middleware\Locale;
261 |
262 | // Available locales.
263 | $locales = ['en' => 'en-US', 'ru' => 'ru-RU', 'uz' => 'uz-UZ']
264 | /**
265 | * Specify supported locales.
266 | *
267 | * @var Locale $middleware
268 | */
269 | $middleware = $middleware->withSupportedLocales($locales);
270 |
271 | // Ignore requests which URLs that match "/api**" wildcard pattern.
272 | $middleware = $middleware->withIgnoredRequestUrlPatterns(['/api**']);
273 |
274 | $response = $middleware->process($request);
275 | ```
276 |
277 | The priority of lookup is the following:
278 |
279 | 1. URI query path, that's `/de/blog`.
280 | 2. URI query parameter name, that's `/blog?_language=de`.
281 | You can customize parameter name via `withQueryParameterName()`.
282 | 3. Cookie named `_language`. You can customize name via `withCookieName()`.
283 | 4. `Accept-Language` header. Not enabled by default. Use `withDetectLocale(true)` to enable it.
284 |
285 | Found locale is not saved by default. It can be saved to cookies:
286 |
287 | ```php
288 | use Yiisoft\Yii\Middleware\Locale;
289 |
290 | /** @var Locale $middleware */
291 | $middleware = $middleware
292 | ->withCookieDuration(new DateInterval('P30D')) // Key parameter for activating saving to cookies.
293 | // Extra customization.
294 | ->withCookieName('_custom_name')
295 | ->withSecureCookie(true)
296 | ```
297 |
298 | To configure more services, such as translator or session, use `SetLocaleEvent`
299 | ([Yii Event Dispatcher](https://github.com/yiisoft/event-dispatcher) is required).
300 |
301 | ```php
302 | use Yiisoft\Translator\TranslatorInterface;
303 | use Yiisoft\Yii\Middleware\Event\SetLocaleEvent;
304 |
305 | final class SetLocaleEventHandler
306 | {
307 | public function __construct(
308 | private TranslatorInterface $translator
309 | ) {
310 | }
311 |
312 | public function handle(SetLocaleEvent $event): void
313 | {
314 | $this->translator->setLocale($event->getLocale());
315 | }
316 | }
317 | ```
318 |
319 | > Note: Using tranlator requires [Yii Message Translator](https://github.com/yiisoft/translator).
320 |
321 | ### `CorsAllowAll`
322 |
323 | Adds CORS headers to the response.
324 |
325 | ## Documentation
326 |
327 | - [Internals](docs/internals.md)
328 |
329 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that.
330 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community).
331 |
332 | ## License
333 |
334 | The Yii Middleware is free software. It is released under the terms of the BSD License.
335 | Please see [`LICENSE`](./LICENSE.md) for more information.
336 |
337 | Maintained by [Yii Software](https://www.yiiframework.com/).
338 |
339 | ## Support the project
340 |
341 | [](https://opencollective.com/yiisoft)
342 |
343 | ## Follow updates
344 |
345 | [](https://www.yiiframework.com/)
346 | [](https://twitter.com/yiiframework)
347 | [](https://t.me/yii3en)
348 | [](https://www.facebook.com/groups/yiitalk)
349 | [](https://yiiframework.com/go/slack)
350 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/yii-middleware",
3 | "type": "library",
4 | "description": "Yii Middleware",
5 | "keywords": [
6 | "yii",
7 | "framework",
8 | "middleware"
9 | ],
10 | "homepage": "https://www.yiiframework.com/",
11 | "license": "BSD-3-Clause",
12 | "support": {
13 | "issues": "https://github.com/yiisoft/yii-middleware/issues?state=open",
14 | "source": "https://github.com/yiisoft/yii-middleware",
15 | "forum": "https://www.yiiframework.com/forum/",
16 | "wiki": "https://www.yiiframework.com/wiki/",
17 | "irc": "ircs://irc.libera.chat:6697/yii",
18 | "chat": "https://t.me/yii3en"
19 | },
20 | "funding": [
21 | {
22 | "type": "opencollective",
23 | "url": "https://opencollective.com/yiisoft"
24 | },
25 | {
26 | "type": "github",
27 | "url": "https://github.com/sponsors/yiisoft"
28 | }
29 | ],
30 | "prefer-stable": true,
31 | "require": {
32 | "php": "8.1 - 8.4",
33 | "psr/event-dispatcher": "^1.0",
34 | "psr/http-factory": "^1.0",
35 | "psr/http-message": "^1.0|^2.0",
36 | "psr/http-server-handler": "^1.0",
37 | "psr/http-server-middleware": "^1.0",
38 | "psr/log": "^3.0",
39 | "yiisoft/aliases": "^3.0",
40 | "yiisoft/cookies": "^1.2.3",
41 | "yiisoft/friendly-exception": "^1.0",
42 | "yiisoft/http": "^1.2",
43 | "yiisoft/network-utilities": "^1.2",
44 | "yiisoft/router": "^4.0",
45 | "yiisoft/session": "^3.0",
46 | "yiisoft/strings": "^2.1",
47 | "yiisoft/validator": "^1.0|^2.0"
48 | },
49 | "require-dev": {
50 | "httpsoft/http-message": "^1.0",
51 | "maglnet/composer-require-checker": "^4.2",
52 | "phpunit/phpunit": "^10.5.45",
53 | "rector/rector": "^2.0.10",
54 | "roave/infection-static-analysis-plugin": "^1.16",
55 | "psr/clock": "^1.0",
56 | "spatie/phpunit-watcher": "^1.23",
57 | "vimeo/psalm": "^5.26.1 || ^6.8.9",
58 | "yiisoft/router-fastroute": "^4.0",
59 | "yiisoft/test-support": "^3.0"
60 | },
61 | "suggest": {
62 | "yiisoft/event-dispatcher": "For using events",
63 | "yiisoft/translator": "For updating translator's locale"
64 | },
65 | "autoload": {
66 | "psr-4": {
67 | "Yiisoft\\Yii\\Middleware\\": "src"
68 | }
69 | },
70 | "autoload-dev": {
71 | "psr-4": {
72 | "Yiisoft\\Yii\\Middleware\\Tests\\": "tests"
73 | }
74 | },
75 | "config": {
76 | "sort-packages": true,
77 | "allow-plugins": {
78 | "infection/extension-installer": true,
79 | "composer/package-versions-deprecated": true
80 | }
81 | },
82 | "scripts": {
83 | "test": "phpunit --testdox",
84 | "test-watch": "phpunit-watcher watch"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | withPaths([
11 | __DIR__ . '/src',
12 | __DIR__ . '/tests',
13 | ])
14 | ->withPhpSets(php81: true)
15 | ->withRules([
16 | InlineConstructorDefaultToPropertyRector::class,
17 | ])
18 | ->withSkip([
19 | NullToStrictStringFuncCallArgRector::class,
20 | ]);
21 |
--------------------------------------------------------------------------------
/src/CorsAllowAll.php:
--------------------------------------------------------------------------------
1 | handle($request);
21 |
22 | return $response
23 | ->withHeader(Header::ALLOW, '*')
24 | ->withHeader(Header::VARY, 'Origin')
25 | ->withHeader(Header::ACCESS_CONTROL_ALLOW_ORIGIN, '*')
26 | ->withHeader(Header::ACCESS_CONTROL_ALLOW_METHODS, 'GET,OPTIONS,HEAD,POST,PUT,PATCH,DELETE')
27 | ->withHeader(Header::ACCESS_CONTROL_ALLOW_HEADERS, '*')
28 | ->withHeader(Header::ACCESS_CONTROL_EXPOSE_HEADERS, '*')
29 | ->withHeader(Header::ACCESS_CONTROL_ALLOW_CREDENTIALS, 'true')
30 | ->withHeader(Header::ACCESS_CONTROL_MAX_AGE, '86400');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Event/SetLocaleEvent.php:
--------------------------------------------------------------------------------
1 | locale;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Exception/BadUriPrefixException.php:
--------------------------------------------------------------------------------
1 | value` array. For example:
21 | ```
22 | ['en' => 'en-US', 'uz' => 'uz-UZ'];
23 | // or
24 | ['en' => 'en_US', 'uz' => 'uz_UZ'];
25 | ```
26 | SOLUTION;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/ForceSecureConnection.php:
--------------------------------------------------------------------------------
1 | redirect = true;
59 | $new->port = $port;
60 | $new->statusCode = $statusCode;
61 | return $new;
62 | }
63 |
64 | /**
65 | * Returns a new instance and disables redirection from HTTP to HTTPS.
66 | *
67 | * @see withRedirection()
68 | */
69 | public function withoutRedirection(): self
70 | {
71 | $new = clone $this;
72 | $new->redirect = false;
73 | return $new;
74 | }
75 |
76 | /**
77 | * Returns a new instance with added the `Content-Security-Policy` header to response.
78 | *
79 | * @param string $directives The directives {@see DEFAULT_CSP_DIRECTIVES}.
80 | *
81 | * @see Header::CONTENT_SECURITY_POLICY
82 | */
83 | public function withCSP(string $directives = self::DEFAULT_CSP_DIRECTIVES): self
84 | {
85 | $new = clone $this;
86 | $new->addCSP = true;
87 | $new->cspDirectives = $directives;
88 | return $new;
89 | }
90 |
91 | /**
92 | * Returns a new instance without the `Content-Security-Policy` header in response.
93 | *
94 | * @see withCSP()
95 | */
96 | public function withoutCSP(): self
97 | {
98 | $new = clone $this;
99 | $new->addCSP = false;
100 | return $new;
101 | }
102 |
103 | /**
104 | * Returns a new instance with added the `Strict-Transport-Security` header to response.
105 | *
106 | * @param int $maxAge The max age {@see DEFAULT_HSTS_MAX_AGE}.
107 | * @param bool $subDomains Whether to add the `includeSubDomains` option to the header value.
108 | */
109 | public function withHSTS(int $maxAge = self::DEFAULT_HSTS_MAX_AGE, bool $subDomains = false): self
110 | {
111 | $new = clone $this;
112 | $new->addHSTS = true;
113 | $new->hstsMaxAge = $maxAge;
114 | $new->hstsSubDomains = $subDomains;
115 | return $new;
116 | }
117 |
118 | /**
119 | * Returns a new instance without the `Strict-Transport-Security` header in response.
120 | *
121 | * @see withHSTS()
122 | */
123 | public function withoutHSTS(): self
124 | {
125 | $new = clone $this;
126 | $new->addHSTS = false;
127 | return $new;
128 | }
129 |
130 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
131 | {
132 | if ($this->redirect && strcasecmp($request
133 | ->getUri()
134 | ->getScheme(), 'http') === 0) {
135 | $url = (string) $request
136 | ->getUri()
137 | ->withScheme('https')
138 | ->withPort($this->port);
139 |
140 | return $this->addHSTS(
141 | $this->responseFactory
142 | ->createResponse($this->statusCode)
143 | ->withHeader(Header::LOCATION, $url)
144 | );
145 | }
146 |
147 | return $this->addHSTS($this->addCSP($handler->handle($request)));
148 | }
149 |
150 | private function addCSP(ResponseInterface $response): ResponseInterface
151 | {
152 | return $this->addCSP
153 | ? $response->withHeader(Header::CONTENT_SECURITY_POLICY, $this->cspDirectives)
154 | : $response;
155 | }
156 |
157 | private function addHSTS(ResponseInterface $response): ResponseInterface
158 | {
159 | $subDomains = $this->hstsSubDomains ? '; includeSubDomains' : '';
160 | return $this->addHSTS
161 | ? $response->withHeader(Header::STRICT_TRANSPORT_SECURITY, "max-age={$this->hstsMaxAge}{$subDomains}")
162 | : $response;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/HttpCache.php:
--------------------------------------------------------------------------------
1 | lastModified = $lastModified;
62 | return $new;
63 | }
64 |
65 | /**
66 | * Returns a new instance with the specified callable that generates the ETag seed string.
67 | *
68 | * @param callable $etagSeed A PHP callback that generates the ETag seed string.
69 | *
70 | * The callback's signature should be:
71 | *
72 | * ```php
73 | * function (ServerRequestInterface $request, mixed $params): string;
74 | * ```
75 | *
76 | * Where `$request` is the {@see ServerRequestInterface} object that this middleware is currently handling;
77 | * `$params` takes the value of {@see withParams()}. The callback should return a string serving
78 | * as the seed for generating an ETag.
79 | */
80 | public function withEtagSeed(callable $etagSeed): self
81 | {
82 | $new = clone $this;
83 | $new->etagSeed = $etagSeed;
84 | return $new;
85 | }
86 |
87 | /**
88 | * Returns a new instance with weak ETags generation enabled. Disabled by default.
89 | *
90 | * You should use weak ETags if the content is semantically equal, but not byte-equal.
91 | *
92 | * @see https://tools.ietf.org/html/rfc7232#section-2.3
93 | */
94 | public function withWeakEtag(): self
95 | {
96 | $new = clone $this;
97 | $new->weakEtag = true;
98 | return $new;
99 | }
100 |
101 | /**
102 | * Returns a new instance with the specified extra parameters for ETag seed string generation.
103 | *
104 | * @param mixed $params Extra parameters for {@see withEtagSeed()} callbacks.
105 | */
106 | public function withParams(mixed $params): self
107 | {
108 | $new = clone $this;
109 | $new->params = $params;
110 | return $new;
111 | }
112 |
113 | /**
114 | * Returns a new instance with the specified value of the `Cache-Control` HTTP header.
115 | *
116 | * @param string|null $header The value of the `Cache-Control` HTTP header. If `null`, the header won't be sent.
117 | *
118 | * @see https://tools.ietf.org/html/rfc2616#section-14.9
119 | */
120 | public function withCacheControlHeader(?string $header): self
121 | {
122 | $new = clone $this;
123 | $new->cacheControlHeader = $header;
124 | return $new;
125 | }
126 |
127 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
128 | {
129 | if (
130 | ($this->lastModified === null && $this->etagSeed === null) ||
131 | !in_array($request->getMethod(), [Method::GET, Method::HEAD], true)
132 | ) {
133 | return $handler->handle($request);
134 | }
135 |
136 | /** @var int|null $lastModified */
137 | $lastModified = $this->lastModified === null ? null : ($this->lastModified)($request, $this->params);
138 | $etag = null;
139 |
140 | if ($this->etagSeed !== null) {
141 | /** @var string|null $seed */
142 | $seed = ($this->etagSeed)($request, $this->params);
143 |
144 | if ($seed !== null) {
145 | $etag = $this->generateEtag($seed);
146 | }
147 | }
148 |
149 | $response = $handler->handle($request);
150 |
151 | if ($this->cacheControlHeader !== null) {
152 | $response = $response->withHeader(Header::CACHE_CONTROL, $this->cacheControlHeader);
153 | }
154 | if ($etag !== null) {
155 | $response = $response->withHeader(Header::ETAG, $etag);
156 | }
157 |
158 | $cacheIsValid = $this->validateCache($request, $lastModified, $etag);
159 | if ($cacheIsValid) {
160 | $response = $response->withStatus(Status::NOT_MODIFIED);
161 | }
162 |
163 | /** @see https://tools.ietf.org/html/rfc7232#section-4.1 */
164 | if ($lastModified !== null && (!$cacheIsValid || $etag === null)) {
165 | $response = $response->withHeader(
166 | Header::LAST_MODIFIED,
167 | gmdate('D, d M Y H:i:s', $lastModified) . ' GMT',
168 | );
169 | }
170 |
171 | return $response;
172 | }
173 |
174 | /**
175 | * Validates if the HTTP cache has valid content.
176 | * If both `Last-Modified` and `ETag` are `null`, it returns `false`.
177 | *
178 | * @param ServerRequestInterface $request The server request instance.
179 | * @param int|null $lastModified The calculated Last-Modified value in terms of a UNIX timestamp.
180 | * If `null`, the `Last-Modified` header won't be validated.
181 | * @param string|null $etag The calculated `ETag` value. If `null`, the `ETag` header won't be validated.
182 | *
183 | * @return bool Whether the HTTP cache is still valid.
184 | */
185 | private function validateCache(ServerRequestInterface $request, ?int $lastModified, ?string $etag): bool
186 | {
187 | if ($request->hasHeader(Header::IF_NONE_MATCH)) {
188 | if ($etag === null) {
189 | return false;
190 | }
191 |
192 | $headerParts = preg_split(
193 | '/[\s,]+/',
194 | str_replace('-gzip', '', $request->getHeaderLine(Header::IF_NONE_MATCH)),
195 | flags: PREG_SPLIT_NO_EMPTY,
196 | );
197 |
198 | // "HTTP_IF_NONE_MATCH" takes precedence over "HTTP_IF_MODIFIED_SINCE".
199 | // https://tools.ietf.org/html/rfc7232#section-3.3
200 | return $headerParts !== false && in_array($etag, $headerParts, true);
201 | }
202 |
203 | if ($request->hasHeader(Header::IF_MODIFIED_SINCE)) {
204 | $header = $request->getHeaderLine(Header::IF_MODIFIED_SINCE);
205 | return $lastModified !== null && @strtotime($header) >= $lastModified;
206 | }
207 |
208 | return false;
209 | }
210 |
211 | /**
212 | * Generates an ETag from the given seed string.
213 | *
214 | * @param string $seed Seed for the ETag.
215 | *
216 | * @return string The generated ETag.
217 | */
218 | private function generateEtag(string $seed): string
219 | {
220 | $etag = '"' . rtrim(base64_encode(sha1($seed, true)), '=') . '"';
221 | return $this->weakEtag ? 'W/' . $etag : $etag;
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/src/IpFilter.php:
--------------------------------------------------------------------------------
1 | $ipRanges
34 | */
35 | public function __construct(
36 | /**
37 | * @deprecated Will be removed in version 2.0. {@see IpRanges} from `network-utilities` package is used instead.
38 | */
39 | ValidatorInterface $validator,
40 | private readonly ResponseFactoryInterface $responseFactory,
41 | private readonly ?string $clientIpAttribute = null,
42 | array $ipRanges = [],
43 | ) {
44 | $this->ipRanges = new IpRanges($ipRanges);
45 | }
46 |
47 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
48 | {
49 | if ($this->clientIpAttribute !== null) {
50 | $clientIp = $request->getAttribute($this->clientIpAttribute);
51 | }
52 |
53 | /** @psalm-var array{REMOTE_ADDR?: mixed} $serverParams */
54 | $serverParams = $request->getServerParams();
55 | $clientIp ??= $serverParams['REMOTE_ADDR'] ?? null;
56 | if ($clientIp === null) {
57 | return $this->createForbiddenResponse();
58 | }
59 |
60 | if (!is_string($clientIp) || !IpHelper::isIp($clientIp)) {
61 | return $this->createForbiddenResponse();
62 | }
63 |
64 | if (!$this->ipRanges->isAllowed($clientIp)) {
65 | return $this->createForbiddenResponse();
66 | }
67 |
68 | return $handler->handle($request);
69 | }
70 |
71 | private function createForbiddenResponse(): ResponseInterface
72 | {
73 | $response = $this->responseFactory->createResponse(Status::FORBIDDEN);
74 | $response->getBody()->write(Status::TEXTS[Status::FORBIDDEN]);
75 |
76 | return $response;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Locale.php:
--------------------------------------------------------------------------------
1 |
47 | */
48 | private array $supportedLocales;
49 |
50 | /**
51 | * @param EventDispatcherInterface $eventDispatcher Event dispatcher instance to dispatch events.
52 | * @param UrlGeneratorInterface $urlGenerator URL generator instance to set locale for.
53 | * @param LoggerInterface $logger Logger instance to write debug logs to.
54 | * @param ResponseFactoryInterface $responseFactory Response factory used to create redirect responses.
55 | * @param array $supportedLocales List of supported locales in key-value format such as `['ru' => 'ru_RU', 'uz' => 'uz_UZ']`.
56 | * @param string[] $ignoredRequestUrlPatterns {@see WildcardPattern Patterns} for ignoring requests with URLs matching.
57 | * @param ?DateInterval $cookieDuration Locale cookie lifetime. `null` disables saving locale to cookies completely.
58 | * @param bool $secureCookie Whether middleware should flag locale cookie as secure. Effective only when
59 | * {@see $cookieDuration} isn't `null`.
60 | */
61 | public function __construct(
62 | private EventDispatcherInterface $eventDispatcher,
63 | private UrlGeneratorInterface $urlGenerator,
64 | private LoggerInterface $logger,
65 | private ResponseFactoryInterface $responseFactory,
66 | array $supportedLocales = [],
67 | private array $ignoredRequestUrlPatterns = [],
68 | private bool $secureCookie = false,
69 | private ?DateInterval $cookieDuration = null,
70 | private ?ClockInterface $clock = null,
71 | ) {
72 | $this->assertSupportedLocalesFormat($supportedLocales);
73 | $this->supportedLocales = $supportedLocales;
74 | }
75 |
76 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
77 | {
78 | if (empty($this->supportedLocales) || $this->isRequestIgnored($request)) {
79 | return $handler->handle($request);
80 | }
81 |
82 | $uri = $request->getUri();
83 | $path = $uri->getPath();
84 | $query = $uri->getQuery();
85 | $locale = $this->getLocaleFromPath($path);
86 |
87 | if ($locale !== null) {
88 | if ($locale === $this->defaultLocale && $request->getMethod() === Method::GET) {
89 | return $this->saveLocale(
90 | $locale,
91 | $this->createRedirectResponse(substr($path, strlen($locale) + 1) ?: '/', $query)
92 | );
93 | }
94 | } else {
95 | /** @psalm-var array $queryParameters */
96 | $queryParameters = $request->getQueryParams();
97 | $locale = $this->getLocaleFromQuery($queryParameters);
98 |
99 | if ($locale === null && $this->cookieDuration !== null) {
100 | /** @psalm-var array $cookieParameters */
101 | $cookieParameters = $request->getCookieParams();
102 | $locale = $this->getLocaleFromCookies($cookieParameters);
103 | }
104 |
105 | if ($locale === null && $this->detectLocale) {
106 | $locale = $this->detectLocale($request);
107 | }
108 |
109 | if ($locale === null || $locale === $this->defaultLocale) {
110 | $this->urlGenerator->setDefaultArgument($this->queryParameterName, null);
111 | $request = $request->withUri($uri->withPath('/' . $this->defaultLocale . $path));
112 |
113 | return $handler->handle($request);
114 | }
115 |
116 | if ($request->getMethod() === Method::GET) {
117 | return $this->createRedirectResponse('/' . $locale . $path, $query);
118 | }
119 | }
120 |
121 | /** @var string $locale */
122 | $this->eventDispatcher->dispatch(new SetLocaleEvent($this->supportedLocales[$locale]));
123 | $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
124 |
125 | $response = $handler->handle($request);
126 |
127 | return $this->saveLocale($locale, $response);
128 | }
129 |
130 | private function createRedirectResponse(string $path, string $query): ResponseInterface
131 | {
132 | return $this
133 | ->responseFactory
134 | ->createResponse(Status::FOUND)
135 | ->withHeader(
136 | Header::LOCATION,
137 | $this->getBaseUrl() . $path . ($query !== '' ? '?' . $query : '')
138 | );
139 | }
140 |
141 | private function getLocaleFromPath(string $path): ?string
142 | {
143 | $parts = [];
144 | foreach ($this->supportedLocales as $code => $locale) {
145 | $parts[] = $code;
146 | $parts[] = $locale;
147 | }
148 |
149 | $pattern = implode('|', $parts);
150 | if (preg_match("#^/($pattern)\b(/?)#i", $path, $matches)) {
151 | $matchedLocale = $matches[1];
152 | if (!isset($this->supportedLocales[$matchedLocale])) {
153 | $matchedLocale = $this->parseLocale($matchedLocale);
154 | }
155 | if (isset($this->supportedLocales[$matchedLocale])) {
156 | $this->logger->debug(sprintf("Locale '%s' found in URL.", $matchedLocale));
157 | return $matchedLocale;
158 | }
159 | }
160 | return null;
161 | }
162 |
163 | /**
164 | * @psalm-param array $queryParameters
165 | */
166 | private function getLocaleFromQuery($queryParameters): ?string
167 | {
168 | if (!isset($queryParameters[$this->queryParameterName])) {
169 | return null;
170 | }
171 |
172 | $this->logger->debug(
173 | sprintf("Locale '%s' found in query string.", $queryParameters[$this->queryParameterName]),
174 | );
175 |
176 | return $this->parseLocale($queryParameters[$this->queryParameterName]);
177 | }
178 |
179 | /**
180 | * @psalm-param array $cookieParameters
181 | */
182 | private function getLocaleFromCookies($cookieParameters): ?string
183 | {
184 | if (!isset($cookieParameters[$this->cookieName])) {
185 | return null;
186 | }
187 |
188 | $this->logger->debug(sprintf("Locale '%s' found in cookies.", $cookieParameters[$this->cookieName]));
189 |
190 | return $this->parseLocale($cookieParameters[$this->cookieName]);
191 | }
192 |
193 | private function detectLocale(ServerRequestInterface $request): ?string
194 | {
195 | foreach ($request->getHeader(Header::ACCEPT_LANGUAGE) as $language) {
196 | if (!isset($this->supportedLocales[$language])) {
197 | $language = $this->parseLocale($language);
198 | }
199 | if (isset($this->supportedLocales[$language])) {
200 | return $language;
201 | }
202 | }
203 | return null;
204 | }
205 |
206 | private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
207 | {
208 | if ($this->cookieDuration === null) {
209 | return $response;
210 | }
211 |
212 | $this->logger->debug('Saving found locale to cookies.');
213 | $cookie = new Cookie(
214 | name: $this->cookieName,
215 | value: $locale,
216 | secure: $this->secureCookie,
217 | clock: $this->clock,
218 | );
219 | $cookie = $cookie->withMaxAge($this->cookieDuration);
220 |
221 | return $cookie->addToResponse($response);
222 | }
223 |
224 | private function parseLocale(string $locale): string
225 | {
226 | foreach (self::LOCALE_SEPARATORS as $separator) {
227 | $separatorPosition = strpos($locale, $separator);
228 | if ($separatorPosition !== false) {
229 | return substr($locale, 0, $separatorPosition);
230 | }
231 | }
232 |
233 | return $locale;
234 | }
235 |
236 | private function isRequestIgnored(ServerRequestInterface $request): bool
237 | {
238 | foreach ($this->ignoredRequestUrlPatterns as $ignoredRequest) {
239 | if ((new WildcardPattern($ignoredRequest))->match($request->getUri()->getPath())) {
240 | return true;
241 | }
242 | }
243 | return false;
244 | }
245 |
246 | /**
247 | * @psalm-assert array $supportedLocales
248 | *
249 | * @throws InvalidLocalesFormatException
250 | */
251 | private function assertSupportedLocalesFormat(array $supportedLocales): void
252 | {
253 | foreach ($supportedLocales as $code => $locale) {
254 | if (!is_string($code) || !is_string($locale)) {
255 | throw new InvalidLocalesFormatException();
256 | }
257 | }
258 | }
259 |
260 | private function getBaseUrl(): string
261 | {
262 | return rtrim($this->urlGenerator->getUriPrefix(), '/');
263 | }
264 |
265 | /**
266 | * Return new instance with supported locales specified.
267 | *
268 | * @param array $locales List of supported locales in key-value format such as `['ru' => 'ru_RU', 'uz' => 'uz_UZ']`.
269 | *
270 | * @throws InvalidLocalesFormatException
271 | */
272 | public function withSupportedLocales(array $locales): self
273 | {
274 | $this->assertSupportedLocalesFormat($locales);
275 | $new = clone $this;
276 | $new->supportedLocales = $locales;
277 | return $new;
278 | }
279 |
280 | /**
281 | * Return new instance with default locale specified.
282 | *
283 | * @param string $defaultLocale Default locale. It must be present as a key in {@see $supportedLocales}.
284 | */
285 | public function withDefaultLocale(string $defaultLocale): self
286 | {
287 | if (!array_key_exists($defaultLocale, $this->supportedLocales)) {
288 | throw new InvalidArgumentException('Default locale allows only keys from supported locales.');
289 | }
290 |
291 | $new = clone $this;
292 | $new->defaultLocale = $defaultLocale;
293 | return $new;
294 | }
295 |
296 | /**
297 | * Return new instance with the name of the query string parameter to look for locale.
298 | *
299 | * @param string $queryParameterName Name of the query string parameter.
300 | */
301 | public function withQueryParameterName(string $queryParameterName): self
302 | {
303 | $new = clone $this;
304 | $new->queryParameterName = $queryParameterName;
305 | return $new;
306 | }
307 |
308 | /**
309 | * Return new instance with the name of cookie parameter to store found locale. Effective only when
310 | * {@see $cookieDuration} isn't `null`.
311 | *
312 | * @param string $cookieName Name of cookie parameter.
313 | */
314 | public function withCookieName(string $cookieName): self
315 | {
316 | $new = clone $this;
317 | $new->cookieName = $cookieName;
318 | return $new;
319 | }
320 |
321 | /**
322 | * Return new instance with enabled or disabled detection of locale based on `Accept-Language` header.
323 | *
324 | * @param bool $enabled Whether middleware should detect locale.
325 | */
326 | public function withDetectLocale(bool $enabled): self
327 | {
328 | $new = clone $this;
329 | $new->detectLocale = $enabled;
330 | return $new;
331 | }
332 |
333 | /**
334 | * Return new instance with {@see WildcardPattern patterns} for ignoring requests with URLs matching.
335 | *
336 | * @param string[] $patterns Patterns.
337 | */
338 | public function withIgnoredRequestUrlPatterns(array $patterns): self
339 | {
340 | $new = clone $this;
341 | $new->ignoredRequestUrlPatterns = $patterns;
342 | return $new;
343 | }
344 |
345 | /**
346 | * Return new instance with enabled or disabled secure cookies.
347 | *
348 | * @param bool $secure Whether middleware should flag locale cookie as secure.
349 | */
350 | public function withSecureCookie(bool $secure): self
351 | {
352 | $new = clone $this;
353 | $new->secureCookie = $secure;
354 | return $new;
355 | }
356 |
357 | /**
358 | * Return new instance with changed cookie duration.
359 | *
360 | * @param ?DateInterval $cookieDuration Locale cookie lifetime. When set to `null`, saving locale to cookies is
361 | * disabled completely.
362 | */
363 | public function withCookieDuration(?DateInterval $cookieDuration): self
364 | {
365 | $new = clone $this;
366 | $new->cookieDuration = $cookieDuration;
367 | return $new;
368 | }
369 | }
370 |
--------------------------------------------------------------------------------
/src/Redirect.php:
--------------------------------------------------------------------------------
1 | $parameters
25 | */
26 | private array $parameters = [];
27 | private int $statusCode = Status::MOVED_PERMANENTLY;
28 |
29 | public function __construct(
30 | private ResponseFactoryInterface $responseFactory,
31 | private UrlGeneratorInterface $urlGenerator,
32 | ) {
33 | }
34 |
35 | /**
36 | * Returns a new instance with the specified URL for redirection.
37 | *
38 | * @param string $url URL for redirection.
39 | */
40 | public function toUrl(string $url): self
41 | {
42 | $new = clone $this;
43 | $new->uri = $url;
44 | return $new;
45 | }
46 |
47 | /**
48 | * Returns a new instance with the specified route data for redirection.
49 | *
50 | * If you've set a redirect URL with {@see toUrl()}, the middleware ignores the route data, since the URL
51 | * is a priority.
52 | *
53 | * @param string $name The route name for redirection.
54 | * @param array $parameters $parameters The route parameters for redirection.
55 | */
56 | public function toRoute(string $name, array $parameters = []): self
57 | {
58 | $new = clone $this;
59 | $new->route = $name;
60 | $new->parameters = $parameters;
61 | return $new;
62 | }
63 |
64 | /**
65 | * Returns a new instance with the specified status code of the response for redirection.
66 | *
67 | * @param int $statusCode The status code of the response for redirection.
68 | */
69 | public function withStatus(int $statusCode): self
70 | {
71 | $new = clone $this;
72 | $new->statusCode = $statusCode;
73 | return $new;
74 | }
75 |
76 | /**
77 | * Returns a new instance with the response status code of permanent redirection.
78 | *
79 | * @see Status::MOVED_PERMANENTLY
80 | */
81 | public function permanent(): self
82 | {
83 | $new = clone $this;
84 | $new->statusCode = Status::MOVED_PERMANENTLY;
85 | return $new;
86 | }
87 |
88 | /**
89 | * Returns a new instance with the response status code of temporary redirection.
90 | *
91 | * @see Status::FOUND
92 | */
93 | public function temporary(): self
94 | {
95 | $new = clone $this;
96 | $new->statusCode = Status::FOUND;
97 | return $new;
98 | }
99 |
100 | /**
101 | * @inheritDoc
102 | *
103 | * @throws RuntimeException If the data for redirection wasn't set earlier.
104 | */
105 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
106 | {
107 | if ($this->route === null && $this->uri === null) {
108 | throw new RuntimeException('Either `toUrl()` or `toRoute()` method should be used.');
109 | }
110 |
111 | /** @psalm-suppress PossiblyNullArgument */
112 | $uri = $this->uri ?? $this->urlGenerator->generate($this->route, $this->parameters);
113 |
114 | return $this->responseFactory
115 | ->createResponse($this->statusCode)
116 | ->withAddedHeader('Location', $uri);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Subfolder.php:
--------------------------------------------------------------------------------
1 | getUri();
44 | $path = $uri->getPath();
45 | $baseUrl = $this->prefix ?? $this->getBaseUrl($request);
46 | $length = strlen($baseUrl);
47 |
48 | if ($this->prefix !== null) {
49 | if (empty($this->prefix)) {
50 | throw new BadUriPrefixException('URI prefix can\'t be empty.');
51 | }
52 |
53 | if ($baseUrl[-1] === '/') {
54 | throw new BadUriPrefixException('Wrong URI prefix value.');
55 | }
56 |
57 | if (!str_starts_with($path, $baseUrl)) {
58 | throw new BadUriPrefixException('URI prefix doesn\'t match.');
59 | }
60 | }
61 |
62 | if ($length > 0) {
63 | $newPath = substr($path, $length);
64 |
65 | if ($newPath === '') {
66 | $newPath = '/';
67 | }
68 |
69 | if ($newPath[0] === '/') {
70 | $request = $request->withUri($uri->withPath($newPath));
71 | $this->uriGenerator->setUriPrefix($baseUrl);
72 | if ($this->baseUrlAlias !== null && $this->prefix === null) {
73 | $this->aliases->set($this->baseUrlAlias, $baseUrl);
74 | }
75 | } elseif ($this->prefix !== null) {
76 | throw new BadUriPrefixException('URI prefix doesn\'t match completely.');
77 | }
78 | }
79 |
80 | return $handler->handle($request);
81 | }
82 |
83 | private function getBaseUrl(ServerRequestInterface $request): string
84 | {
85 | /**
86 | * @var array{
87 | * SCRIPT_FILENAME?:string,
88 | * PHP_SELF?:string,
89 | * ORIG_SCRIPT_NAME?:string,
90 | * DOCUMENT_ROOT?:string
91 | * } $serverParams
92 | */
93 | $serverParams = $request->getServerParams();
94 | $scriptUrl = $serverParams['SCRIPT_FILENAME'] ?? '/index.php';
95 | $scriptName = basename($scriptUrl);
96 |
97 | if (isset($serverParams['PHP_SELF']) && basename($serverParams['PHP_SELF']) === $scriptName) {
98 | $scriptUrl = $serverParams['PHP_SELF'];
99 | } elseif (
100 | isset($serverParams['ORIG_SCRIPT_NAME']) &&
101 | basename($serverParams['ORIG_SCRIPT_NAME']) === $scriptName
102 | ) {
103 | $scriptUrl = $serverParams['ORIG_SCRIPT_NAME'];
104 | } elseif (
105 | isset($serverParams['PHP_SELF']) &&
106 | ($pos = strpos($serverParams['PHP_SELF'], $scriptName)) !== false
107 | ) {
108 | $scriptUrl = substr($serverParams['PHP_SELF'], 0, $pos + strlen($scriptName));
109 | } elseif (
110 | !empty($serverParams['DOCUMENT_ROOT']) &&
111 | str_starts_with($scriptUrl, $serverParams['DOCUMENT_ROOT'])
112 | ) {
113 | $scriptUrl = str_replace([$serverParams['DOCUMENT_ROOT'], '\\'], ['', '/'], $scriptUrl);
114 | }
115 |
116 | return rtrim(dirname($scriptUrl), '\\/');
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/TagRequest.php:
--------------------------------------------------------------------------------
1 | handle($request->withAttribute('requestTag', $this->getRequestTag()));
22 | }
23 |
24 | private function getRequestTag(): string
25 | {
26 | return uniqid('', true);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------