GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Pull Request — master (#1)
by Šimon
04:44 queued 13s
created

Helper::readRawBody()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
crap 2
1
<?php
2
namespace GraphQL\Server;
3
4
use GraphQL\Error\Error;
5
use GraphQL\Error\FormattedError;
6
use GraphQL\Error\InvariantViolation;
7
use GraphQL\Executor\ExecutionResult;
8
use GraphQL\Executor\Executor;
9
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
10
use GraphQL\Executor\Promise\Promise;
11
use GraphQL\Executor\Promise\PromiseAdapter;
12
use GraphQL\GraphQL;
13
use GraphQL\Language\AST\DocumentNode;
14
use GraphQL\Language\Parser;
15
use GraphQL\Utils\AST;
16
use GraphQL\Utils\Utils;
17
use Psr\Http\Message\ResponseInterface;
18
use Psr\Http\Message\ServerRequestInterface;
19
use Psr\Http\Message\StreamInterface;
20
21
/**
22
 * Contains functionality that could be re-used by various server implementations
23
 */
24
class Helper
25
{
26
27
    /**
28
     * Parses HTTP request using PHP globals and returns GraphQL OperationParams
29
     * contained in this request. For batched requests it returns an array of OperationParams.
30
     *
31
     * This function does not check validity of these params
32
     * (validation is performed separately in validateOperationParams() method).
33
     *
34
     * If $readRawBodyFn argument is not provided - will attempt to read raw request body
35
     * from `php://input` stream.
36
     *
37
     * Internally it normalizes input to $method, $bodyParams and $queryParams and
38
     * calls `parseRequestParams()` to produce actual return value.
39
     *
40
     * For PSR-7 request parsing use `parsePsrRequest()` instead.
41
     *
42
     * @api
43
     * @param callable|null $readRawBodyFn
44
     * @return OperationParams|OperationParams[]
45
     * @throws RequestError
46
     */
47 14
    public function parseHttpRequest(callable $readRawBodyFn = null)
48
    {
49 14
        $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null;
50 14
        $bodyParams = [];
51 14
        $urlParams = $_GET;
52
53 14
        if ($method === 'POST') {
54 12
            $contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null;
55
56 12
            if (stripos($contentType, 'application/graphql') !== false) {
57 1
                $rawBody =  $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
58 1
                $bodyParams = ['query' => $rawBody ?: ''];
59 11
            } else if (stripos($contentType, 'application/json') !== false) {
60 7
                $rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
61 7
                $bodyParams = json_decode($rawBody ?: '', true);
62
63 7
                if (json_last_error()) {
64 1
                    throw new RequestError("Could not parse JSON: " . json_last_error_msg());
65
                }
66 6
                if (!is_array($bodyParams)) {
67 1
                    throw new RequestError(
68
                        "GraphQL Server expects JSON object or array, but got " .
69 6
                        Utils::printSafeJson($bodyParams)
70
                    );
71
                }
72 4
            } else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
73 1
                $bodyParams = $_POST;
74 3
            } else if (null === $contentType) {
75 1
                throw new RequestError('Missing "Content-Type" header');
76
            } else {
77 2
                throw new RequestError("Unexpected content type: " . Utils::printSafeJson($contentType));
78
            }
79
        }
80
81 9
        return $this->parseRequestParams($method, $bodyParams, $urlParams);
82
    }
83
84
    /**
85
     * Parses normalized request params and returns instance of OperationParams
86
     * or array of OperationParams in case of batch operation.
87
     *
88
     * Returned value is a suitable input for `executeOperation` or `executeBatch` (if array)
89
     *
90
     * @api
91
     * @param string $method
92
     * @param array $bodyParams
93
     * @param array $queryParams
94
     * @return OperationParams|OperationParams[]
95
     * @throws RequestError
96
     */
97 12
    public function parseRequestParams($method, array $bodyParams, array $queryParams)
98
    {
99 12
        if ($method === 'GET') {
100 1
            $result = OperationParams::create($queryParams, true);
101 11
        } else if ($method === 'POST') {
102 9
            if (isset($bodyParams[0])) {
103 1
                $result = [];
104 1
                foreach ($bodyParams as $index => $entry) {
105 1
                    $op = OperationParams::create($entry);
106 1
                    $result[] = $op;
107
                }
108
            } else {
109 9
                $result = OperationParams::create($bodyParams);
110
            }
111
        } else {
112 2
            throw new RequestError('HTTP Method "' . $method . '" is not supported');
113
        }
114 10
        return $result;
115
    }
116
117
    /**
118
     * Checks validity of OperationParams extracted from HTTP request and returns an array of errors
119
     * if params are invalid (or empty array when params are valid)
120
     *
121
     * @api
122
     * @param OperationParams $params
123
     * @return Error[]
124
     */
125 35
    public function validateOperationParams(OperationParams $params)
126
    {
127 35
        $errors = [];
128 35
        if (!$params->query && !$params->queryId) {
129 2
            $errors[] = new RequestError('GraphQL Request must include at least one of those two parameters: "query" or "queryId"');
130
        }
131 35
        if ($params->query && $params->queryId) {
132 1
            $errors[] = new RequestError('GraphQL Request parameters "query" and "queryId" are mutually exclusive');
133
        }
134
135 35
        if ($params->query !== null && (!is_string($params->query) || empty($params->query))) {
0 ignored issues
show
introduced by
The condition is_string($params->query) is always true.
Loading history...
136 1
            $errors[] = new RequestError(
137
                'GraphQL Request parameter "query" must be string, but got ' .
138 1
                Utils::printSafeJson($params->query)
139
            );
140
        }
141 35
        if ($params->queryId !== null && (!is_string($params->queryId) || empty($params->queryId))) {
0 ignored issues
show
introduced by
The condition is_string($params->queryId) is always true.
Loading history...
142 1
            $errors[] = new RequestError(
143
                'GraphQL Request parameter "queryId" must be string, but got ' .
144 1
                Utils::printSafeJson($params->queryId)
145
            );
146
        }
147
148 35
        if ($params->operation !== null && (!is_string($params->operation) || empty($params->operation))) {
0 ignored issues
show
introduced by
The condition is_string($params->operation) is always true.
Loading history...
149 1
            $errors[] = new RequestError(
150
                'GraphQL Request parameter "operation" must be string, but got ' .
151 1
                Utils::printSafeJson($params->operation)
152
            );
153
        }
154 35
        if ($params->variables !== null && (!is_array($params->variables) || isset($params->variables[0]))) {
0 ignored issues
show
introduced by
The condition is_array($params->variables) is always true.
Loading history...
155 1
            $errors[] = new RequestError(
156
                'GraphQL Request parameter "variables" must be object or JSON string parsed to object, but got ' .
157 1
                Utils::printSafeJson($params->getOriginalInput('variables'))
158
            );
159
        }
160 35
        return $errors;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $errors returns an array which contains values of type GraphQL\Server\RequestError which are incompatible with the documented value type GraphQL\Error\Error.
Loading history...
161
    }
162
163
    /**
164
     * Executes GraphQL operation with given server configuration and returns execution result
165
     * (or promise when promise adapter is different from SyncPromiseAdapter)
166
     *
167
     * @api
168
     * @param ServerConfig $config
169
     * @param OperationParams $op
170
     *
171
     * @return ExecutionResult|Promise
172
     */
173 24
    public function executeOperation(ServerConfig $config, OperationParams $op)
174
    {
175 24
        $promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter();
176 24
        $result = $this->promiseToExecuteOperation($promiseAdapter, $config, $op);
177
178 22
        if ($promiseAdapter instanceof SyncPromiseAdapter) {
179 22
            $result = $promiseAdapter->wait($result);
180
        }
181
182 22
        return $result;
183
    }
184
185
    /**
186
     * Executes batched GraphQL operations with shared promise queue
187
     * (thus, effectively batching deferreds|promises of all queries at once)
188
     *
189
     * @api
190
     * @param ServerConfig $config
191
     * @param OperationParams[] $operations
192
     * @return ExecutionResult[]|Promise
193
     */
194 3
    public function executeBatch(ServerConfig $config, array $operations)
195
    {
196 3
        $promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter();
197 3
        $result = [];
198
199 3
        foreach ($operations as $operation) {
200 3
            $result[] = $this->promiseToExecuteOperation($promiseAdapter, $config, $operation, true);
201
        }
202
203 3
        $result = $promiseAdapter->all($result);
204
205
        // Wait for promised results when using sync promises
206 3
        if ($promiseAdapter instanceof SyncPromiseAdapter) {
207 3
            $result = $promiseAdapter->wait($result);
208
        }
209 3
        return $result;
210
    }
211
212
    /**
213
     * @param PromiseAdapter $promiseAdapter
214
     * @param ServerConfig $config
215
     * @param OperationParams $op
216
     * @param bool $isBatch
217
     * @return Promise
218
     */
219 27
    private function promiseToExecuteOperation(PromiseAdapter $promiseAdapter, ServerConfig $config, OperationParams $op, $isBatch = false)
220
    {
221
        try {
222 27
            if (!$config->getSchema()) {
223
                throw new InvariantViolation("Schema is required for the server");
224
            }
225 27
            if ($isBatch && !$config->getQueryBatching()) {
226 1
                throw new RequestError("Batched queries are not supported by this server");
227
            }
228
229 26
            $errors = $this->validateOperationParams($op);
230
231 26
            if (!empty($errors)) {
232
                $errors = Utils::map($errors, function(RequestError $err) {
233 1
                    return Error::createLocatedError($err, null, null);
234 1
                });
235 1
                return $promiseAdapter->createFulfilled(
236 1
                    new ExecutionResult(null, $errors)
237
                );
238
            }
239
240 25
            $doc = $op->queryId ? $this->loadPersistedQuery($config, $op) : $op->query;
241
242 23
            if (!$doc instanceof DocumentNode) {
243 23
                $doc = Parser::parse($doc);
244
            }
245
246 22
            $operationType = AST::getOperation($doc, $op->operation);
247 22
            if ($op->isReadOnly() && $operationType !== 'query') {
248 1
                throw new RequestError("GET supports only query operation");
249
            }
250
251 21
            $result = GraphQL::promiseToExecute(
252 21
                $promiseAdapter,
253 21
                $config->getSchema(),
254 21
                $doc,
255 21
                $this->resolveRootValue($config, $op, $doc, $operationType),
256 21
                $this->resolveContextValue($config, $op, $doc, $operationType),
257 21
                $op->variables,
258 21
                $op->operation,
259 21
                $config->getFieldResolver(),
260 21
                $this->resolveValidationRules($config, $op, $doc, $operationType)
261
            );
262
263 6
        } catch (RequestError $e) {
264 3
            $result = $promiseAdapter->createFulfilled(
265 3
                new ExecutionResult(null, [Error::createLocatedError($e)])
266
            );
267 3
        } catch (Error $e) {
268 1
            $result = $promiseAdapter->createFulfilled(
269 1
                new ExecutionResult(null, [$e])
270
            );
271
        }
272
273
        $applyErrorHandling = function (ExecutionResult $result) use ($config) {
274 24
            if ($config->getErrorsHandler()) {
275 1
                $result->setErrorsHandler($config->getErrorsHandler());
276
            }
277 24
            if ($config->getErrorFormatter() || $config->getDebug()) {
278 2
                $result->setErrorFormatter(
279 2
                    FormattedError::prepareFormatter($config->getErrorFormatter(),
280 2
                    $config->getDebug())
281
                );
282
            }
283 24
            return $result;
284 24
        };
285
286 24
        return $result->then($applyErrorHandling);
287
    }
288
289
    /**
290
     * @param ServerConfig $config
291
     * @param OperationParams $op
292
     * @return mixed
293
     * @throws RequestError
294
     */
295 5
    private function loadPersistedQuery(ServerConfig $config, OperationParams $op)
296
    {
297
        // Load query if we got persisted query id:
298 5
        $loader = $config->getPersistentQueryLoader();
299
300 5
        if (!$loader) {
301 1
            throw new RequestError("Persisted queries are not supported by this server");
302
        }
303
304 4
        $source = $loader($op->queryId, $op);
305
306 4
        if (!is_string($source) && !$source instanceof DocumentNode) {
307 1
            throw new InvariantViolation(sprintf(
308 1
                "Persistent query loader must return query string or instance of %s but got: %s",
309 1
                DocumentNode::class,
310 1
                Utils::printSafe($source)
311
            ));
312
        }
313
314 3
        return $source;
315
    }
316
317
    /**
318
     * @param ServerConfig $config
319
     * @param OperationParams $params
320
     * @param DocumentNode $doc
321
     * @param $operationType
322
     * @return array
323
     */
324 21
    private function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType)
