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

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

353
        return $this->responseFactory->createResponse($schemaProvider->getTable(/** @scrutinizer ignore-type */ $tableName));
Loading history...
354
    }
355
356
    public function request(
357
        ServerRequestInterface $request,
358
        CollectorRepositoryInterface $collectorRepository
359
    ): ResponseInterface {
360
        $request = $request->getQueryParams();
361
        $debugEntryId = $request['debugEntryId'] ?? null;
362
363
        $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

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