Passed
Push — master ( e0a1df...b8a631 )
by Dmitriy
03:02
created

InspectController::buildCurl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

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

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

364
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
365
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
366
367
        $request = Message::parseRequest($rawRequest);
368
369
        $client = new Client();
370
        $response = $client->send($request);
371
372
        $result = VarDumper::create($response)->asPrimitives();
373
374
        return $this->responseFactory->createResponse($result);
375
    }
376
377
    public function buildCurl(
378
        ServerRequestInterface $request,
379
        CollectorRepositoryInterface $collectorRepository
380
    ): ResponseInterface {
381
        $request = $request->getQueryParams();
382
        $debugEntryId = $request['debugEntryId'] ?? null;
383
384
385
        $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

385
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
386
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
387
388
        $request = Message::parseRequest($rawRequest);
389
390
        try {
391
            // https://github.com/alexkart/curl-builder/issues/7
392
            $output = (new Command())
393
                ->setRequest($request)
394
                ->build();
395
        } catch (Throwable $e) {
396
            return $this->responseFactory->createResponse([
397
                'command' => null,
398
                'exception' => (string)$e,
399
            ]);
400
        }
401
402
        return $this->responseFactory->createResponse([
403
            'command' => $output,
404
        ]);
405
    }
406
407
    private function removeBasePath(string $rootPath, string $path): string|array|null
408
    {
409
        return preg_replace(
410
            '/^' . preg_quote($rootPath, '/') . '/',
411
            '',
412
            $path,
413
            1
414
        );
415
    }
416
417
    private function getUserOwner(int $uid): array
418
    {
419
        if ($uid === 0 || !function_exists('posix_getpwuid') || false === ($info = posix_getpwuid($uid))) {
420
            return [
421
                'id' => $uid,
422
            ];
423
        }
424
        return [
425
            'uid' => $info['uid'],
426
            'gid' => $info['gid'],
427
            'name' => $info['name'],
428
        ];
429
    }
430
431
    private function getGroupOwner(int $gid): array
432
    {
433
        if ($gid === 0 || !function_exists('posix_getgrgid') || false === ($info = posix_getgrgid($gid))) {
434
            return [
435
                'id' => $gid,
436
            ];
437
        }
438
        return [
439
            'gid' => $info['gid'],
440
            'name' => $info['name'],
441
        ];
442
    }
443
444
    private function serializeFileInfo(SplFileInfo $file): array
445
    {
446
        return [
447
            'baseName' => $file->getBasename(),
448
            'extension' => $file->getExtension(),
449
            'user' => $this->getUserOwner((int) $file->getOwner()),
450
            'group' => $this->getGroupOwner((int) $file->getGroup()),
451
            'size' => $file->getSize(),
452
            'type' => $file->getType(),
453
            'permissions' => substr(sprintf('%o', $file->getPerms()), -4),
454
        ];
455
    }
456
457
    private function readFile(string $destination): DataResponse
458
    {
459
        $rootPath = $this->aliases->get('@root');
460
        $file = new SplFileInfo($destination);
461
        return $this->responseFactory->createResponse(
462
            array_merge(
463
                [
464
                    'directory' => $this->removeBasePath($rootPath, dirname($destination)),
465
                    'content' => file_get_contents($destination),
466
                    'path' => $this->removeBasePath($rootPath, $destination),
467
                    'absolutePath' => $destination,
468
                ],
469
                $this->serializeFileInfo($file)
470
            )
471
        );
472
    }
473
}
474