Passed
Pull Request — master (#116)
by
unknown
02:55
created

InspectController::getTranslations()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 22
c 3
b 0
f 0
dl 0
loc 39
ccs 0
cts 24
cp 0
rs 8.6346
cc 7
nc 16
nop 1
crap 56
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
            $categoryName = $categorySource->getName();
77
78
            if (!isset($messages[$categoryName])) {
79
                $messages[$categoryName] = [];
80
            }
81
82
            try {
83
                foreach ($locales as $locale) {
84
                    if (!isset($messages[$categoryName][$locale])) {
85
                        $messages[$categoryName][$locale] = [];
86
                    }
87
                    $messages[$categoryName][$locale] = array_merge(
88
                        $messages[$categoryName][$locale],
89
                        $categorySource->getMessages($locale)
90
                    );
91
                }
92
            } catch (Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
93
            }
94
        }
95
96
        $response = VarDumper::create($messages)->asPrimitives(255);
97
        return $this->responseFactory->createResponse($response);
98
    }
99
100
    public function putTranslation(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
101
    {
102
        /**
103
         * @var CategorySource[] $categorySources
104
         */
105
        $categorySources = $container->get('[email protected]');
106
107
        $body = $request->getParsedBody();
108
        $categoryName = $body['category'] ?? '';
109
        $locale = $body['locale'] ?? '';
110
        $translationId = $body['translation'] ?? '';
111
        $newMessage = $body['message'] ?? '';
112
113
        $categorySource = null;
114
        foreach ($categorySources as $possibleCategorySource) {
115
            if ($possibleCategorySource->getName() === $categoryName) {
116
                $categorySource = $possibleCategorySource;
117
            }
118
        }
119
        if ($categorySource === null) {
120
            throw new InvalidArgumentException(
121
                sprintf(
122
                    'Invalid category name "%s". Only the following categories are available: "%s"',
123
                    $categoryName,
124
                    implode(
125
                        '", "',
126
                        array_map(fn (CategorySource $categorySource) => $categorySource->getName(), $categorySources)
127
                    )
128
                )
129
            );
130
        }
131
        $messages = $categorySource->getMessages($locale);
132
        $messages = array_replace_recursive($messages, [
133
            $translationId => [
134
                'message' => $newMessage,
135
            ],
136
        ]);
137
        $categorySource->write($locale, $messages);
138
139
        $result = [$locale => $messages];
140
        $response = VarDumper::create($result)->asPrimitives(255);
141
        return $this->responseFactory->createResponse($response);
142
    }
143
144
    public function params(): ResponseInterface
145
    {
146
        $params = ApplicationState::$params;
147
        ksort($params);
148
149
        return $this->responseFactory->createResponse($params);
150
    }
151
152
    public function files(ServerRequestInterface $request): ResponseInterface
153
    {
154
        $request = $request->getQueryParams();
155
        $class = $request['class'] ?? '';
156
        $method = $request['method'] ?? '';
157
158
        if (!empty($class) && class_exists($class)) {
159
            $reflection = new ReflectionClass($class);
160
            $destination = $reflection->getFileName();
161
            if ($method !== '' && $reflection->hasMethod($method)) {
162
                $reflectionMethod = $reflection->getMethod($method);
163
                $startLine = $reflectionMethod->getStartLine();
164
                $endLine = $reflectionMethod->getEndLine();
165
            }
166
            if ($destination === false) {
167
                return $this->responseFactory->createResponse([
168
                    'message' => sprintf('Cannot find source of class "%s".', $class),
169
                ], 404);
170
            }
171
            return $this->readFile($destination, [
172
                'startLine' => $startLine ?? null,
173
                'endLine' => $endLine ?? null,
174
            ]);
175
        }
176
177
        $path = $request['path'] ?? '';
178
179
        $rootPath = $this->aliases->get('@root');
180
181
        $destination = $this->removeBasePath($rootPath, $path);
182
183
        if (!str_starts_with($destination, '/')) {
184
            $destination = '/' . $destination;
185
        }
186
187
        $destination = realpath($rootPath . $destination);
188
189
        if ($destination === false) {
190
            return $this->responseFactory->createResponse([
191
                'message' => sprintf('Destination "%s" does not exist', $path),
192
            ], 404);
193
        }
194
195
        if (!is_dir($destination)) {
196
            return $this->readFile($destination);
197
        }
198
199
        $directoryIterator = new RecursiveDirectoryIterator(
200
            $destination,
201
            FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO
202
        );
203
204
        $files = [];
205
        foreach ($directoryIterator as $file) {
206
            if ($file->getBasename() === '.') {
207
                continue;
208
            }
209
210
            $path = $file->getPathName();
211
            if ($file->isDir()) {
212
                if ($file->getBasename() === '..') {
213
                    $path = realpath($path);
214
                }
215
                $path .= '/';
216
            }
217
            /**
218
             * Check if path is inside the application directory
219
             */
220
            if (!str_starts_with($path, $rootPath)) {
221
                continue;
222
            }
223
            $path = $this->removeBasePath($rootPath, $path);
224
            $files[] = array_merge(
225
                [
226
                    'path' => $path,
227
                ],
228
                $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

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

373
        return $this->responseFactory->createResponse($schemaProvider->getTable(/** @scrutinizer ignore-type */ $tableName));
Loading history...
374
    }
375
376
    public function request(
377
        ServerRequestInterface $request,
378
        CollectorRepositoryInterface $collectorRepository
379
    ): ResponseInterface {
380
        $request = $request->getQueryParams();
381
        $debugEntryId = $request['debugEntryId'] ?? null;
382
383
        $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

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

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