325
    {
326
        // Allow customizing validation rules per operation:
327 21
        $validationRules = $config->getValidationRules();
328
329 21
        if (is_callable($validationRules)) {
330 4
            $validationRules = $validationRules($params, $doc, $operationType);
331
332 4
            if (!is_array($validationRules)) {
333 1
                throw new InvariantViolation(sprintf(
334 1
                    "Expecting validation rules to be array or callable returning array, but got: %s",
335 1
                    Utils::printSafe($validationRules)
336
                ));
337
            }
338
        }
339
340 20
        return $validationRules;
341
    }
342
343
    /**
344
     * @param ServerConfig $config
345
     * @param OperationParams $params
346
     * @param DocumentNode $doc
347
     * @param $operationType
348
     * @return mixed
349
     */
350 21
    private function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType)
351
    {
352 21
        $root = $config->getRootValue();
353
354 21
        if ($root instanceof \Closure) {
355 1
            $root = $root($params, $doc, $operationType);
356
        }
357
358 21
        return $root;
359
    }
360
361
    /**
362
     * @param ServerConfig $config
363
     * @param OperationParams $params
364
     * @param DocumentNode $doc
365
     * @param $operationType
366
     * @return mixed
367
     */
368 21
    private function resolveContextValue(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType)
369
    {
370 21
        $context = $config->getContext();
371
372 21
        if ($context instanceof \Closure) {
373 1
            $context = $context($params, $doc, $operationType);
374
        }
375
376 21
        return $context;
377
    }
