Passed
Push — master ( 1c4206...0a9844 )
by Dmitriy
08:23 queued 05:28
created

InspectController::classes()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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

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

363
        return $this->responseFactory->createResponse($schemaProvider->getTable(/** @scrutinizer ignore-type */ $tableName));
Loading history...
364
    }
365
366
    public function request(
367
        ServerRequestInterface $request,
368
        CollectorRepositoryInterface $collectorRepository
369
    ): ResponseInterface {
370
        $request = $request->getQueryParams();
371
        $debugEntryId = $request['debugEntryId'] ?? null;
372
373
        $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\Re...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

373
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
374
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
375
376
        $request = Message::parseRequest($rawRequest);
377
378
        $client = new Client();
379
        $response = $client->send($request);
380
381
        $result = VarDumper::create($response)->asPrimitives();
382
383
        return $this->responseFactory->createResponse($result);
384
    }
385
386
    public function eventListeners(ContainerInterface $container)
387
    {
388
        $config = $container->get(ConfigInterface::class);
389
390
        return $this->responseFactory->createResponse([
391
            'common' => VarDumper::create($config->get('events'))->asPrimitives(),
392
            // TODO: change events-web to events-web when it will be possible
393
            'console' => [], //VarDumper::create($config->get('events-web'))->asPrimitives(),
394
            'web' => VarDumper::create($config->get('events-web'))->asPrimitives(),
395
        ]);
396
    }
397
398
    public function buildCurl(
399
        ServerRequestInterface $request,
400
        CollectorRepositoryInterface $collectorRepository
401
    ): ResponseInterface {
402
        $request = $request->getQueryParams();
403
        $debugEntryId = $request['debugEntryId'] ?? null;
404
405
406
        $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\Re...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

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