MaintenanceMiddleware::getCookieManager()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 3
b 0
f 0
nc 1
nop 0
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\RequestData;
57
use Platine\Framework\Http\Response\RedirectResponse;
58
use Platine\Framework\Http\Response\TemplateResponse;
59
use Platine\Framework\Http\RouteHelper;
60
use Platine\Http\Handler\MiddlewareInterface;
61
use Platine\Http\Handler\RequestHandlerInterface;
62
use Platine\Http\ResponseInterface;
63
use Platine\Http\ServerRequestInterface;
64
use Platine\Stdlib\Helper\Json;
65
use Platine\Stdlib\Helper\Str;
66
use Platine\Template\Template;
67
use Throwable;
68
69
/**
70
 * @class MaintenanceMiddleware
71
 * @package Platine\Framework\Http\Middleware
72
 * @template T
73
 */
74
class MaintenanceMiddleware implements MiddlewareInterface
75
{
76
    /**
77
     * Create new instance
78
     * @param Config<T> $config
79
     * @param Application $app
80
     */
81
    public function __construct(
82
        protected Config $config,
83
        protected Application $app
84
    ) {
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function process(
91
        ServerRequestInterface $request,
92
        RequestHandlerInterface $handler
93
    ): ResponseInterface {
94
95
        if ($this->isExceptUrl($request)) {
96
            return $handler->handle($request);
97
        }
98
99
        if ($this->app->isInMaintenance()) {
100
            try {
101
                $data = $this->app->maintenance()->data();
102
            } catch (Throwable $ex) {
103
                throw $ex;
104
            }
105
106
            if (
107
                    isset($data['secret']) && trim($request->getUri()->getPath(), '/') === $data['secret']
108
            ) {
109
                return $this->bypassResponse($data['secret']);
110
            }
111
112
            if ($this->hasValidBypassCookie($request, $data)) {
113
                return $handler->handle($request);
114
            }
115
116
            if (isset($data['template'])) {
117
                return $this->templateResponse(
118
                    $data['template'],
119
                    $data
120
                );
121
            }
122
123
            $message = $data['message'] ?? 'The server is temporarily busy, try again later';
124
            $httpException = new HttpException(
125
                $request,
126
                $message,
127
                $data['status'] ?? 503
128
            );
129
130
            $httpException->setTitle('Service Unavailable')
131
                          ->setDescription($message)
132
                          ->setHeaders($this->getHeaders($data));
133
134
            throw $httpException;
135
        }
136
137
        return $handler->handle($request);
138
    }
139
140
    protected function bypassResponse(string $secret): ResponseInterface
141
    {
142
        $routeHelper = $this->getRouteHelper();
143
        $bypassRouteName = $this->config->get('maintenance.bypass_route', '');
144
        $url = $routeHelper->generateUrl($bypassRouteName);
145
146
        // Cookie
147
        $manager = $this->getCookieManager();
148
        $cookie = $this->createBypassCookie($secret);
149
150
        $manager->add($cookie);
151
        $redirectResponse = new RedirectResponse($url);
152
153
        return $manager->send($redirectResponse);
154
    }
155
156
    /**
157
     * Return the template response
158
     * @param string $template
159
     * @param array<string, mixed> $data
160
     * @return ResponseInterface
161
     */
162
    protected function templateResponse(string $template, array $data): ResponseInterface
163
    {
164
        $response = new TemplateResponse(
165
            $this->getTemplate(),
166
            $template,
167
            $data,
168
            $data['status'] ?? 503
169
        );
170
171
        foreach ($this->getHeaders($data) as $name => $value) {
172
            $response = $response->withAddedHeader($name, $value);
173
        }
174
175
        return $response;
176
    }
177
178
    /**
179
     * Check whether it's request URL white list
180
     * @param ServerRequestInterface $request
181
     * @return bool
182
     */
183
    protected function isExceptUrl(ServerRequestInterface $request): bool
184
    {
185
        $path = $request->getUri()->getPath();
186
        foreach ($this->getExceptUrls() as $url) {
187
            if (Str::is($url, $path)) {
188
                return true;
189
            }
190
        }
191
192
        return false;
193
    }
194
195
    /**
196
     * Return the except URLs
197
     * @return array<string, string>
198
     */
199
    protected function getExceptUrls(): array
200
    {
201
        return $this->config->get('maintenance.url_whitelist', []);
202
    }
203
204
    /**
205
     * Whether has valid bypass cookie
206
     * @param ServerRequestInterface $request
207
     * @param array<string, mixed> $data
208
     * @return bool
209
     */
210
    protected function hasValidBypassCookie(ServerRequestInterface $request, array $data): bool
211
    {
212
        if (!isset($data['secret'])) {
213
            return false;
214
        }
215
216
        $secret = $data['secret'];
217
        $name = $this->getCookieName();
218
        $cookieValue = (new RequestData($request))->cookie($name);
219
        if (empty($cookieValue)) {
220
            return false;
221
        }
222
223
        $payload = Json::decode(base64_decode($cookieValue), true);
224
225
        return is_array($payload) &&
226
                is_int($payload['expire'] ?? null) &&
227
                isset($payload['hash']) &&
228
                hash_equals(hash_hmac(
229
                    'sha256',
230
                    (string) $payload['expire'],
231
                    $secret
232
                ), $payload['hash']) &&
233
                (int) $payload['expire'] >= time();
234
    }
235
236
    /**
237
     * Return the bypass cookie
238
     * @param string $secret
239
     * @return Cookie
240
     */
241
    protected function createBypassCookie(string $secret): Cookie
242
    {
243
        $name = $this->getCookieName();
244
        $expire = time() + $this->config->get('maintenance.cookie.lifetime', 43200);
245
        $data = [
246
            'expire' => $expire,
247
            'hash' => hash_hmac('sha256', (string) $expire, $secret)
248
        ];
249
        $value = base64_encode(Json::encode($data));
250
        $sessionCookieConfig = $this->config->get('session.cookie', []);
251
252
        $cookie = new Cookie(
253
            $name,
254
            $value,
255
            $expire,
256
            $sessionCookieConfig['domain'] ?? null,
257
            $sessionCookieConfig['path'] ?? null,
258
            $sessionCookieConfig['secure'] ?? false,
259
            true
260
        );
261
262
        return $cookie;
263
    }
264
265
    /**
266
     * Return the headers to be added in the response
267
     * @param array<string, mixed> $data
268
     * @return array<string, mixed>
269
     */
270
    protected function getHeaders(array $data): array
271
    {
272
        $headers = isset($data['retry']) ? ['Retry-After' => (string) $data['retry']] : [];
273
        if (isset($data['refresh'])) {
274
            $headers['Refresh'] = (string) $data['refresh'];
275
        }
276
277
        return $headers;
278
    }
279
280
    /**
281
     * Return the template instance
282
     * @return Template
283
     */
284
    protected function getTemplate(): Template
285
    {
286
        return $this->app->get(Template::class);
287
    }
288
289
    /**
290
     * Return the route helper instance
291
     * @return RouteHelper
292
     */
293
    protected function getRouteHelper(): RouteHelper
294
    {
295
        return $this->app->get(RouteHelper::class);
296
    }
297
298
    /**
299
     * Return the Cookie Manager instance
300
     * @return CookieManager
301
     */
302
    protected function getCookieManager(): CookieManager
303
    {
304
        return $this->app->get(CookieManager::class);
305
    }
306
307
    /**
308
     * Return the cookie name
309
     * @return string
310
     */
311
    protected function getCookieName(): string
312
    {
313
        return $this->config->get(
314
            'maintenance.cookie.name',
315
            'platine_maintenance'
316
        );
317
    }
318
}
319