Passed
Pull Request — master (#59)
by Dmitriy
29:51 queued 27:02
created

InspectController::getUserOwner()   A

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 2
Bugs 0 Features 0
Metric Value
eloc 7
c 2
b 0
f 0
dl 0
loc 11
ccs 0
cts 6
cp 0
rs 10
cc 4
nc 2
nop 1
crap 20
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 SplFileInfo;
15
use RuntimeException;
16
use Throwable;
17
use Yiisoft\Aliases\Aliases;
18
use Yiisoft\Config\ConfigInterface;
19
use Yiisoft\DataResponse\DataResponseFactoryInterface;
20
use Yiisoft\Translator\CategorySource;
21
use Yiisoft\VarDumper\VarDumper;
22
use Yiisoft\Yii\Debug\Api\Inspector\ApplicationState;
23
use Yiisoft\Yii\Debug\Api\Inspector\CommandInterface;
24
25
class InspectController
26
{
27
    public function __construct(
28
        private DataResponseFactoryInterface $responseFactory,
29
    ) {
30
    }
31
32
    public function config(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
33
    {
34
        $config = $container->get(ConfigInterface::class);
35
36
        $request = $request->getQueryParams();
37
        $group = $request['group'] ?? 'web';
38
39
        $data = $config->get($group);
40
        ksort($data);
41
42
        $response = VarDumper::create($data)->asJson(false, 255);
43
        return $this->responseFactory->createResponse(json_decode($response, null, 512, JSON_THROW_ON_ERROR));
44
    }
45
46
    public function translations(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request 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

46
    public function translations(ContainerInterface $container, /** @scrutinizer ignore-unused */ ServerRequestInterface $request): 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...
47
    {
48
        /**
49
         * @var $categorySources CategorySource[]
50
         */
51
        $categorySources = $container->get('[email protected]');
52
53
        $params = ApplicationState::$params;
54
55
        $locales = array_keys($params['locale']['locales']);
56
        if ($locales === []) {
57
            throw new RuntimeException(
58
                'Unable to determine list of available locales. ' .
59
                'Make sure that "$params[\'locale\'][\'locales\']" contains all available locales.'
60
            );
61
        }
62
        $messages = [];
63
        foreach ($categorySources as $categorySource) {
64
            $messages[$categorySource->getName()] = [
65
                'messages' => [],
66
            ];
67
68
            try {
69
                foreach ($locales as $locale) {
70
                    $messages[$categorySource->getName()]['messages'][$locale] = $categorySource->getMessages($locale);
0 ignored issues
show
Bug introduced by
The method getMessages() does not exist on Yiisoft\Translator\CategorySource. Did you maybe mean getMessage()? ( Ignorable by Annotation )

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

70
                    /** @scrutinizer ignore-call */ 
71
                    $messages[$categorySource->getName()]['messages'][$locale] = $categorySource->getMessages($locale);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
71
                }
72
            } catch (Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
73
            }
74
        }
75
76
        $response = VarDumper::create($messages)->asPrimitives(255);
77
        return $this->responseFactory->createResponse($response);
78
    }
79
80
    public function params(): ResponseInterface
81
    {
82
        $params = ApplicationState::$params;
83
        ksort($params);
84
85
        return $this->responseFactory->createResponse($params);
86
    }
87
88
    public function files(Aliases $aliases, ServerRequestInterface $request): ResponseInterface
89
    {
90
        $request = $request->getQueryParams();
91
        $path = $request['path'] ?? '';
92
93
        $rootPath = $aliases->get('@root');
94
95
        $destination = $this->removeBasePath($rootPath, $path);
96
97
        if (!str_starts_with('/', $destination)) {
98
            $destination = '/' . $destination;
99
        }
100
101
        $destination = realpath($rootPath . $destination);
102
103
        if (!file_exists($destination)) {
104
            throw new InvalidArgumentException(sprintf('Destination "%s" does not exist', $destination));
105
        }
106
107
        if (!is_dir($destination)) {
108
            $file = new SplFileInfo($destination);
109
            return $this->responseFactory->createResponse(
110
                array_merge(
111
                    [
112
                        'directory' => $this->removeBasePath($rootPath, dirname($destination)),
113
                        'content' => file_get_contents($destination),
114
                        'path' => $this->removeBasePath($rootPath, $destination),
115
                        'absolutePath' => $destination,
116
                    ],
117
                    $this->serializeFileInfo($file)
118
                )
119
            );
120
        }
121
122
        /**
123
         * @var $directoryIterator SplFileInfo[]
124
         */
125
        $directoryIterator = new RecursiveDirectoryIterator(
126
            $destination,
127
            FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO
128
        );
129
130
        $files = [];
131
        foreach ($directoryIterator as $file) {
132
            if ($file->getBasename() === '.') {
133
                continue;
134
            }
135
136
            $path = $file->getPathName();
137
            if ($file->isDir()) {
138
                if ($file->getBasename() === '..') {
139
                    $path = realpath($path);
140
                }
141
                $path .= '/';
142
            }
143
            /**
144
             * Check if path is inside the application directory
145
             */
146
            if (!str_starts_with($path, $rootPath)) {
147
                continue;
148
            }
149
            $path = $this->removeBasePath($rootPath, $path);
150
            $files[] = array_merge(
151
                [
152
                    'path' => $path,
153
                ],
154
                $this->serializeFileInfo($file)
155
            );
156
        }
157
158
        return $this->responseFactory->createResponse($files);
159
    }
160
161
    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

161
    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...
162
    {
163
        // TODO: how to get params for console or other param groups?
164
        $classes = [];
165
166
        $inspected = [...get_declared_classes(), ...get_declared_interfaces()];
167
        // TODO: think how to ignore heavy objects
168
        $patterns = [
169
            fn (string $class) => !str_starts_with($class, 'ComposerAutoloaderInit'),
170
            fn (string $class) => !str_starts_with($class, 'Composer\\'),
171
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\Yii\\Debug\\'),
172
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\ErrorHandler\\ErrorHandler'),
173
            fn (string $class) => !str_contains($class, '@anonymous'),
174
            fn (string $class) => !is_subclass_of($class, Throwable::class),
175
        ];
176
        foreach ($patterns as $patternFunction) {
177
            $inspected = array_filter($inspected, $patternFunction);
178
        }
179
180
        foreach ($inspected as $className) {
181
            $class = new ReflectionClass($className);
182
183
            if ($class->isInternal()) {
184
                continue;
185
            }
186
187
            $classes[] = $className;
188
        }
189
        sort($classes);
190
191
        return $this->responseFactory->createResponse($classes);
192
    }
193
194
    public function object(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
195
    {
196
        $queryParams = $request->getQueryParams();
197
        $className = $queryParams['classname'];
198
199
        $reflection = new ReflectionClass($className);
200
201
        if ($reflection->isInternal()) {
202
            throw new InvalidArgumentException('Inspector cannot initialize internal classes.');
203
        }
204
        if ($reflection->implementsInterface(Throwable::class)) {
205
            throw new InvalidArgumentException('Inspector cannot initialize exceptions.');
206
        }
207
208
        $variable = $container->get($className);
209
        $result = VarDumper::create($variable)->asJson(false, 3);
210
211
        return $this->responseFactory->createResponse([
212
            'object' => json_decode($result, null, 512, JSON_THROW_ON_ERROR),
213
            'path' => $reflection->getFileName(),
214
        ]);
215
    }
216
217
    public function getCommands(ConfigInterface $config): ResponseInterface
218
    {
219
        $params = $config->get('params');
220
        $commandMap = $params['yiisoft/yii-debug-api']['inspector']['commandMap'] ?? [];
221
222
        $result = [];
223
        foreach ($commandMap as $groupName => $commands) {
224
            foreach ($commands as $name => $command) {
225
                if (!is_subclass_of($command, CommandInterface::class)) {
226
                    continue;
227
                }
228
                $result[] = [
229
                    'name' => $name,
230
                    'title' => $command::getTitle(),
231
                    'group' => $groupName,
232
                    'description' => $command::getDescription(),
233
                ];
234
            }
235
        }
236
237
        return $this->responseFactory->createResponse($result);
238
    }
239
240
    public function runCommand(
241
        ServerRequestInterface $request,
242
        ContainerInterface $container,
243
        ConfigInterface $config
244
    ): ResponseInterface {
245
        $params = $config->get('params');
246
        $commandMap = $params['yiisoft/yii-debug-api']['inspector']['commandMap'] ?? [];
247
248
        /**
249
         * @var array<string, class-string<CommandInterface>> $commandList
250
         */
251
        $commandList = [];
252
        foreach ($commandMap as $commands) {
253
            foreach ($commands as $name => $command) {
254
                if (!is_subclass_of($command, CommandInterface::class)) {
255
                    continue;
256
                }
257
                $commandList[$name] = $command;
258
            }
259
        }
260
261
        $request = $request->getQueryParams();
262
        $commandName = $request['command'] ?? null;
263
264
        if ($commandName === null) {
265
            throw new InvalidArgumentException(
266
                sprintf(
267
                    'Command must not be null. Available commands: "%s".',
268
                    implode('", "', $commandList)
269
                )
270
            );
271
        }
272
273
        if (!array_key_exists($commandName, $commandList)) {
274
            throw new InvalidArgumentException(
275
                sprintf(
276
                    'Unknown command "%s". Available commands: "%s".',
277
                    $commandName,
278
                    implode('", "', $commandList)
279
                )
280
            );
281
        }
282
283
        $commandClass = $commandList[$commandName];
284
        /**
285
         * @var $command CommandInterface
286
         */
287
        $command = $container->get($commandClass);
288
289
        $result = $command->run();
290
291
        return $this->responseFactory->createResponse([
292
            'status' => $result->getStatus(),
293
            'result' => $result->getResult(),
294
            'error' => $result->getErrors(),
295
        ]);
296
    }
297
298
    private function removeBasePath(string $rootPath, string $path): string|array|null
299
    {
300
        return preg_replace(
301
            '/^' . preg_quote($rootPath, '/') . '/',
302
            '',
303
            $path,
304
            1
305
        );
306
    }
307
308
    private function getUserOwner(int $uid): array
309
    {
310
        if ($uid === 0 || !function_exists('posix_getpwuid') || false === ($info = posix_getpwuid($uid))) {
311
            return [
312
                'id' => $uid,
313
            ];
314
        }
315
        return [
316
            'uid' => $info['uid'],
317
            'gid' => $info['gid'],
318
            'name' => $info['name'],
319
        ];
320
    }
321
322
    private function getGroupOwner(int $gid): array
323
    {
324
        if ($gid === 0 || !function_exists('posix_getgrgid') || false === ($info = posix_getgrgid($gid))) {
325
            return [
326
                'id' => $gid,
327
            ];
328
        }
329
        return [
330
            'gid' => $info['gid'],
331
            'name' => $info['name'],
332
        ];
333
    }
334
335
    private function serializeFileInfo(SplFileInfo $file): array
336
    {
337
        return [
338
            'baseName' => $file->getBasename(),
339
            'extension' => $file->getExtension(),
340
            'user' => $this->getUserOwner((int) $file->getOwner()),
341
            'group' => $this->getGroupOwner((int) $file->getGroup()),
342
            'size' => $file->getSize(),
343
            'type' => $file->getType(),
344
            'permissions' => substr(sprintf('%o', $file->getPerms()), -4),
345
        ];
346
    }
347
}
348