Passed
Pull Request — master (#111)
by Dmitriy
11:57 queued 08:57
created

InspectController::checkRoute()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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

207
                $this->serializeFileInfo(/** @scrutinizer ignore-type */ $file)
Loading history...
208
            );
209
        }
210
211
        return $this->responseFactory->createResponse($files);
212
    }
213
214
    public function classes(): ResponseInterface
215
    {
216
        // TODO: how to get params for console or other param groups?
217
        $classes = [];
218
219
        $inspected = [...get_declared_classes(), ...get_declared_interfaces()];
220
        // TODO: think how to ignore heavy objects
221
        $patterns = [
222
            fn (string $class) => !str_starts_with($class, 'ComposerAutoloaderInit'),
223
            fn (string $class) => !str_starts_with($class, 'Composer\\'),
224
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\Yii\\Debug\\'),
225
            fn (string $class) => !str_starts_with($class, 'Yiisoft\\ErrorHandler\\ErrorHandler'),
226
            fn (string $class) => !str_contains($class, '@anonymous'),
227
            fn (string $class) => !is_subclass_of($class, Throwable::class),
228
        ];
229
        foreach ($patterns as $patternFunction) {
230
            $inspected = array_filter($inspected, $patternFunction);
231
        }
232
233
        foreach ($inspected as $className) {
234
            $class = new ReflectionClass($className);
235
236
            if ($class->isInternal() || $class->isAbstract() || $class->isAnonymous()) {
237
                continue;
238
            }
239
240
            $classes[] = $className;
241
        }
242
        sort($classes);
243
244
        return $this->responseFactory->createResponse($classes);
245
    }
246
247
    public function object(ContainerInterface $container, ServerRequestInterface $request): ResponseInterface
248
    {
249
        $queryParams = $request->getQueryParams();
250
        $className = $queryParams['classname'];
251
252
        $reflection = new ReflectionClass($className);
253
254
        if ($reflection->isInternal()) {
255
            throw new InvalidArgumentException('Inspector cannot initialize internal classes.');
256
        }
257
        if ($reflection->implementsInterface(Throwable::class)) {
258
            throw new InvalidArgumentException('Inspector cannot initialize exceptions.');
259
        }
260
261
        $variable = $container->get($className);
262
        $result = VarDumper::create($variable)->asPrimitives(3);
263
264
        return $this->responseFactory->createResponse([
265
            'object' => $result,
266
            'path' => $reflection->getFileName(),
267
        ]);
268
    }
269
270
    public function phpinfo(): ResponseInterface
271
    {
272
        ob_start();
273
        phpinfo();
274
        $phpinfo = ob_get_contents();
275
        ob_get_clean();
276
277
        return $this->responseFactory->createResponse($phpinfo);
278
    }
279
280
    public function routes(RouteCollectionInterface $routeCollection): ResponseInterface
281
    {
282
        $routes = [];
283
        foreach ($routeCollection->getRoutes() as $route) {
284
            $data = $route->__debugInfo();
285
            $routes[] = [
286
                'name' => $data['name'],
287
                'hosts' => $data['hosts'],
288
                'pattern' => $data['pattern'],
289
                'methods' => $data['methods'],
290
                'defaults' => $data['defaults'],
291
                'override' => $data['override'],
292
                'middlewares' => $data['middlewareDefinitions'],
293
            ];
294
        }
295
        $response = VarDumper::create($routes)->asPrimitives(5);
296
297
        return $this->responseFactory->createResponse($response);
298
    }
