Issues (536)

src/Debug/ExceptionManager.php (2 issues)

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\Debug;
13
14
use BlitzPHP\Exceptions\HttpException;
15
use BlitzPHP\Exceptions\TokenMismatchException;
16
use BlitzPHP\View\View;
17
use Symfony\Component\Finder\SplFileInfo;
18
use Throwable;
19
use Whoops\Handler\Handler;
20
use Whoops\Handler\HandlerInterface;
21
use Whoops\Handler\JsonResponseHandler;
22
use Whoops\Handler\PlainTextHandler;
23
use Whoops\Handler\PrettyPageHandler;
24
use Whoops\Inspector\InspectorInterface;
25
use Whoops\Run;
26
use Whoops\RunInterface;
27
use Whoops\Util\Misc;
28
29
/**
30
 * Capture et affiche les erreurs et exceptions via whoops
31
 *
32
 * Necessite l'instalation de `flip/whoops`
33
 */
34
class ExceptionManager
35
{
36
    /**
37
     * Gestionnaire d'exception (instance Whoops)
38
     */
39
    private ?Run $debugger = null;
40
41
    /**
42
     * Configuration du gestionnaire d'exception
43
     */
44
    private object $config;
45
46
    public function __construct()
47
    {
48
        if (class_exists(Run::class)) {
49
            $this->debugger = new Run();
50
            $this->config   = (object) config('exceptions');
51
        }
52
    }
53
54
    /**
55
     * Demarre le processus
56
     */
57
    public function register(): void
58
    {
59
        if (! $this->debugger) {
60
            return;
61
        }
62
63
        $this->registerWhoopsHandler()
64
            ->registerHttpErrorsHandler()
65
            ->registerAppHandlers();
66
67
        $this->debugger->register();
68
    }
69
70
    /**
71
     * Enregistre les gestionnaires d'exception spécifiques à l'application.
72
     *
73
     * Cette méthode parcourt les gestionnaires configurés et les ajoute au débogueur.
74
     * Elle prend en charge à la fois les gestionnaires callable et les noms de classe sous forme de chaîne qui peuvent être instanciés.
75
     */
76
    private function registerAppHandlers(): self
77
    {
78
        foreach ($this->config->handlers as $handler) {
79
            if (is_callable($handler)) {
80
                $this->debugger->pushHandler($handler);
0 ignored issues
show
The method pushHandler() does not exist on null. ( Ignorable by Annotation )

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

80
                $this->debugger->/** @scrutinizer ignore-call */ 
81
                                 pushHandler($handler);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
81
            } elseif (is_string($handler) && class_exists($handler)) {
82
                $class = service('container')->make($handler);
83
                if (is_callable($class) || $class instanceof HandlerInterface) {
84
                    $this->debugger->pushHandler($class);
85
                }
86
            }
87
        }
88
89
        return $this;
90
    }
91
92
    /**
93
     * Enregistre un gestionnaire pour les erreurs HTTP.
94
     *
95
     * Cette méthode met en place un gestionnaire d'erreurs personnalisé qui traite les exceptions,
96
     * les consigne si elle est configurée, et tente d'afficher les vues d'erreur appropriées.
97
     * Elle gère les codes d'état HTTP, la journalisation et les vues d'erreur personnalisées.
98
     */
99
    private function registerHttpErrorsHandler(): self
100
    {
101
        $this->debugger->pushHandler(function (Throwable $exception, InspectorInterface $inspector, RunInterface $run): int {
102
            $exception      = $this->prepareException($exception);
103
            $exception_code = $exception->getCode();
104
105
            if ($exception_code >= 400 && $exception_code < 600) {
106
                $run->sendHttpCode($exception_code);
107
            }
108
109
            if (true === $this->config->log && ! in_array($exception_code, $this->config->ignore_codes, true)) {
110
                service('logger')->error($exception);
111
            }
112
113
            if (is_dir($this->config->error_view_path)) {
114
                $files = array_map(static fn (SplFileInfo $file) => $file->getFilenameWithoutExtension(), service('fs')->files($this->config->error_view_path));
115
            } else {
116
                $files = [];
117
            }
118
119
            $files = collect($files)->flip()->only($exception_code, is_online() ? 'production' : '')->flip()->all();
120
121
            if ($files !== []) {
122
                $view = new View();
123
124
                $view->setAdapter(config('view.active_adapter', 'native'), ['view_path' => $this->config->error_view_path])
0 ignored issues
show
It seems like config('view.active_adapter', 'native') can also be of type null and object; however, parameter $adapter of BlitzPHP\View\View::setAdapter() 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

124
                $view->setAdapter(/** @scrutinizer ignore-type */ config('view.active_adapter', 'native'), ['view_path' => $this->config->error_view_path])
Loading history...
125
                    ->first($files, ['message' => $exception->getMessage()])
126
                    ->render();
127
128
                return Handler::QUIT;
129
            }
130
131
            return Handler::DONE;
132
        });
133
134
        return $this;
135
    }
136
137
    /**
138
     * Enregistre un gestionnaire de Whoops à des fins de débogage.
139
     *
140
     * Cette méthode met en place différents gestionnaires en fonction de l'environnement et des paramètres de configuration.
141
     * Elle vérifie la ligne de commande, l'état en ligne, les requêtes AJAX et les requêtes JSON.
142
     * En fonction des conditions, elle utilise PlainTextHandler, JsonResponseHandler ou PrettyPageHandler.
143
     *
144
     * Le PrettyPageHandler est configuré avec les paramètres de l'éditeur, le titre de la page, les chemins d'accès à l'application,
145
     * les données sur liste noire et les tables de données. Il gère également différents types de données pour les tables de données.
146
     */
147
    private function registerWhoopsHandler(): self
148
    {
149
        if (Misc::isCommandLine()) {
150
            $this->debugger->pushHandler(new PlainTextHandler(service('logger')));
151
152
            return $this;
153
        }
154
155
        if (is_online()) {
156
            return $this;
157
        }
158
159
        if (Misc::isAjaxRequest() || service('request')->isJson()) {
160
            $this->debugger->pushHandler(new JsonResponseHandler());
161
162
            return $this;
163
        }
164
165
        $handler = new PrettyPageHandler();
166
167
        $handler->handleUnconditionally(true);
168
        $handler->setEditor($this->config->editor ?: PrettyPageHandler::EDITOR_VSCODE);
169
        $handler->setPageTitle($this->config->title ?: $handler->getPageTitle());
170
        $handler->setApplicationPaths($this->getApplicationPaths());
171
172
        foreach ($this->config->blacklist as $blacklist) {
173
            [$name, $key] = explode('/', $blacklist) + [1 => '*'];
174
175
            if ($name[0] !== '_') {
176
                $name = '_' . $name;
177
            }
178
179
            $name = strtoupper($name);
180
181
            if ($key !== '*') {
182
                foreach (explode(',', $key) as $k) {
183
                    $handler->blacklist($name, $k);
184
                }
185
            } else {
186
                $values = match ($name) {
187
                    '_GET'     => $_GET,
188
                    '_POST'    => $_POST,
189
                    '_COOKIE'  => $_COOKIE,
190
                    '_SERVER'  => $_SERVER,
191
                    '_ENV'     => $_ENV,
192
                    '_FILES'   => $_FILES ?: [],
193
                    '_SESSION' => $_SESSION ?? [],
194
                    default    => [],
195
                };
196
197
                foreach (array_keys($values) as $key) {
198
                    $handler->blacklist($name, $key);
199
                }
200
            }
201
        }
202
203
        foreach ($this->config->data as $label => $data) {
204
            if (is_array($data)) {
205
                $handler->addDataTable($label, $data);
206
            } elseif (is_callable($data)) {
207
                $handler->addDataTableCallback($label, $data);
208
            }
209
        }
210
211
        $this->debugger->pushHandler($handler);
212
213
        return $this;
214
    }
215
216
    /**
217
     * Préparer l'exception pour le rendu.
218
     */
219
    private static function prepareException(Throwable $e): Throwable
220
    {
221
        if ($e instanceof TokenMismatchException) {
222
            return new HttpException($e->getMessage(), 419, $e);
223
        }
224
225
        return $e;
226
    }
227
228
    /**
229
     * Récupère les chemins d'accès à l'application.
230
     */
231
    private function getApplicationPaths(): array
232
    {
233
        return collect(service('fs')->directories(base_path()))
234
            ->flip()
235
            ->except(base_path('vendor'))
236
            ->flip()
237
            ->all();
238
    }
239
}
240