RestController::respondFail()   B
last analyzed

Complexity

Conditions 9
Paths 96

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
cc 9
eloc 13
c 0
b 0
f 0
nc 96
nop 4
dl 0
loc 23
ccs 0
cts 8
cp 0
crap 90
rs 8.0555
1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Controllers;
13
14
use BlitzPHP\Annotations\AnnotationReader;
15
use BlitzPHP\Annotations\Http\AjaxOnlyAnnotation;
16
use BlitzPHP\Annotations\Http\RequestMappingAnnotation;
17
use BlitzPHP\Contracts\Http\StatusCode;
18
use BlitzPHP\Exceptions\ValidationException;
19
use BlitzPHP\Formatter\Formatter;
20
use BlitzPHP\Traits\Http\ApiResponseTrait;
21
use BlitzPHP\Utilities\Jwt;
22
use Exception;
23
use mindplay\annotations\IAnnotation;
24
use Psr\Http\Message\ResponseInterface;
25
use stdClass;
26
use Throwable;
27
28
/**
29
 * Le contrôleur de base pour les API REST
30
 */
31
class RestController extends BaseController
32
{
33
    use ApiResponseTrait;
34
35
    /**
36
     * Configurations
37
     *
38
     * @var stdClass
39
     */
40
    protected $config;
41
42
    /**
43
     * Langue à utiliser
44
     *
45
     * @var string
46
     */
47
    private $locale;
48
49
    /**
50
     * Type mime associé à chaque format de sortie
51
     *
52
     * Répertoriez tous les formats pris en charge, le première sera le format par défaut.
53
     */
54
    protected $mimes = [
55
        'json' => 'application/json',
56
        'csv'  => 'application/csv',
57
        // 'html'       => 'text/html',
58
        'jsonp'      => 'application/javascript',
59
        'php'        => 'text/plain',
60
        'serialized' => 'application/vnd.php.serialized',
61
        'xml'        => 'application/xml',
62
63
        'array' => 'php/array',
64
    ];
65
66
    /**
67
     * @var array|object Payload provenant du token jwt
68
     */
69
    protected $payload;
70
71
    public function __construct()
72
    {
73
        $this->config = (object) config('rest');
74
75
        $locale       = $this->config->language ?? null;
76
        $this->locale = ! empty($locale) ? $locale : $this->request->getLocale();
77
    }
78
79
    public function _remap(string $method, array $params = [])
80
    {
81
        $class = static::class;
82
83
        // Bien sûr qu'il existe, mais peuvent-ils en faire quelque chose ?
84
        if (! method_exists($class, $method)) {
85
            return $this->respondNotImplemented($this->_translate('notImplemented', [$class, $method]));
86
        }
87
88
        // Appel de la méthode du contrôleur et passage des arguments
89
        try {
90
            $instance = service('container')->get($class);
91
            $instance->initialize($this->request, $this->response, $this->logger);
92
93
            $instance = $this->_execAnnotations($instance, AnnotationReader::fromClass($instance));
94
            $instance = $this->_execAnnotations($instance, AnnotationReader::fromMethod($instance, $method));
95
96
            $checkProcess = $this->checkProcess();
97
            if ($checkProcess instanceof ResponseInterface) {
0 ignored issues
show
introduced by
$checkProcess is always a sub-type of Psr\Http\Message\ResponseInterface.
Loading history...
98
                return $checkProcess;
99
            }
100
101
            $instance->payload = $this->payload;
102
103
            $response = service('container')->call([$instance, $method], $params);
104
105
            if ($response instanceof ResponseInterface) {
106
                return $response;
107
            }
108
109
            return $this->respondOk($response);
110
        } catch (Throwable $ex) {
111
            return $this->manageException($ex);
112
        }
113
    }
114
115
    /**
116
     * Gestionnaire des exceptions
117
     *
118
     * Ceci permet aux classes filles de specifier comment elles doivent gerer les exceptions lors de la methode remap
119
     *
120
     * @return ResponseInterface
121
     */
122
    protected function manageException(Throwable $ex)
123
    {
124
        if ($ex instanceof ValidationException) {
125
            $message = 'Validation failed';
126
            $errors  = $ex->getErrors()->all();
127
128
            return $this->respondBadRequest($message, $ex->getCode(), $errors);
129
        }
130
131
        if (! on_dev()) {
132
            $url = explode('?', $this->request->getRequestTarget())[0];
133
134
            return $this->respondBadRequest($this->_translate('badUsed', [$url]));
135
        }
136
137
        return $this->respondInternalError('Internal Server Error', [
138
            'type'    => $ex::class,
139
            'message' => $ex->getMessage(),
140
            'code'    => $ex->getCode(),
141
            'file'    => $ex->getFile(),
142
            'line'    => $ex->getLine(),
143
        ]);
144
    }
145
146
    /**
147
     * Fournit une méthode simple et unique pour renvoyer une réponse d'API, formatée
148
     * pour correspondre au format demandé, avec le type de contenu et le code d'état appropriés.
149
     *
150
     * @param mixed    $data   Les donnees a renvoyer
151
     * @param int|null $status Le statut de la reponse
152
     */
153
    final protected function respond($data, ?int $status = StatusCode::OK)
154
    {
155
        // Si les données sont NULL et qu'aucun code d'état HTTP n'est fourni, affichage, erreur et sortie
156
        if ($data === null && $status === null) {
157
            $status = StatusCode::NOT_FOUND;
158
        }
159
160
        $this->response = $this->response->withStatus($status)->withCharset(strtolower(config('app.charset') ?? 'utf-8'));
0 ignored issues
show
Bug introduced by
It seems like config('app.charset') ?? 'utf-8' can also be of type null and object; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

160
        $this->response = $this->response->withStatus($status)->withCharset(strtolower(/** @scrutinizer ignore-type */ config('app.charset') ?? 'utf-8'));
Loading history...
161
162
        $this->_parseResponse($data);
163
164
        return $this->response;
165
    }
166
167
    /**
168
     * Utilisé pour les échecs génériques pour lesquels aucune méthode personnalisée n'existe.
169
     *
170
     * @param string          $message Le message décrivant l'erreur
171
     * @param int|string|null $code    Code d'erreur personnalisé, spécifique à l'API
172
     * @param array           $errors  La liste des erreurs rencontrées
173
     *
174
     * @return ResponseInterface
175
     */
176
    final protected function respondFail(?string $message = "Une erreur s'est produite", ?int $status = StatusCode::INTERNAL_ERROR, int|string|null $code = null, array $errors = [])
177
    {
178
        $message = $message ?: "Une erreur s'est produite";
179
        $code    = ($code !== 0 && $code !== '' && $code !== '0') ? $code : $status;
180
181
        $response = [
182
            $this->config->field['message'] ?? 'message' => $message,
183
        ];
184
        if (! empty($this->config->field['status'])) {
185
            $response[$this->config->field['status']] = false;
186
        }
187
        if (! empty($this->config->field['code'])) {
188
            $response[$this->config->field['code']] = $code;
189
        }
190
        if ($errors !== []) {
191
            $response[$this->config->field['errors'] ?? 'errors'] = $errors;
192
        }
193
194
        if ($this->config->strict !== true) {
195
            $status = StatusCode::OK;
196
        }
197
198
        return $this->respond($response, $status);
199
    }
200
201
    /**
202
     * Utilisé pour les succès génériques pour lesquels aucune méthode personnalisée n'existe.
203
     *
204
     * @param mixed|null $result Les données renvoyées par l'API
205
     *
206
     * @return ResponseInterface
207
     */
208
    final protected function respondSuccess(?string $message = 'Resultat', $result = null, ?int $status = StatusCode::OK)
209
    {
210
        $message = $message ?: 'Resultat';
211
        $status  = $status !== null && $status !== 0 ? $status : StatusCode::OK;
212
213
        $response = [
214
            $this->config->field['message'] ?? 'message' => $message,
215
        ];
216
        if (! empty($this->config->field['status'])) {
217
            $response[$this->config->field['status']] = true;
218
        }
219
        if (is_array($result)) {
220
            $result = array_map(fn ($element) => $this->formatEntity($element), $result);
221
        }
222
223
        $response[$this->config->field['result'] ?? 'result'] = $this->formatEntity($result);
224
225
        return $this->respond($response, $status);
226
    }
227
228
    /**
229
     * Formatte les données à renvoyer lorsqu'il s'agit des objets de la classe Entity
230
     *
231
     * @param mixed $element
232
     *
233
     * @return mixed
234
     */
235
    protected function formatEntity($element)
236
    {
237
        /*
238
        if ($element instanceof Entity) {
239
            if (method_exists($element, 'format')) {
240
                return service('container')->call([$element, 'format']);
241
            }
242
243
            return call_user_func([$element, 'toArray']);
244
        }
245
        */
246
        return $element;
247
    }
248
249
    /**
250
     * Genere un token d'authentification
251
     */
252
    protected function generateToken(array $data = [], array $config = []): string
253
    {
254
        $config = array_merge(['base_url' => base_url()], $this->config->jwt ?? [], $config);
255
256
        return Jwt::encode($data, $config);
257
    }
258
259
    /**
260
     * Decode un token d'autorisation
261
     *
262
     * @return mixed
263
     */
264
    protected function decodeToken(string $token, string $authType = 'bearer', array $config = [])
265
    {
266
        $config = array_merge(['base_url' => base_url()], $this->config->jwt ?? [], $config);
267
268
        if ('bearer' === $authType) {
269
            return Jwt::decode($token, $config);
270
        }
271
272
        return null;
273
    }
274
275
    /**
276
     * Recupere le token d'acces a partier des headers
277
     */
278
    protected function getBearerToken(): ?string
279
    {
280
        return Jwt::getToken();
281
    }
282
283
    /**
284
     * Recupere le header "Authorization"
285
     */
286
    protected function getAuthorizationHeader(): ?string
287
    {
288
        return Jwt::getAuthorization();
289
    }
290
291
    /**
292
     * Une méthode pratique pour traduire une chaîne ou un tableau d'entrées et
293
     * formater le résultat avec le MessageFormatter de l'extension intl.
294
     */
295
    protected function lang(string $line, ?array $args = null): string
296
    {
297
        return lang($line, $args, $this->locale);
0 ignored issues
show
Bug introduced by
It seems like $args can also be of type null; however, parameter $args of lang() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

297
        return lang($line, /** @scrutinizer ignore-type */ $args, $this->locale);
Loading history...
298
    }
299
300
    /**
301
     * @internal Ne pas censé être utilisé par le developpeur
302
     */
303
    protected function _translate(string $line, ?array $args = null): string
304
    {
305
        return $this->lang('Rest.' . $line, $args);
306
    }
307
308
    /**
309
     * Specifie que seules les requetes ajax sont acceptees
310
     */
311
    final protected function ajaxOnly(): self
312
    {
313
        $this->config->ajax_only = true;
314
315
        return $this;
316
    }
317
318
    /**
319
     * Definit les methodes authorisees par le web service
320
     */
321
    final protected function allowedMethods(string ...$methods): self
322
    {
323
        if ($methods !== []) {
324
            $this->config->allowed_methods = array_map(static fn ($str) => strtoupper($str), $methods);
325
        }
326
327
        return $this;
328
    }
329
330
    /**
331
     * Definit le format de donnees a renvoyer au client
332
     */
333
    final protected function returnFormat(string $format): self
334
    {
335
        $this->config->format = $format;
336
337
        return $this;
338
    }
339
340
    /**
341
     * N'autorise que les acces pas https
342
     */
343
    final protected function requireHttps(): self
344
    {
345
        $this->config->force_https = true;
346
347
        return $this;
348
    }
349
350
    /**
351
     * auth
352
     *
353
     * @param false|string $type
354
     */
355
    final protected function auth($type): self
356
    {
357
        $this->config->auth = $type;
358
359
        return $this;
360
    }
361
362
    /**
363
     * Definit la liste des adresses IP a bannir
364
     * Si le premier argument vaut "false", la suite ne sert plus a rien
365
     */
366
    final protected function ipBlacklist(...$params): self
367
    {
368
        $params = func_get_args();
369
        $enable = array_shift($params);
370
371
        if (false === $enable) {
372
            $params = [];
373
        } else {
374
            array_unshift($params, $enable);
375
            $params = array_merge($this->config->ip_blacklist ?? [], $params);
376
        }
377
378
        $this->config->ip_blacklist = $params;
379
380
        return $this;
381
    }
382
383
    /**
384
     * Definit la liste des adresses IP qui sont autorisees a acceder a la ressources
385
     * Si le premier argument vaut "false", la suite ne sert plus a rien
386
     */
387
    final protected function ipWhitelist(...$params): self
388
    {
389
        $params = func_get_args();
390
        $enable = array_shift($params);
391
392
        if (false === $enable) {
393
            $params = [];
394
        } else {
395
            array_unshift($params, $enable);
396
            $params = array_merge($this->config->ip_whitelist ?? [], $params);
397
        }
398
399
        $this->config->ip_whitelist = $params;
400
401
        return $this;
402
    }
403
404
    /**
405
     * Formatte les donnees a envoyer au bon format
406
     *
407
     * @param mixed $data Les donnees a envoyer
408
     */
409
    private function _parseResponse($data)
410
    {
411
        $format = strtolower($this->config->format);
412
        $mime   = null;
413
414
        if (array_key_exists($format, $this->mimes)) {
415
            $mime = $this->mimes[$format];
416
        } elseif (in_array($format, $this->mimes, true)) {
417
            $mime = $format;
418
        }
419
420
        // Si la méthode de format existe, appelle et renvoie la sortie dans ce format
421
        if (! empty($mime)) {
422
            $output = Formatter::type($mime)->format($data);
423
424
            // Définit l'en-tête du format
425
            // Ensuite, vérifiez si le client a demandé un rappel, et si la sortie contient ce rappel :
426
            $callback = $this->request->getQuery('callback');
427
            if (! empty($callback) && $mime === $this->mimes['json'] && preg_match('/^' . $callback . '/', $output)) {
0 ignored issues
show
Bug introduced by
Are you sure $callback of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

427
            if (! empty($callback) && $mime === $this->mimes['json'] && preg_match('/^' . /** @scrutinizer ignore-type */ $callback . '/', $output)) {
Loading history...
428
                $this->response = $this->response->withType($this->mimes['jsonp']);
429
            } else {
430
                $this->response = $this->response->withType($mime === $this->mimes['array'] ? $this->mimes['json'] : $mime);
431
            }
432
433
            // Un tableau doit être analysé comme une chaîne, afin de ne pas provoquer d'erreur de tableau en chaîne
434
            // Json est la forme la plus appropriée pour un tel type de données
435
            if ($mime === $this->mimes['array']) {
436
                $output = Formatter::type($this->mimes['json'])->format($output);
437
            }
438
        } else {
439
            // S'il s'agit d'un tableau ou d'un objet, analysez-le comme un json, de manière à être une 'chaîne'
440
            if (is_array($data) || is_object($data)) {
441
                $data = Formatter::type($this->mimes['json'])->format($data);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type object; however, parameter $data of BlitzPHP\Formatter\FormatterInterface::format() does only seem to accept array|string, maybe add an additional type check? ( Ignorable by Annotation )

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

441
                $data = Formatter::type($this->mimes['json'])->format(/** @scrutinizer ignore-type */ $data);
Loading history...
442
            }
443
            // Le format n'est pas pris en charge, sortez les données brutes sous forme de chaîne
444
            $output = $data;
445
        }
446
447
        $this->response = $this->response->withStringBody($output);
448
    }
449
450
    /**
451
     * Execute les annotations definies dans le contrôleur
452
     *
453
     * @param list<IAnnotation> $annotations Liste des annotations d'un contrôleur/méthode
0 ignored issues
show
Bug introduced by
The type BlitzPHP\Controllers\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
454
     */
455
    protected function _execAnnotations(self $instance, array $annotations): self
456
    {
457
        foreach ($annotations as $annotation) {
458
            switch (get_type_name($annotation)) {
459
                case RequestMappingAnnotation::class:
460
                    $this->allowedMethods(...(array) $annotation->method);
461
                    break;
462
463
                case AjaxOnlyAnnotation::class:
464
                    $this->ajaxOnly();
465
                    break;
466
467
                default:
468
                    break;
469
            }
470
        }
471
472
        return $instance;
473
    }
474
475
    /**
476
     * Verifie si les informations fournis par le client du ws sont conforme aux attentes du developpeur
477
     *
478
     * @throws Exception
479
     */
480
    private function checkProcess(): bool|ResponseInterface
481
    {
482
        // Verifie si la requete est en ajax
483
        if (! $this->request->is('ajax') && $this->config->ajax_only) {
0 ignored issues
show
Bug introduced by
'ajax' of type string is incompatible with the type BlitzPHP\Http\list expected by parameter $type of BlitzPHP\Http\ServerRequest::is(). ( Ignorable by Annotation )

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

483
        if (! $this->request->is(/** @scrutinizer ignore-type */ 'ajax') && $this->config->ajax_only) {
Loading history...
484
            return $this->respondNotAcceptable($this->_translate('ajaxOnly'));
485
        }
486
487
        // Verifie si la requete est en https
488
        if (! $this->request->is('https') && $this->config->force_https) {
489
            return $this->respondForbidden($this->_translate('unsupported'));
490
        }
491
492
        // Verifie si la methode utilisee pour la requete est autorisee
493
        if (! in_array(strtoupper($this->request->getMethod()), $this->config->allowed_methods, true)) {
494
            return $this->respondNotAcceptable($this->_translate('unknownMethod'));
495
        }
496
497
        // Verifie que l'ip qui emet la requete n'est pas dans la blacklist
498
        if (! empty($this->config->ip_blacklis)) {
499
            $this->config->ip_blacklist = implode(',', $this->config->ip_blacklist);
500
501
            // Correspond à une adresse IP dans une liste noire, par ex. 127.0.0.0, 0.0.0.0
502
            $pattern = sprintf('/(?:,\s*|^)\Q%s\E(?=,\s*|$)/m', $this->request->clientIp());
503
504
            // Renvoie 1, 0 ou FALSE (en cas d'erreur uniquement). Donc convertir implicitement 1 en TRUE
505
            if (preg_match($pattern, $this->config->ip_blacklist)) {
506
                return $this->respondUnauthorized($this->_translate('ipDenied'));
507
            }
508
        }
509
510
        // Verifie que l'ip qui emet la requete est dans la whitelist
511
        if (! empty($this->config->ip_whitelist)) {
512
            $whitelist   = $this->config->ip_whitelist;
513
            $whitelist[] = '127.0.0.1';
514
            $whitelist[] = '0.0.0.0';
515
516
            // coupez les espaces de début et de fin des ip
517
            $whitelist = array_map('trim', $whitelist);
518
519
            if (! in_array($this->request->clientIp(), $whitelist, true)) {
520
                return $this->respondUnauthorized($this->_translate('ipUnauthorized'));
521
            }
522
        }
523
524
        // Verifie l'authentification du client
525
        if (false !== $this->config->auth && ! $this->request->is('options') && 'bearer' === strtolower($this->config->auth)) {
526
            $token = $this->getBearerToken();
527
            if ($token === null || $token === '' || $token === '0') {
528
                return $this->respondInvalidToken($this->_translate('tokenNotFound'));
529
            }
530
            $payload = $this->decodeToken($token, 'bearer');
531
            if ($payload instanceof Throwable) {
532
                return $this->respondInvalidToken($payload->getMessage());
533
            }
534
            $this->payload = $payload;
535
        }
536
537
        return true;
538
    }
539
}
540