Passed
Pull Request — master (#72)
by Dmitriy
02:41
created

InspectController::config()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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