InspectController::getUserOwner()   A
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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

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

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

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

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