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

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

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

318
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
319
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
320
321
        $request = Message::parseRequest($rawRequest);
322
323
        $client = new Client();
324
        $response = $client->send($request);
325
326
        $result = VarDumper::create($response)->asPrimitives();
327
328
        return $this->responseFactory->createResponse($result);
329
    }
330
331
    public function buildCurl(
332
        ServerRequestInterface $request,
333
        CollectorRepositoryInterface $collectorRepository
334
    ): ResponseInterface {
335
        $request = $request->getQueryParams();
336
        $debugEntryId = $request['debugEntryId'] ?? null;
337
338
339
        $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

339
        $data = $collectorRepository->getDetail(/** @scrutinizer ignore-type */ $debugEntryId);
Loading history...
340
        $rawRequest = $data[RequestCollector::class]['requestRaw'];
341
342
        $request = Message::parseRequest($rawRequest);
343
344
        try {
345
            // https://github.com/alexkart/curl-builder/issues/7
346
            $output = (new Command())
347
                ->setRequest($request)
0 ignored issues
show
Bug introduced by
$request of type GuzzleHttp\Psr7\Request is incompatible with the type Psr\Http\Message\ServerRequestInterface|null expected by parameter $request of Alexkart\CurlBuilder\Command::setRequest(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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