Passed
Pull Request — master (#69)
by Dmitriy
02:53
created

InspectController::removeBasePath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 7
ccs 0
cts 3
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\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 getTranslations(ContainerInterface $container): ResponseInterface
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
66
            try {
67
                foreach ($locales as $locale) {
68
                    $messages[$categorySource->getName()][$locale] = $categorySource->getMessages($locale);
69
                }
70
            } catch (Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
71
            }
72
        }
73
74
        $response = VarDumper::create($messages)->asPrimitives(255);
75
        return $this->responseFactory->createResponse($response);
76
    }
77
78
    public function putTranslation(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
79
    {
80
        /**
81
         * @var $categorySources CategorySource[]
82
         */
83
        $categorySources = $container->get('[email protected]');
84
85
        $body = $request->getParsedBody();
86
        $categoryName = $body['category'] ?? '';
87
        $locale = $body['locale'] ?? '';
88
        $translationId = $body['translation'] ?? '';
89
        $newMessage = $body['message'] ?? '';
90
91
        $categorySource = null;
92
        foreach ($categorySources as $possibleCategorySource) {
93
            if ($possibleCategorySource->getName() === $categoryName) {
94
                $categorySource = $possibleCategorySource;
95
            }
96
        }
97
        if ($categorySource === null) {
98
            throw new InvalidArgumentException(
99
                sprintf(
100
                    'Invalid category name "%s". Only the following categories are available: "%s"',
101
                    $categoryName,
102
                    implode(
103
                        '", "',
104
                        array_map(fn (CategorySource $categorySource) => $categorySource->getName(), $categorySources)
105
                    )
106
                )
107
            );
108
        }
109
        $messages = $categorySource->getMessages($locale);
110
        $messages = array_replace_recursive($messages, [
111
            $translationId => [
112
                'message' => $newMessage,
113
            ],
114
        ]);
115
        $categorySource->write($locale, $messages);
116
117
        $result = [$locale => $messages];
118
        $response = VarDumper::create($result)->asPrimitives(255);
119
        return $this->responseFactory->createResponse($response);
120
    }
121
122
    public function params(): ResponseInterface
123
    {
124
        $params = ApplicationState::$params;
125
        ksort($params);
126
127
        return $this->responseFactory->createResponse($params);
128
    }
129
130
    public function files(Aliases $aliases, ServerRequestInterface $request): ResponseInterface
131
    {
132
        $request = $request->getQueryParams();
133
        $path = $request['path'] ?? '';
134
135
        $rootPath = $aliases->get('@root');
136
137
        $destination = $this->removeBasePath($rootPath, $path);
138
139
        if (!str_starts_with('/', $destination)) {
140
            $destination = '/' . $destination;
141
        }
142
143
        $destination = realpath($rootPath . $destination);
144
145
        if (!file_exists($destination)) {
146
            throw new InvalidArgumentException(sprintf('Destination "%s" does not exist', $destination));
147
        }
148
149
        if (!is_dir($destination)) {
150
            $file = new SplFileInfo($destination);
151
            return $this->responseFactory->createResponse(
152
                array_merge(
153
                    [
154
                        'directory' => $this->removeBasePath($rootPath, dirname($destination)),
155
                        'content' => file_get_contents($destination),
156
                        'path' => $this->removeBasePath($rootPath, $destination),
157
                        'absolutePath' => $destination,
158
                    ],
159
                    $this->serializeFileInfo($file)
160
                )
161
            );
162
        }
163
164
        /**
165
         * @var $directoryIterator SplFileInfo[]
166
         */
167
        $directoryIterator = new RecursiveDirectoryIterator(
168
            $destination,
169
            FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO
170
        );
171
172
        $files = [];
173
        foreach ($directoryIterator as $file) {
174
            if ($file->getBasename() === '.') {
175
                continue;
176
            }
177
178
            $path = $file->getPathName();
179
            if ($file->isDir()) {
180
                if ($file->getBasename() === '..') {
181
                    $path = realpath($path);
182
                }
183
                $path .= '/';
184
            }
185
            /**
186
             * Check if path is inside the application directory
187
             */
188
            if (!str_starts_with($path, $rootPath)) {
189
                continue;
190
            }
191
            $path = $this->removeBasePath($rootPath, $path);
192
            $files[] = array_merge(
193
                [
194
                    'path' => $path,
195
                ],
196
                $this->serializeFileInfo($file)
197
            );
198
        }
199
200
        return $this->responseFactory->createResponse($files);
201
    }
202
203
    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

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