Passed
Pull Request — master (#87)
by Dmitriy
08:51 queued 06:00
created

InspectController::serializeFileInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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

210
    public function classes(/** @scrutinizer ignore-unused */ ContainerInterface $container): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
211
    {
212
        // TODO: how to get params for console or other param groups?
213
        $classes = [];
214
215
        $inspected = [...get_declared_classes(), ...get_declared_interfaces()];
216
        // TODO: think how to ignore heavy objects
217
        $patterns = [
218
            fn (string $class) => !str_starts_with($class, 'ComposerAutoloaderInit'),
219
            fn (string $class) => !str_starts_with($class, 'Composer\\'),
220
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\Yii\\Debug\\'),
221
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\ErrorHandler\\ErrorHandler'),
222
            fn (string $class) => !str_contains($class, '@anonymous'),
223
            fn (string $class) => !is_subclass_of($class, Throwable::class),
224
        ];
225
        foreach ($patterns as $patternFunction) {
226
            $inspected = array_filter($inspected, $patternFunction);
227
        }
228
229
        foreach ($inspected as $className) {
230
            $class = new ReflectionClass($className);
231
232
            if ($class->isInternal()) {
233
                continue;
234
            }
235
236
            $classes[] = $className;
237
        }
238
        sort($classes);
239
240
        return $this->responseFactory->createResponse($classes);
241
    }
242
243
    public function object(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
244
    {
245
        $queryParams = $request->getQueryParams();
246
        $className = $queryParams['classname'];
247
248
        $reflection = new ReflectionClass($className);
249
250
        if ($reflection->isInternal()) {
251
            throw new InvalidArgumentException('Inspector cannot initialize internal classes.');
252
        }
253
        if ($reflection->implementsInterface(Throwable::class)) {
254
            throw new InvalidArgumentException('Inspector cannot initialize exceptions.');
255
        }
256
257
        $variable = $container->get($className);
258
        $result = VarDumper::create($variable)->asJson(false, 3);
259
260
        return $this->responseFactory->createResponse([
261
            'object' => json_decode($result, null, 512, JSON_THROW_ON_ERROR),
262
            'path' => $reflection->getFileName(),
263
        ]);
264
    }
265
266
    public function phpinfo(): ResponseInterface
267
    {
268
        ob_start();
269
        phpinfo();
270
        $phpinfo = ob_get_contents();
271
        ob_get_clean();
272
273
        return $this->responseFactory->createResponse($phpinfo);
274
    }
275
276
    public function getCommands(ConfigInterface $config): ResponseInterface
277
    {
278
        $params = $config->get('params');
279
        $commandMap = $params['yiisoft/yii-debug-api']['inspector']['commandMap'] ?? [];
280
281
        $result = [];
282
        foreach ($commandMap as $groupName => $commands) {
283
            foreach ($commands as $name => $command) {
284
                if (!is_subclass_of($command, CommandInterface::class)) {
285
                    continue;
286
                }
287
                $result[] = [
288
                    'name' => $name,
289
                    'title' => $command::getTitle(),
290
                    'group' => $groupName,
291
                    'description' => $command::getDescription(),
292
                ];
293
            }
294
        }
295
296
        return $this->responseFactory->createResponse($result);
297
    }
298
299
    public function routes(RouteCollectionInterface $routeCollection): ResponseInterface
300
    {
301
        $routes = [];
302
        foreach ($routeCollection->getRoutes() as $route) {
303
            $data = $route->__debugInfo();
304
            $routes[] = [
305
                'name' => $data['name'],
306
                'hosts' => $data['hosts'],
307
                'pattern' => $data['pattern'],
308
                'methods' => $data['methods'],
309
                'defaults' => $data['defaults'],
310
                'override' => $data['override'],
311
                'middlewares' => $data['middlewareDefinitions'],
312
            ];
313
        }
314
        $response = VarDumper::create($routes)->asJson(false, 5);
315
        return $this->responseFactory->createResponse(json_decode($response, null, 512, JSON_THROW_ON_ERROR));
316
    }
317
318
    public function runCommand(
319
        ServerRequestInterface $request,
320
        ContainerInterface $container,
321
        ConfigInterface $config
322
    ): ResponseInterface {
323
        $params = $config->get('params');
324
        $commandMap = $params['yiisoft/yii-debug-api']['inspector']['commandMap'] ?? [];
325
326
        /**
327
         * @var array<string, class-string<CommandInterface>> $commandList
328
         */
329
        $commandList = [];
330
        foreach ($commandMap as $commands) {
331
            foreach ($commands as $name => $command) {
332
                if (!is_subclass_of($command, CommandInterface::class)) {
333
                    continue;
334
                }
335
                $commandList[$name] = $command;
336
            }
337
        }
338
339
        $request = $request->getQueryParams();
340
        $commandName = $request['command'] ?? null;
341
342
        if ($commandName === null) {
343
            throw new InvalidArgumentException(
344
                sprintf(
345
                    'Command must not be null. Available commands: "%s".',
346
                    implode('", "', $commandList)
347
                )
348
            );
349
        }
350
351
        if (!array_key_exists($commandName, $commandList)) {
352
            throw new InvalidArgumentException(
353
                sprintf(
354
                    'Unknown command "%s". Available commands: "%s".',
355
                    $commandName,
356
                    implode('", "', $commandList)
357
                )
358
            );
359
        }
360
361
        $commandClass = $commandList[$commandName];
362
        /**
363
         * @var $command CommandInterface
364
         */
365
        $command = $container->get($commandClass);
366
367
        $result = $command->run();
368
369
        return $this->responseFactory->createResponse([
370
            'status' => $result->getStatus(),
371
            'result' => $result->getResult(),
372
            'error' => $result->getErrors(),
373
        ]);
374
    }
375
376
    public function getTables(SchemaProviderInterface $schemaProvider): ResponseInterface
377
    {
378
        return $this->responseFactory->createResponse($schemaProvider->getTables());
379
    }
380
381
    public function getTable(SchemaProviderInterface $schemaProvider, CurrentRoute $currentRoute): ResponseInterface
382
    {
383
        $tableName = $currentRoute->getArgument('name');
384
385
        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

385
        return $this->responseFactory->createResponse($schemaProvider->getTable(/** @scrutinizer ignore-type */ $tableName));
Loading history...
386
    }
387
388
    public function request(
389
        ServerRequestInterface $request,
390
        CollectorRepositoryInterface $collectorRepository
391
    ): ResponseInterface {
392
        $request = $request->getQueryParams();
393
        $debugEntryId = $request['debugEntryId'] ?? null;
394
395
        $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

395
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
396
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
397
398
        $request = Message::parseRequest($rawRequest);
399
400
        $client = new Client();
401
        $response = $client->send($request);
402
403
        $result = VarDumper::create($response)->asPrimitives();
404
405
        return $this->responseFactory->createResponse($result);
406
    }
407
408
    private function removeBasePath(string $rootPath, string $path): string|array|null
409
    {
410
        return preg_replace(
411
            '/^' . preg_quote($rootPath, '/') . '/',
412
            '',
413
            $path,
414
            1
415
        );
416
    }
417
418
    private function getUserOwner(int $uid): array
419
    {
420
        if ($uid === 0 || !function_exists('posix_getpwuid') || false === ($info = posix_getpwuid($uid))) {
421
            return [
422
                'id' => $uid,
423
            ];
424
        }
425
        return [
426
            'uid' => $info['uid'],
427
            'gid' => $info['gid'],
428
            'name' => $info['name'],
429
        ];
430
    }
431
432
    private function getGroupOwner(int $gid): array
433
    {
434
        if ($gid === 0 || !function_exists('posix_getgrgid') || false === ($info = posix_getgrgid($gid))) {
435
            return [
436
                'id' => $gid,
437
            ];
438
        }
439
        return [
440
            'gid' => $info['gid'],
441
            'name' => $info['name'],
442
        ];
443
    }
444
445
    private function serializeFileInfo(SplFileInfo $file): array
446
    {
447
        return [
448
            'baseName' => $file->getBasename(),
449
            'extension' => $file->getExtension(),
450
            'user' => $this->getUserOwner((int) $file->getOwner()),
451
            'group' => $this->getGroupOwner((int) $file->getGroup()),
452
            'size' => $file->getSize(),
453
            'type' => $file->getType(),
454
            'permissions' => substr(sprintf('%o', $file->getPerms()), -4),
455
        ];
456
    }
457
}
458