Passed
Pull Request — master (#3063)
by
unknown
04:28
created

EntrypointAction   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 187
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 88
dl 0
loc 187
rs 9.28
c 0
b 0
f 0
wmc 39

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 2
B applyMapToVariables() 0 33 8
A parseRequest() 0 25 6
A buildExceptionResponse() 0 5 1
A parseMultipartRequest() 0 17 4
A parseData() 0 19 6
B __invoke() 0 29 10
A decodeVariables() 0 7 2
1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace ApiPlatform\Core\GraphQl\Action;
15
16
use ApiPlatform\Core\GraphQl\Exception\ExceptionFormatterCallbackInterface;
17
use ApiPlatform\Core\GraphQl\ExecutorInterface;
18
use ApiPlatform\Core\GraphQl\Type\SchemaBuilderInterface;
19
use GraphQL\Error\Debug;
20
use GraphQL\Error\Error;
21
use GraphQL\Error\UserError;
22
use GraphQL\Executor\ExecutionResult;
23
use Symfony\Component\HttpFoundation\JsonResponse;
24
use Symfony\Component\HttpFoundation\Request;
25
use Symfony\Component\HttpFoundation\Response;
26
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
27
28
/**
29
 * GraphQL API entrypoint.
30
 *
31
 * @author Alan Poulain <[email protected]>
32
 */
33
final class EntrypointAction
34
{
35
    private $schemaBuilder;
36
    private $executor;
37
    private $graphiQlAction;
38
    private $graphQlPlaygroundAction;
39
    private $debug;
40
    private $graphiqlEnabled;
41
    private $graphQlPlaygroundEnabled;
42
    private $defaultIde;
43
    private $exceptionFormatterCallback;
44
45
    public function __construct(SchemaBuilderInterface $schemaBuilder, ExecutorInterface $executor, GraphiQlAction $graphiQlAction, GraphQlPlaygroundAction $graphQlPlaygroundAction, ExceptionFormatterCallbackInterface $exceptionFormatterCallback, bool $debug = false, bool $graphiqlEnabled = false, bool $graphQlPlaygroundEnabled = false, $defaultIde = false)
46
    {
47
        $this->schemaBuilder = $schemaBuilder;
48
        $this->executor = $executor;
49
        $this->graphiQlAction = $graphiQlAction;
50
        $this->graphQlPlaygroundAction = $graphQlPlaygroundAction;
51
        $this->debug = $debug ? Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE : false;
52
        $this->graphiqlEnabled = $graphiqlEnabled;
53
        $this->graphQlPlaygroundEnabled = $graphQlPlaygroundEnabled;
54
        $this->defaultIde = $defaultIde;
55
        $this->exceptionFormatterCallback = $exceptionFormatterCallback;
56
    }
57
58
    public function __invoke(Request $request): Response
59
    {
60
        if ($request->isMethod('GET') && 'html' === $request->getRequestFormat()) {
61
            if ('graphiql' === $this->defaultIde && $this->graphiqlEnabled) {
62
                return ($this->graphiQlAction)($request);
63
            }
64
65
            if ('graphql-playground' === $this->defaultIde && $this->graphQlPlaygroundEnabled) {
66
                return ($this->graphQlPlaygroundAction)($request);
67
            }
68
        }
69
70
        try {
71
            [$query, $operation, $variables] = $this->parseRequest($request);
72
            if (null === $query) {
73
                throw new BadRequestHttpException('GraphQL query is not valid.');
74
            }
75
76
            $executionResult = $this->executor->executeQuery($this->schemaBuilder->getSchema(), $query, null, null, $variables, $operation)
77
              ->setErrorFormatter($this->exceptionFormatterCallback);
78
        } catch (BadRequestHttpException $e) {
79
            $exception = new UserError($e->getMessage(), 0, $e);
80
81
            return $this->buildExceptionResponse($exception, Response::HTTP_BAD_REQUEST);
82
        } catch (\Exception $e) {
83
            return $this->buildExceptionResponse($e, Response::HTTP_OK);
84
        }
85
86
        return new JsonResponse($executionResult->toArray($this->debug));
87
    }
88
89
    /**
90
     * @throws BadRequestHttpException
91
     */
92
    private function parseRequest(Request $request): array
93
    {
94
        $query = $request->query->get('query');
95
        $operation = $request->query->get('operation');
96
        if ($variables = $request->query->get('variables', [])) {
97
            $variables = $this->decodeVariables($variables);
98
        }
99
100
        if (!$request->isMethod('POST')) {
101
            return [$query, $operation, $variables];
102
        }
103
104
        if ('json' === $request->getContentType()) {
105
            return $this->parseData($query, $operation, $variables, $request->getContent());
106
        }
107
108
        if ('graphql' === $request->getContentType()) {
109
            $query = $request->getContent();
110
        }
111
112
        if ('multipart' === $request->getContentType()) {
113
            return $this->parseMultipartRequest($query, $operation, $variables, $request->request->all(), $request->files->all());
114
        }
115
116
        return [$query, $operation, $variables];
117
    }
118
119
    /**
120
     * @throws BadRequestHttpException
121
     */
122
    private function parseData(?string $query, ?string $operation, array $variables, string $jsonContent): array
123
    {
124
        if (!\is_array($data = json_decode($jsonContent, true))) {
125
            throw new BadRequestHttpException('GraphQL data is not valid JSON.');
126
        }
127
128
        if (isset($data['query'])) {
129
            $query = $data['query'];
130
        }
131
132
        if (isset($data['variables'])) {
133
            $variables = \is_array($data['variables']) ? $data['variables'] : $this->decodeVariables($data['variables']);
134
        }
135
136
        if (isset($data['operation'])) {
137
            $operation = $data['operation'];
138
        }
139
140
        return [$query, $operation, $variables];
141
    }
142
143
    /**
144
     * @throws BadRequestHttpException
145
     */
146
    private function parseMultipartRequest(?string $query, ?string $operation, array $variables, array $bodyParameters, array $files): array
147
    {
148
        /** @var string $operations */
149
        /** @var string $map */
150
        if ((null === $operations = $bodyParameters['operations'] ?? null) || (null === $map = $bodyParameters['map'] ?? null)) {
151
            throw new BadRequestHttpException('GraphQL multipart request does not respect the specification.');
152
        }
153
154
        [$query, $operation, $variables] = $this->parseData($query, $operation, $variables, $operations);
155
156
        if (!\is_array($decodedMap = json_decode($map, true))) {
157
            throw new BadRequestHttpException('GraphQL multipart request map is not valid JSON.');
158
        }
159
160
        $variables = $this->applyMapToVariables($decodedMap, $variables, $files);
161
162
        return [$query, $operation, $variables];
163
    }
164
165
    /**
166
     * @throws BadRequestHttpException
167
     */
168
    private function applyMapToVariables(array $map, array $variables, array $files): array
169
    {
170
        foreach ($map as $key => $value) {
171
            if (null === $file = $files[$key] ?? null) {
172
                throw new BadRequestHttpException('GraphQL multipart request file has not been sent correctly.');
173
            }
174
175
            foreach ($map[$key] as $mapValue) {
176
                $path = explode('.', $mapValue);
177
178
                if ('variables' !== $path[0]) {
179
                    throw new BadRequestHttpException('GraphQL multipart request path in map is invalid.');
180
                }
181
182
                unset($path[0]);
183
184
                $mapPathExistsInVariables = array_reduce($path, static function (array $inVariables, string $pathElement) {
185
                    return \array_key_exists($pathElement, $inVariables) ? $inVariables[$pathElement] : false;
186
                }, $variables);
187
188
                if (false === $mapPathExistsInVariables) {
189
                    throw new BadRequestHttpException('GraphQL multipart request path in map does not match the variables.');
190
                }
191
192
                $variableFileValue = &$variables;
193
                foreach ($path as $pathValue) {
194
                    $variableFileValue = &$variableFileValue[$pathValue];
195
                }
196
                $variableFileValue = $file;
197
            }
198
        }
199
200
        return $variables;
201
    }
202
203
    /**
204
     * @throws BadRequestHttpException
205
     */
206
    private function decodeVariables(string $variables): array
207
    {
208
        if (!\is_array($variables = json_decode($variables, true))) {
209
            throw new BadRequestHttpException('GraphQL variables are not valid JSON.');
210
        }
211
212
        return $variables;
213
    }
214
215
    private function buildExceptionResponse(\Exception $e, int $statusCode): JsonResponse
216
    {
217
        $executionResult = new ExecutionResult(null, [new Error($e->getMessage(), null, null, null, null, $e)]);
218
219
        return new JsonResponse($executionResult->toArray($this->debug), $statusCode);
220
    }
221
}
222