Passed
Push — develop ( da958d...4e6cc2 )
by nguereza
03:00
created

MaintenanceMiddleware::hasValidBypassCookie()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * Platine Framework
5
 *
6
 * Platine Framework is a lightweight, high-performance, simple and elegant PHP
7
 * Web framework
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Framework
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file MaintenanceMiddleware.php
34
 *
35
 *  The Maintenance middleware class is used to check application maintenance and
36
 * handle the request if needed
37
 *
38
 *  @package    Platine\Framework\Http\Middleware
39
 *  @author Platine Developers Team
40
 *  @copyright  Copyright (c) 2020
41
 *  @license    http://opensource.org/licenses/MIT  MIT License
42
 *  @link   https://www.platine-php.com
43
 *  @version 1.0.0
44
 *  @filesource
45
 */
46
47
declare(strict_types=1);
48
49
namespace Platine\Framework\Http\Middleware;
50
51
use Platine\Config\Config;
52
use Platine\Cookie\Cookie;
53
use Platine\Cookie\CookieManager;
54
use Platine\Framework\App\Application;
55
use Platine\Framework\Http\Exception\HttpException;
56
use Platine\Framework\Http\Response\RedirectResponse;
57
use Platine\Framework\Http\Response\TemplateResponse;
58
use Platine\Framework\Http\RouteHelper;
59
use Platine\Http\Handler\MiddlewareInterface;
60
use Platine\Http\Handler\RequestHandlerInterface;
61
use Platine\Http\ResponseInterface;
62
use Platine\Http\ServerRequestInterface;
63
use Platine\Stdlib\Helper\Json;
64
use Platine\Stdlib\Helper\Str;
65
use Platine\Template\Template;
66
use Throwable;
67
68
/**
69
 * @class MaintenanceMiddleware
70
 * @package Platine\Framework\Http\Middleware
71
 * @template T
72
 */
