Passed
Push — master ( ce7fff...9920f1 )
by Alan
04:04
created

EntrypointAction::buildExceptionResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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