378
379
    /**
380
     * Send response using standard PHP `header()` and `echo`.
381
     *
382
     * @api
383
     * @param Promise|ExecutionResult|ExecutionResult[] $result
384
     * @param bool $exitWhenDone
385
     */
386
    public function sendResponse($result, $exitWhenDone = false)
387
    {
388
        if ($result instanceof Promise) {
389
            $result->then(function($actualResult) use ($exitWhenDone) {
390
                $this->doSendResponse($actualResult, $exitWhenDone);
391
            });
392
        } else {
393
            $this->doSendResponse($result, $exitWhenDone);
394
        }
395
    }
396
397
    /**
398
     * @param $result
399
     * @param $exitWhenDone
400
     */
401
    private function doSendResponse($result, $exitWhenDone)
402
    {
403
        $httpStatus = $this->resolveHttpStatus($result);
404
        $this->emitResponse($result, $httpStatus, $exitWhenDone);
405
    }
406
407
    /**
408
     * @param array|\JsonSerializable $jsonSerializable
409
     * @param int $httpStatus
410
     * @param bool $exitWhenDone
411
     */
412
    public function emitResponse($jsonSerializable, $httpStatus, $exitWhenDone)
413
    {
414
        $body = json_encode($jsonSerializable);
415
        header('Content-Type: application/json', true, $httpStatus);
416
        echo $body;
417
418
        if ($exitWhenDone) {
419
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
420
        }
421
    }
