Passed
Pull Request — master (#127)
by Dmitriy
03:02
created

InspectController::checkRoute()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 40
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 26
nc 7
nop 3
dl 0
loc 40
ccs 0
cts 28
cp 0
crap 30
rs 9.1928
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Debug\Api\Inspector\Controller;
6
7
use Alexkart\CurlBuilder\Command;
8
use FilesystemIterator;
9
use GuzzleHttp\Client;
10
use GuzzleHttp\Psr7\Message;
11
use InvalidArgumentException;
12
use Psr\Container\ContainerInterface;
13
use Psr\Http\Message\ResponseInterface;
14
use Psr\Http\Message\ServerRequestFactoryInterface;
15
use Psr\Http\Message\ServerRequestInterface;
16
use RecursiveDirectoryIterator;
17
use ReflectionClass;
18
use RuntimeException;
19
use SplFileInfo;
20
use Throwable;
21
use Yiisoft\Aliases\Aliases;
22
use Yiisoft\Config\ConfigInterface;
23
use Yiisoft\DataResponse\DataResponse;
24
use Yiisoft\DataResponse\DataResponseFactoryInterface;
25
use Yiisoft\Http\Method;
26
use Yiisoft\Router\CurrentRoute;
27
use Yiisoft\Router\RouteCollectionInterface;
28
use Yiisoft\Router\UrlMatcherInterface;
29
use Yiisoft\Translator\CategorySource;
30
use Yiisoft\VarDumper\VarDumper;
31
use Yiisoft\Yii\Debug\Api\Debug\Repository\CollectorRepositoryInterface;
32
use Yiisoft\Yii\Debug\Api\Inspector\ApplicationState;
33
use Yiisoft\Yii\Debug\Api\Inspector\Database\SchemaProviderInterface;
34
use Yiisoft\Yii\Debug\Collector\Web\RequestCollector;
35
36
use function Yiisoft\Yii\Debug\Api\Controller\sort;
0 ignored issues
show
introduced by
The function Yiisoft\Yii\Debug\Api\Controller\sort was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
37
38
class InspectController
39
{
40
    public function __construct(
41
        private DataResponseFactoryInterface $responseFactory,
42
        private Aliases $aliases,
43
    ) {
44
    }
45
46
    public function config(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
47
    {
48
        $config = $container->get(ConfigInterface::class);
49
50
        $request = $request->getQueryParams();
51
        $group = $request['group'] ?? 'di';
52
53
        $data = $config->get($group);
54
        ksort($data);
55
56
        $response = VarDumper::create($data)->asPrimitives(255);
57
58
        return $this->responseFactory->createResponse($response);
59
    }
60
61
    public function getTranslations(ContainerInterface $container): ResponseInterface
62
    {
63
        /** @var CategorySource[] $categorySources */
64
        $categorySources = $container->get('[email protected]');
65
66
        $params = ApplicationState::$params;
67
68
        /** @var string[] $locales */
69
        $locales = array_keys($params['locale']['locales']);
70
        if ($locales === []) {
71
            throw new RuntimeException(
72
                'Unable to determine list of available locales. ' .
73
                'Make sure that "$params[\'locale\'][\'locales\']" contains all available locales.'
74
            );
75
        }
76
        $messages = [];
77
        foreach ($categorySources as $categorySource) {
78
            $categoryName = $categorySource->getName();
79
80
            if (!isset($messages[$categoryName])) {
81
                $messages[$categoryName] = [];
82
            }
83
84
            try {
85
                foreach ($locales as $locale) {
86
                    $messages[$categoryName][$locale] = array_merge(
87
                        $messages[$categoryName][$locale] ?? [],
88
                        $categorySource->getMessages($locale)
89
                    );
90
                }
91
            } catch (Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
92
            }
93
        }
94
95
        $response = VarDumper::create($messages)->asPrimitives(255);
96
        return $this->responseFactory->createResponse($response);
97
    }
98
99
    public function putTranslation(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
100
    {
101
        /**
102
         * @var CategorySource[] $categorySources
103
         */
104
        $categorySources = $container->get('[email protected]');
105
106
        $body = \json_decode($request->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
107
        $categoryName = $body['category'] ?? '';
108
        $locale = $body['locale'] ?? '';
109
        $translationId = $body['translation'] ?? '';
110
        $newMessage = $body['message'] ?? '';
111
112
        $categorySource = null;
113
        foreach ($categorySources as $possibleCategorySource) {
114
            if ($possibleCategorySource->getName() === $categoryName) {
115
                $categorySource = $possibleCategorySource;
116
            }
117
        }
118
        if ($categorySource === null) {
119
            throw new InvalidArgumentException(
120
                sprintf(
121
                    'Invalid category name "%s". Only the following categories are available: "%s"',
122
                    $categoryName,
123
                    implode(
124
                        '", "',
125
                        array_map(fn (CategorySource $categorySource) => $categorySource->getName(), $categorySources)
126
                    )
127
                )
128
            );
129
        }
130
        $messages = $categorySource->getMessages($locale);
131
        $messages = array_replace_recursive($messages, [
132
            $translationId => [
133
                'message' => $newMessage,
134
            ],
135
        ]);
136
        $categorySource->write($locale, $messages);
137
138
        $result = [$locale => $messages];
139
        $response = VarDumper::create($result)->asPrimitives(255);
140
        return $this->responseFactory->createResponse($response);
141
    }
142
143
    public function params(): ResponseInterface
144
    {
145
        $params = ApplicationState::$params;
146
        ksort($params);
147
148
        return $this->responseFactory->createResponse($params);
149
    }
150
151
    public function files(ServerRequestInterface $request): ResponseInterface
152
    {
153
        $request = $request->getQueryParams();
154
        $class = $request['class'] ?? '';
155
        $method = $request['method'] ?? '';
156
157
        if (!empty($class) && class_exists($class)) {
158
            $reflection = new ReflectionClass($class);
159
            $destination = $reflection->getFileName();
160
            if ($method !== '' && $reflection->hasMethod($method)) {
161
                $reflectionMethod = $reflection->getMethod($method);
162
                $startLine = $reflectionMethod->getStartLine();
163
                $endLine = $reflectionMethod->getEndLine();
164
            }
165
            if ($destination === false) {
166
                return $this->responseFactory->createResponse([
167
                    'message' => sprintf('Cannot find source of class "%s".', $class),
168
                ], 404);
169
            }
170
            return $this->readFile($destination, [
171
                'startLine' => $startLine ?? null,
172
                'endLine' => $endLine ?? null,
173
            ]);
174
        }
175
176
        $path = $request['path'] ?? '';
177
178
        $rootPath = $this->aliases->get('@root');
179
180
        $destination = $this->removeBasePath($rootPath, $path);
181
182
        if (!str_starts_with($destination, '/')) {
183
            $destination = '/' . $destination;
184
        }
185
186
        $destination = realpath($rootPath . $destination);
187
188
        if ($destination === false) {
189
            return $this->responseFactory->createResponse([
190
                'message' => sprintf('Destination "%s" does not exist', $path),
191
            ], 404);
192
        }
193
194
        if (!is_dir($destination)) {
195
            return $this->readFile($destination);
196
        }
197
198
        $directoryIterator = new RecursiveDirectoryIterator(
199
            $destination,
200
            FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO
201
        );
202
203
        $files = [];
204
        foreach ($directoryIterator as $file) {
205
            if ($file->getBasename() === '.') {
206
                continue;
207
            }
208
209
            $path = $file->getPathName();
210
            if ($file->isDir()) {
211
                if ($file->getBasename() === '..') {
212
                    $path = realpath($path);
213
                }
214
                $path .= '/';
215
            }
216
            /**
217
             * Check if path is inside the application directory
218
             */
219
            if (!str_starts_with($path, $rootPath)) {
220
                continue;
221
            }
222
            $path = $this->removeBasePath($rootPath, $path);
223
            $files[] = array_merge(
224
                [
225
                    'path' => $path,
226
                ],
227
                $this->serializeFileInfo($file)
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type string; however, parameter $file of Yiisoft\Yii\Debug\Api\In...er::serializeFileInfo() does only seem to accept SplFileInfo, 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

227
                $this->serializeFileInfo(/** @scrutinizer ignore-type */ $file)
Loading history...
228
            );
229
        }
230
231
        return $this->responseFactory->createResponse($files);
232
    }
233
234
    public function classes(): ResponseInterface
235
    {
236
        // TODO: how to get params for console or other param groups?
237
        $classes = [];
238
239
        $inspected = [...get_declared_classes(), ...get_declared_interfaces()];
240
        // TODO: think how to ignore heavy objects
241
        $patterns = [
242
            fn (string $class) => !str_starts_with($class, 'ComposerAutoloaderInit'),
243
            fn (string $class) => !str_starts_with($class, 'Composer\\'),
244
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\Yii\\Debug\\'),
245
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\ErrorHandler\\ErrorHandler'),
246
            fn (string $class) => !str_contains($class, '@anonymous'),
247
            fn (string $class) => !is_subclass_of($class, Throwable::class),
248
        ];
249
        foreach ($patterns as $patternFunction) {
250
            $inspected = array_filter($inspected, $patternFunction);
251
        }
252
253
        foreach ($inspected as $className) {
254
            $class = new ReflectionClass($className);
255
256
            if ($class->isInternal() || $class->isAbstract() || $class->isAnonymous()) {
257
                continue;
258
            }
259
260
            $classes[] = $className;
261
        }
262
        sort($classes);
263
264
        return $this->responseFactory->createResponse($classes);
265
    }
266
267
    public function object(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
268
    {
269
        $queryParams = $request->getQueryParams();
270
        $className = $queryParams['classname'];
271
272
        $reflection = new ReflectionClass($className);
273
274
        if ($reflection->isInternal()) {
275
            throw new InvalidArgumentException('Inspector cannot initialize internal classes.');
276
        }
277
        if ($reflection->implementsInterface(Throwable::class)) {
278
            throw new InvalidArgumentException('Inspector cannot initialize exceptions.');
279
        }
280
281
        $variable = $container->get($className);
282
        $result = VarDumper::create($variable)->asPrimitives(3);
283
284
        return $this->responseFactory->createResponse([
285
            'object' => $result,
286
            'path' => $reflection->getFileName(),
287
        ]);
288
    }
289
290
    public function phpinfo(): ResponseInterface
291
    {
292
        ob_start();
293
        phpinfo();
294
        $phpinfo = ob_get_contents();
295
        ob_get_clean();
296
297
        return $this->responseFactory->createResponse($phpinfo);
298
    }
299
300
    public function routes(RouteCollectionInterface $routeCollection): ResponseInterface
301
    {
302
        $routes = [];
303
        foreach ($routeCollection->getRoutes() as $route) {
304
            $data = $route->__debugInfo();
305
            $routes[] = [
306
                'name' => $data['name'],
307
                'hosts' => $data['hosts'],
308
                'pattern' => $data['pattern'],
309
                'methods' => $data['methods'],
310
                'defaults' => $data['defaults'],
311
                'override' => $data['override'],
312
                'middlewares' => $data['middlewareDefinitions'],
313
            ];
314
        }
315
        $response = VarDumper::create($routes)->asPrimitives(5);
316
317
        return $this->responseFactory->createResponse($response);
318
    }
319
320
    public function checkRoute(
321
        ServerRequestInterface $request,
322
        UrlMatcherInterface $matcher,
323
        ServerRequestFactoryInterface $serverRequestFactory
324
    ): ResponseInterface {
325
        $queryParams = $request->getQueryParams();
326
        $path = $queryParams['route'] ?? null;
327
        if ($path === null) {
328
            return $this->responseFactory->createResponse([
329
                'message' => 'Path is not specified.',
330
            ], 422);
331
        }
332
        $path = trim($path);
333
334
        $method = 'GET';
335
        if (str_contains($path, ' ')) {
336
            [$possibleMethod, $restPath] = explode(' ', $path, 2);
337
            if (in_array($possibleMethod, Method::ALL, true)) {
338
                $method = $possibleMethod;
339
                $path = $restPath;
340
            }
341
        }
342
        $request = $serverRequestFactory->createServerRequest($method, $path);
343
344
        $result = $matcher->match($request);
345
        if (!$result->isSuccess()) {
346
            return $this->responseFactory->createResponse([
347
                'result' => false,
348
            ]);
349
        }
350
351
        $route = $result->route();
352
        $reflection = new \ReflectionObject($route);
353
        $property = $reflection->getProperty('middlewareDefinitions');
354
        $middlewareDefinitions = $property->getValue($route);
355
        $action = end($middlewareDefinitions);
356
357
        return $this->responseFactory->createResponse([
358
            'result' => true,
359
            'action' => $action,
360
        ]);
361
    }
362
363
    public function getTables(SchemaProviderInterface $schemaProvider): ResponseInterface
364
    {
365
        return $this->responseFactory->createResponse($schemaProvider->getTables());
366
    }
367
368
    public function getTable(SchemaProviderInterface $schemaProvider, CurrentRoute $currentRoute): ResponseInterface
369
    {
370
        $tableName = $currentRoute->getArgument('name');
371
372
        return $this->responseFactory->createResponse($schemaProvider->getTable($tableName));
0 ignored issues
show
Bug introduced by
It seems like $tableName can also be of type null; however, parameter $tableName of Yiisoft\Yii\Debug\Api\In...erInterface::getTable() 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

372
        return $this->responseFactory->createResponse($schemaProvider->getTable(/** @scrutinizer ignore-type */ $tableName));
Loading history...
373
    }
374
375
    public function request(
376
        ServerRequestInterface $request,
377
        CollectorRepositoryInterface $collectorRepository
378
    ): ResponseInterface {
379
        $request = $request->getQueryParams();
380
        $debugEntryId = $request['debugEntryId'] ?? null;
381
382
        $data = $collectorRepository->getDetail($debugEntryId);
0 ignored issues
show
Bug introduced by
It seems like $debugEntryId can also be of type null; however, parameter $id of Yiisoft\Yii\Debug\Api\De...yInterface::getDetail() 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

382
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
383
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
384
385
        $request = Message::parseRequest($rawRequest);
386
387
        $client = new Client();
388
        $response = $client->send($request);
389
390
        $result = VarDumper::create($response)->asPrimitives();
391
392
        return $this->responseFactory->createResponse($result);
393
    }
394
395
    public function eventListeners(ContainerInterface $container)
396
    {
397
        $config = $container->get(ConfigInterface::class);
398
399
        return $this->responseFactory->createResponse([
400
            'common' => VarDumper::create($config->get('events'))->asPrimitives(),
401
            // TODO: change events-web to events-web when it will be possible
402
            'console' => [], //VarDumper::create($config->get('events-web'))->asPrimitives(),
403
            'web' => VarDumper::create($config->get('events-web'))->asPrimitives(),
404
        ]);
405
    }
406
407
    public function buildCurl(
408
        ServerRequestInterface $request,
409
        CollectorRepositoryInterface $collectorRepository
410
    ): ResponseInterface {
411
        $request = $request->getQueryParams();
412
        $debugEntryId = $request['debugEntryId'] ?? null;
413
414
415
        $data = $collectorRepository->getDetail($debugEntryId);
0 ignored issues
show
Bug introduced by
It seems like $debugEntryId can also be of type null; however, parameter $id of Yiisoft\Yii\Debug\Api\De...yInterface::getDetail() 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

415
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
416
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
417
418
        $request = Message::parseRequest($rawRequest);
419
420
        try {
421
            $output = (new Command())
422
                ->setRequest($request)
423
                ->build();
424
        } catch (Throwable $e) {
425
            return $this->responseFactory->createResponse([
426
                'command' => null,
427
                'exception' => (string)$e,
428
            ]);
429
        }
430
431
        return $this->responseFactory->createResponse([
432
            'command' => $output,
433
        ]);
434
    }
435
436
    private function removeBasePath(string $rootPath, string $path): string|array|null
437
    {
438
        return preg_replace(
439
            '/^' . preg_quote($rootPath, '/') . '/',
440
            '',
441
            $path,
442
            1
443
        );
444
    }
445
446
    private function getUserOwner(int $uid): array
447
    {
448
        if ($uid === 0 || !function_exists('posix_getpwuid') || false === ($info = posix_getpwuid($uid))) {
449
            return [
450
                'id' => $uid,
451
            ];
452
        }
453
        return [
454
            'uid' => $info['uid'],
455
            'gid' => $info['gid'],
456
            'name' => $info['name'],
457
        ];
458
    }
459
460
    private function getGroupOwner(int $gid): array
461
    {
462
        if ($gid === 0 || !function_exists('posix_getgrgid') || false === ($info = posix_getgrgid($gid))) {
463
            return [
464
                'id' => $gid,
465
            ];
466
        }
467
        return [
468
            'gid' => $info['gid'],
469
            'name' => $info['name'],
470
        ];
471
    }
472
473
    private function serializeFileInfo(SplFileInfo $file): array
474
    {
475
        return [
476
            'baseName' => $file->getBasename(),
477
            'extension' => $file->getExtension(),
478
            'user' => $this->getUserOwner((int) $file->getOwner()),
479
            'group' => $this->getGroupOwner((int) $file->getGroup()),
480
            'size' => $file->getSize(),
481
            'type' => $file->getType(),
482
            'permissions' => substr(sprintf('%o', $file->getPerms()), -4),
483
        ];
484
    }
485
486
    private function readFile(string $destination, array $extra = []): DataResponse
487
    {
488
        $rootPath = $this->aliases->get('@root');
489
        $file = new SplFileInfo($destination);
490
        return $this->responseFactory->createResponse(
491
            array_merge(
492
                $extra,
493
                [
494
                    'directory' => $this->removeBasePath($rootPath, dirname($destination)),
495
                    'content' => file_get_contents($destination),
496
                    'path' => $this->removeBasePath($rootPath, $destination),
497
                    'absolutePath' => $destination,
498
                ],
499
                $this->serializeFileInfo($file)
500
            )
501
        );
502
    }
503
}
504