Passed
Push — master ( 1995be...2b4444 )
by Dmitriy
07:56 queued 04:33
created

InspectController::getGroupOwner()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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

209
    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...
210
    {
211
        // TODO: how to get params for console or other param groups?
212
        $classes = [];
213
214
        $inspected = [...get_declared_classes(), ...get_declared_interfaces()];
215
        // TODO: think how to ignore heavy objects
216
        $patterns = [
217
            fn (string $class) => !str_starts_with($class, 'ComposerAutoloaderInit'),
218
            fn (string $class) => !str_starts_with($class, 'Composer\\'),
219
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\Yii\\Debug\\'),
220
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\ErrorHandler\\ErrorHandler'),
221
            fn (string $class) => !str_contains($class, '@anonymous'),
222
            fn (string $class) => !is_subclass_of($class, Throwable::class),
223
        ];
224
        foreach ($patterns as $patternFunction) {
225
            $inspected = array_filter($inspected, $patternFunction);
226
        }
227
228
        foreach ($inspected as $className) {
229
            $class = new ReflectionClass($className);
230
231
            if ($class->isInternal()) {
232
                continue;
233
            }
234
235
            $classes[] = $className;
236
        }
237
        sort($classes);
238
239
        return $this->responseFactory->createResponse($classes);
240
    }
241
242
    public function object(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
243
    {
244
        $queryParams = $request->getQueryParams();
245
        $className = $queryParams['classname'];
246
247
        $reflection = new ReflectionClass($className);
248
249
        if ($reflection->isInternal()) {
250
            throw new InvalidArgumentException('Inspector cannot initialize internal classes.');
251
        }
252
        if ($reflection->implementsInterface(Throwable::class)) {
253
            throw new InvalidArgumentException('Inspector cannot initialize exceptions.');
254
        }
255
256
        $variable = $container->get($className);
257
        $result = VarDumper::create($variable)->asJson(false, 3);
258
259
        return $this->responseFactory->createResponse([
260
            'object' => json_decode($result, null, 512, JSON_THROW_ON_ERROR),
261
            'path' => $reflection->getFileName(),
262
        ]);
263
    }
264
265
    public function phpinfo(): ResponseInterface
266
    {
267
        ob_start();
268
        phpinfo();
269
        $phpinfo = ob_get_contents();
270
        ob_get_clean();
271
272
        return $this->responseFactory->createResponse($phpinfo);
273
    }
274
275
    public function routes(RouteCollectionInterface $routeCollection): ResponseInterface
276
    {
277
        $routes = [];
278
        foreach ($routeCollection->getRoutes() as $route) {
279
            $data = $route->__debugInfo();
280
            $routes[] = [
281
                'name' => $data['name'],
282
                'hosts' => $data['hosts'],
283
                'pattern' => $data['pattern'],
284
                'methods' => $data['methods'],
285
                'defaults' => $data['defaults'],
286
                'override' => $data['override'],
287
                'middlewares' => $data['middlewareDefinitions'],
288
            ];
289
        }
290
        $response = VarDumper::create($routes)->asJson(false, 5);
291
        return $this->responseFactory->createResponse(json_decode($response, null, 512, JSON_THROW_ON_ERROR));
292
    }
293
294
    public function getTables(SchemaProviderInterface $schemaProvider): ResponseInterface
295
    {
296
        return $this->responseFactory->createResponse($schemaProvider->getTables());
297
    }
298
299
    public function getTable(SchemaProviderInterface $schemaProvider, CurrentRoute $currentRoute): ResponseInterface
300
    {
301
        $tableName = $currentRoute->getArgument('name');
302
303
        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

303
        return $this->responseFactory->createResponse($schemaProvider->getTable(/** @scrutinizer ignore-type */ $tableName));
Loading history...
304
    }
305
306
    public function request(
307
        ServerRequestInterface $request,
308
        CollectorRepositoryInterface $collectorRepository
309
    ): ResponseInterface {
310
        $request = $request->getQueryParams();
311
        $debugEntryId = $request['debugEntryId'] ?? null;
312
313
        $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

313
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
314
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
315
316
        $request = Message::parseRequest($rawRequest);
317
318
        $client = new Client();
319
        $response = $client->send($request);
320
321
        $result = VarDumper::create($response)->asPrimitives();
322
323
        return $this->responseFactory->createResponse($result);
324
    }
325
326
    private function removeBasePath(string $rootPath, string $path): string|array|null
327
    {
328
        return preg_replace(
329
            '/^' . preg_quote($rootPath, '/') . '/',
330
            '',
331
            $path,
332
            1
333
        );
334
    }
335
336
    private function getUserOwner(int $uid): array
337
    {
338
        if ($uid === 0 || !function_exists('posix_getpwuid') || false === ($info = posix_getpwuid($uid))) {
339
            return [
340
                'id' => $uid,
341
            ];
342
        }
343
        return [
344
            'uid' => $info['uid'],
345
            'gid' => $info['gid'],
346
            'name' => $info['name'],
347
        ];
348
    }
349
350
    private function getGroupOwner(int $gid): array
351
    {
352
        if ($gid === 0 || !function_exists('posix_getgrgid') || false === ($info = posix_getgrgid($gid))) {
353
            return [
354
                'id' => $gid,
355
            ];
356
        }
357
        return [
358
            'gid' => $info['gid'],
359
            'name' => $info['name'],
360
        ];
361
    }
362
363
    private function serializeFileInfo(SplFileInfo $file): array
364
    {
365
        return [
366
            'baseName' => $file->getBasename(),
367
            'extension' => $file->getExtension(),
368
            'user' => $this->getUserOwner((int) $file->getOwner()),
369
            'group' => $this->getGroupOwner((int) $file->getGroup()),
370
            'size' => $file->getSize(),
371
            'type' => $file->getType(),
372
            'permissions' => substr(sprintf('%o', $file->getPerms()), -4),
373
        ];
374
    }
375
}
376