422
423
    /**
424
     * @return bool|string
425
     */
426
    private function readRawBody()
427
    {
428
        return file_get_contents('php://input');
429
    }
430
431
    /**
432
     * @param $result
433
     * @return int
434
     */
435 1
    private function resolveHttpStatus($result)
436
    {
437 1
        if (is_array($result) && isset($result[0])) {
438
            Utils::each($result, function ($executionResult, $index) {
439
                if (!$executionResult instanceof ExecutionResult) {
440
                    throw new InvariantViolation(sprintf(
441
                        "Expecting every entry of batched query result to be instance of %s but entry at position %d is %s",
442
                        ExecutionResult::class,
443
                        $index,
444
                        Utils::printSafe($executionResult)
445
                    ));
446
                }
447
            });
448
            $httpStatus = 200;
449
        } else {
450 1
            if (!$result instanceof ExecutionResult) {
451
                throw new InvariantViolation(sprintf(
452
                    "Expecting query result to be instance of %s but got %s",
453
                    ExecutionResult::class,
454
                    Utils::printSafe($result)
455
                ));
456
            }
457 1
            if ($result->data === null && !empty($result->errors)) {
458
                $httpStatus = 400;
459
            } else {
460 1
                $httpStatus = 200;
461
            }
462
        }
463 1
        return $httpStatus;
464
    }
