Passed
Pull Request — master (#59)
by Dmitriy
04:12 queued 01:18
created

InspectController::translations()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 17
c 2
b 0
f 0
dl 0
loc 32
ccs 0
cts 14
cp 0
rs 9.3888
cc 5
nc 5
nop 2
crap 30
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\Command\CodeceptionCommand;
24
use Yiisoft\Yii\Debug\Api\Inspector\Command\PHPUnitCommand;
25
use Yiisoft\Yii\Debug\Api\Inspector\Command\PsalmCommand;
26
27
class InspectController
28
{
29
    public function __construct(
30
        private DataResponseFactoryInterface $responseFactory,
31
    ) {
32
    }
33
34
    public function config(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
35
    {
36
        $config = $container->get(ConfigInterface::class);
37
38
        $request = $request->getQueryParams();
39
        $group = $request['group'] ?? 'web';
40
41
        $data = $config->get($group);
42
        ksort($data);
43
44
        $response = VarDumper::create($data)->asJson(false, 255);
45
        return $this->responseFactory->createResponse(json_decode($response, null, 512, JSON_THROW_ON_ERROR));
46
    }
47
48
    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

48
    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...
49
    {
50
        /**
51
         * @var $categorySources CategorySource[]
52
         */
53
        $categorySources = $container->get('[email protected]');
54
55
        $params = ApplicationState::$params;
56
57
        $locales = array_keys($params['locale']['locales']);
58
        if ($locales === []) {
59
            throw new RuntimeException(
60
                'Unable to determine list of available locales. ' .
61
                'Make sure that "$params[\'locale\'][\'locales\']" contains all available locales.'
62
            );
63
        }
64
        $messages = [];
65
        foreach ($categorySources as $categorySource) {
66
            $messages[$categorySource->getName()] = [
67
                'messages' => [],
68
            ];
69
70
            try {
71
                foreach ($locales as $locale) {
72
                    $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

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

163
    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...
164
    {
165
        // TODO: how to get params for console or other param groups?
166
        $classes = [];
167
168
        $inspected = [...get_declared_classes(), ...get_declared_interfaces()];
169
        // TODO: think how to ignore heavy objects
170
        $patterns = [
171
            fn (string $class) => !str_starts_with($class, 'ComposerAutoloaderInit'),
172
            fn (string $class) => !str_starts_with($class, 'Composer\\'),
173
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\Yii\\Debug\\'),
174
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\ErrorHandler\\ErrorHandler'),
175
            fn (string $class) => !str_contains($class, '@anonymous'),
176
            fn (string $class) => !is_subclass_of($class, Throwable::class),
177
        ];
178
        foreach ($patterns as $patternFunction) {
179
            $inspected = array_filter($inspected, $patternFunction);
180
        }
181
182
        foreach ($inspected as $className) {
183
            $class = new ReflectionClass($className);
184
185
            if ($class->isInternal()) {
186
                continue;
187
            }
188
189
            $classes[] = $className;
190
        }
191
        sort($classes);
192
193
        return $this->responseFactory->createResponse($classes);
194
    }
195
196
    public function object(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
197
    {
198
        $queryParams = $request->getQueryParams();
199
        $className = $queryParams['classname'];
200
201
        $reflection = new ReflectionClass($className);
202
203
        if ($reflection->isInternal()) {
204
            throw new InvalidArgumentException('Inspector cannot initialize internal classes.');
205
        }
206
        if ($reflection->implementsInterface(Throwable::class)) {
207
            throw new InvalidArgumentException('Inspector cannot initialize exceptions.');
208
        }
209
210
        $variable = $container->get($className);
211
        $result = VarDumper::create($variable)->asJson(false, 3);
212
213
        return $this->responseFactory->createResponse([
214
            'object' => json_decode($result, null, 512, JSON_THROW_ON_ERROR),
215
            'path' => $reflection->getFileName(),
216
        ]);
217
    }
218
219
    public function command(ServerRequestInterface $request, ContainerInterface $container): ResponseInterface
220
    {
221
        // TODO: would be great to recognise test engine automatically
222
        $map = [
223
            'test/phpunit' => PHPUnitCommand::class,
224
            'test/codeception' => CodeceptionCommand::class,
225
            'analyse/psalm' => PsalmCommand::class,
226
        ];
227
228
        $request = $request->getQueryParams();
229
        $commandName = $request['command'] ?? 'test/codeception';
230
231
        if (!array_key_exists($commandName, $map)) {
232
            throw new InvalidArgumentException('Unknown command');
233
        }
234
235
        $result = $container->get($map[$commandName])->run();
236
237
        return $this->responseFactory->createResponse($result);
238
    }
239
240
    private function removeBasePath(string $rootPath, string $path): string|array|null
241
    {
242
        return preg_replace(
243
            '/^' . preg_quote($rootPath, '/') . '/',
244
            '',
245
            $path,
246
            1
247
        );
248
    }
249
250
    private function getUserOwner(int $uid): array
251
    {
252
        if ($uid === 0 || !function_exists('posix_getpwuid') || false === ($info = posix_getpwuid($uid))) {
253
            return [
254
                'id' => $uid,
255
            ];
256
        }
257
        return [
258
            'uid' => $info['uid'],
259
            'gid' => $info['gid'],
260
            'name' => $info['name'],
261
        ];
262
    }
263
264
    private function getGroupOwner(int $gid): array
265
    {
266
        if ($gid === 0 || !function_exists('posix_getgrgid') || false === ($info = posix_getgrgid($gid))) {
267
            return [
268
                'id' => $gid,
269
            ];
270
        }
271
        return [
272
            'gid' => $info['gid'],
273
            'name' => $info['name'],
274
        ];
275
    }
276
277
    private function serializeFileInfo(SplFileInfo $file): array
278
    {
279
        return [
280
            'baseName' => $file->getBasename(),
281
            'extension' => $file->getExtension(),
282
            'user' => $this->getUserOwner((int) $file->getOwner()),
283
            'group' => $this->getGroupOwner((int) $file->getGroup()),
284
            'size' => $file->getSize(),
285
            'type' => $file->getType(),
286
            'permissions' => substr(sprintf('%o', $file->getPerms()), -4),
287
        ];
288
    }
289
}
290