73
class MaintenanceMiddleware implements MiddlewareInterface
74
{
75
    /**
76
     * The configuration instance
77
     * @var Config<T>
78
     */
79
    protected Config $config;
80
81
    /**
82
     * The Application instance
83
     * @var Application
84
     */
85
    protected Application $app;
86
87
    /**
88
     * Create new instance
89
     * @param Config<T> $config
90
     * @param Application $app
91
     */
92
    public function __construct(
93
        Config $config,
94
        Application $app
95
    ) {
96
        $this->config = $config;
97
        $this->app = $app;
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function process(
104
        ServerRequestInterface $request,
105
        RequestHandlerInterface $handler
106
    ): ResponseInterface {
107
108
        if ($this->isExceptUrl($request)) {
109
            return $handler->handle($request);
110
        }
111
112
        if ($this->app->isInMaintenance()) {
113
            try {
114
                $data = $this->app->maintenance()->data();
115
            } catch (Throwable $ex) {
116
                throw $ex;
117
            }
118
119
            if (isset($data['secret']) && trim($request->getUri()->getPath(), '/') === $data['secret']) {
120
                return $this->bypassResponse($data['secret']);
121
            }
122
123
            if ($this->hasValidBypassCookie($request, $data)) {
124
                return $handler->handle($request);
125
            }
126
127
            if (isset($data['template'])) {
128
                return $this->templateResponse($data['template'], $data);
129
            }
130
131
            $message = $data['message'] ?? 'The server is temporarily busy, try again later';
132
133
            $httpException = new HttpException(
134
                $request,
135
                $message,
136
                $data['status'] ?? 503
137
            );
138
139
            $httpException->setTitle('Service Unavailable')
140
                          ->setDescription($message);
141
142
            // TODO Add headers
143
            throw $httpException;
144
        }
145
146
        return $handler->handle($request);
147
    }
148
149
    protected function bypassResponse(string $secret): ResponseInterface
150
    {
151
        $routeHelper = $this->getRouteHelper();
152
        $bypassRouteName = $this->config->get('maintenance.bypass_route', '');
153
        $url = $routeHelper->generateUrl($bypassRouteName);
154
155
        // Cookie
156
        $manager = $this->getCookieManager();
157
        $cookie = $this->createBypassCookie($secret);
158
159
        $manager->add($cookie);
160
        $redirectResponse = new RedirectResponse($url);
161
162
        return $manager->send($redirectResponse);
163
    }
164
165
    /**
166
     * Return the template response
167
     * @param string $template
168
     * @param array<string, mixed> $data
169
     * @return ResponseInterface
170
     */
171
    protected function templateResponse(string $template, array $data): ResponseInterface
172
    {
173
        $response = new TemplateResponse(
174
            $this->getTemplate(),
175
            $template,
176
            $data,
177
            $data['status'] ?? 503
178
        );
179
180
        foreach ($this->getHeaders($data) as $name => $value) {
181
            $response = $response->withAddedHeader($name, $value);
182
        }
183
184
        return $response;
185
    }
186
187
    /**
188
     * Whether has valid bypass cookie
189
     * @param ServerRequestInterface $request
190
     * @param array<string, mixed> $data
191
     * @return bool
192
     */
193
    protected function hasValidBypassCookie(ServerRequestInterface $request, array $data): bool
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

193
    protected function hasValidBypassCookie(ServerRequestInterface $request, /** @scrutinizer ignore-unused */ array $data): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

193
    protected function hasValidBypassCookie(/** @scrutinizer ignore-unused */ ServerRequestInterface $request, array $data): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
194
    {
195
        return false;
196
    }
197
198
    /**
199
     * Check whether it's request URL white list
200
     * @param ServerRequestInterface $request
201
     * @return bool
202
     */
203
    protected function isExceptUrl(ServerRequestInterface $request): bool
204
    {
205
        $path = $request->getUri()->getPath();
206
        foreach ($this->getExceptUrls() as $url) {
207
            if (Str::is($url, $path)) {
208
                return true;
209
            }
210
        }
211
212
        return false;
213
    }
214
215
    /**
216
     * Return the except URLs
217
     * @return array<string, string>
218
     */
219
    protected function getExceptUrls(): array
220
    {
221
        return $this->config->get('maintenance.url_whitelist', []);
222
    }
223
224
    /**
225
     * Return the bypass cookie
226
     * @param string $secret
227
     * @return Cookie
228
     */
229
    protected function createBypassCookie(string $secret): Cookie
230
    {
231
        $name = $this->getCookieName();
232
        $expire = time() + $this->config->get('maintenance.cookie.lifetime', 43200);
233
        $data = [
234
            'expire' => $expire,
235
            'hash' => hash_hmac('sha256', (string) $expire, $secret)
236
        ];
237
        $value = base64_encode(Json::encode($data));
238
239
        $cookie = new Cookie($name, $value);
240
241
        $sessionCookieConfig = $this->config->get('session.cookie', []);
242
243
        $cookie->withExpires($expire)
244
               ->withPath($sessionCookieConfig['path'] ?? null)
245
               ->withDomain($sessionCookieConfig['domain'] ?? null)
246
               ->withHttpOnly(true);
247
248
249
        return $cookie;
250
    }
251
252
    /**
253
     * Return the headers to be added in the response
254
     * @param array<string, mixed> $data
255
     * @return array<string, mixed>
256
     */
257
    protected function getHeaders(array $data): array
258
    {
259
        $headers = isset($data['retry']) ? ['Retry-After' => $data['retry']] : [];
260
        if (isset($data['refresh'])) {
261
            $headers['Refresh'] = $data['refresh'];
262
        }
263
264
        return $headers;
265
    }
266
267
    /**
268
     * Return the template instance
269
     * @return Template
270
     */
271
    protected function getTemplate(): Template
272
    {
273
        return $this->app->get(Template::class);
274
    }
275
276
    /**
277
     * Return the route helper instance
278
     * @return RouteHelper
279
     */
280
    protected function getRouteHelper(): RouteHelper
281
    {
282
        return $this->app->get(RouteHelper::class);
283
    }
284
285
    /**
286
     * Return the Cookie Manager instance
287
     * @return CookieManager
288
     */
289
    protected function getCookieManager(): CookieManager
290
    {
291
        return $this->app->get(CookieManager::class);
292
    }
293
294
    /**
295
     * Return the cookie name
296
     * @return string
297
     */
298
    protected function getCookieName(): string
299
    {
300
        return $this->config->get('maintenance.cookie.name', 'platine_maintenance');
301
    }
302
}
303