465
466
    /**
467
     * Converts PSR-7 request to OperationParams[]
468
     *
469
     * @api
470
     * @param ServerRequestInterface $request
471
     * @return array|Helper
472
     * @throws RequestError
473
     */
474 14
    public function parsePsrRequest(ServerRequestInterface $request)
475
    {
476 14
        if ($request->getMethod() === 'GET') {
477 1
            $bodyParams = [];
478
        } else {
479 13
            $contentType = $request->getHeader('content-type');
480
481 13
            if (!isset($contentType[0])) {
482 1
                throw new RequestError('Missing "Content-Type" header');
483
            }
484
485 12
            if (stripos($contentType[0], 'application/graphql') !== false) {
486 1
                $bodyParams = ['query' => $request->getBody()->getContents()];
487 11
            } else if (stripos($contentType[0], 'application/json') !== false) {
488 10
                $bodyParams = $request->getParsedBody();
489
490 10
                if (null === $bodyParams) {
491 2
                    throw new InvariantViolation(
492 2
                        "PSR-7 request is expected to provide parsed body for \"application/json\" requests but got null"
493
                    );
494
                }
495
496 8
                if (!is_array($bodyParams)) {
497 1
                    throw new RequestError(
498
                        "GraphQL Server expects JSON object or array, but got " .
499 8
                        Utils::printSafeJson($bodyParams)
500
                    );
501
                }
502
            } else {
503 1
                $bodyParams = $request->getParsedBody();
504
505 1
                if (!is_array($bodyParams)) {
506
                    throw new RequestError("Unexpected content type: " . Utils::printSafeJson($contentType[0]));
507
                }
508
            }
509
        }
510
511 10
        return $this->parseRequestParams(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->parseReque...uest->getQueryParams()) returns the type GraphQL\Server\Operation...\Server\OperationParams which is incompatible with the documented return type GraphQL\Server\Helper|array.
Loading history...
512 10
            $request->getMethod(),
513 10
            $bodyParams,
514 10
            $request->getQueryParams()
515
        );
516
    }
517
518
    /**
519
     * Converts query execution result to PSR-7 response
520
     *
521
     * @api
522
     * @param Promise|ExecutionResult|ExecutionResult[] $result
523
     * @param ResponseInterface $response
524
     * @param StreamInterface $writableBodyStream
525
     * @return Promise|ResponseInterface
526
     */
527 1
    public function toPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
528
    {
529 1
        if ($result instanceof Promise) {
530
            return $result->then(function($actualResult) use ($response, $writableBodyStream) {
531
                return $this->doConvertToPsrResponse($actualResult, $response, $writableBodyStream);
532
            });
533
        } else {
534 1
            return $this->doConvertToPsrResponse($result, $response, $writableBodyStream);
535
        }
536
    }
537
538 1
    private function doConvertToPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
539
    {
540 1
        $httpStatus = $this->resolveHttpStatus($result);
541
542 1
        $result = json_encode($result);
543 1
        $writableBodyStream->write($result);
544
545
        return $response
546 1
            ->withStatus($httpStatus)
547 1
            ->withHeader('Content-Type', 'application/json')
548 1
            ->withBody($writableBodyStream);
549
    }
550
}
551