299
300
    public function checkRoute(
301
        ServerRequestInterface $request,
302
        UrlMatcherInterface $matcher,
303
        ServerRequestFactoryInterface $serverRequestFactory
304
    ): ResponseInterface {
305
        $queryParams = $request->getQueryParams();
306
        $path = $queryParams['route'] ?? null;
307
        if ($path === null) {
308
            return $this->responseFactory->createResponse([
309
                'message' => 'Path is not specified.',
310
            ], 400);
311
        }
312
313
        // TODO: parse from path
314
        $method = 'GET';
315
        $request = $serverRequestFactory->createServerRequest($method, $path);
316
317
        $result = $matcher->match($request);
318
        if (!$result->isSuccess()) {
319
            return $this->responseFactory->createResponse([
320
                'result' => false,
321
            ]);
322
        }
323
324
        $route = $result->route();
325
        $reflection = new \ReflectionObject($route);
326
        $property = $reflection->getProperty('middlewareDefinitions');
327
        $middlewareDefinitions = $property->getValue($route);
328
        $action = end($middlewareDefinitions);
329
330
        return $this->responseFactory->createResponse([
331
            'result' => true,
332
            'action' => $action,
333
        ]);
334
    }
335
336
    public function getTables(SchemaProviderInterface $schemaProvider): ResponseInterface
337
    {
338
        return $this->responseFactory->createResponse($schemaProvider->getTables());
339
    }
340
341
    public function getTable(SchemaProviderInterface $schemaProvider, CurrentRoute $currentRoute): ResponseInterface
342
    {
343
        $tableName = $currentRoute->getArgument('name');
344
345
        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

345
        return $this->responseFactory->createResponse($schemaProvider->getTable(/** @scrutinizer ignore-type */ $tableName));
Loading history...
346
    }
347
348
    public function request(
349
        ServerRequestInterface $request,
350
        CollectorRepositoryInterface $collectorRepository
351
    ): ResponseInterface {
352
        $request = $request->getQueryParams();
353
        $debugEntryId = $request['debugEntryId'] ?? null;
354
355
        $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

355
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
356
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
357
358
        $request = Message::parseRequest($rawRequest);
359
360
        $client = new Client();
361
        $response = $client->send($request);
362
363
        $result = VarDumper::create($response)->asPrimitives();
364
365
        return $this->responseFactory->createResponse($result);
366
    }
367
368
    private function removeBasePath(string $rootPath, string $path): string|array|null
369
    {
370
        return preg_replace(
371
            '/^' . preg_quote($rootPath, '/') . '/',
372
            '',
373
            $path,
374
            1
375
        );
376
    }
377
378
    private function getUserOwner(int $uid): array
379
    {
380
        if ($uid === 0 || !function_exists('posix_getpwuid') || false === ($info = posix_getpwuid($uid))) {
381
            return [
382
                'id' => $uid,
383
            ];
384
        }
385
        return [
386
            'uid' => $info['uid'],
387
            'gid' => $info['gid'],
388
            'name' => $info['name'],
389
        ];
390
    }
391
392
    private function getGroupOwner(int $gid): array
393
    {
394
        if ($gid === 0 || !function_exists('posix_getgrgid') || false === ($info = posix_getgrgid($gid))) {
395
            return [
396
                'id' => $gid,
397
            ];
398
        }
399
        return [
400
            'gid' => $info['gid'],
401
            'name' => $info['name'],
402
        ];
403
    }
404
405
    private function serializeFileInfo(SplFileInfo $file): array
406
    {
407
        return [
408
            'baseName' => $file->getBasename(),
409
            'extension' => $file->getExtension(),
410
            'user' => $this->getUserOwner((int) $file->getOwner()),
411
            'group' => $this->getGroupOwner((int) $file->getGroup()),
412
            'size' => $file->getSize(),
413
            'type' => $file->getType(),
414
            'permissions' => substr(sprintf('%o', $file->getPerms()), -4),
415
        ];
416
    }
417
418
    private function readFile(string $destination): DataResponse
419
    {
420
        $rootPath = $this->aliases->get('@root');
421
        $file = new SplFileInfo($destination);
422
        return $this->responseFactory->createResponse(
423
            array_merge(
424
                [
425
                    'directory' => $this->removeBasePath($rootPath, dirname($destination)),
426
                    'content' => file_get_contents($destination),
427
                    'path' => $this->removeBasePath($rootPath, $destination),
428
                    'absolutePath' => $destination,
429
                ],
430
                $this->serializeFileInfo($file)
431
            )
432
        );
433
    }
434
}
435