diff --git a/vendor/magento/module-customer-graph-ql/Controller/HttpRequestValidator/AuthorizationRequestValidator.php b/vendor/magento/module-customer-graph-ql/Controller/HttpRequestValidator/AuthorizationRequestValidator.php new file mode 100644 index 0000000000000..8131419a1825e --- /dev/null +++ b/vendor/magento/module-customer-graph-ql/Controller/HttpRequestValidator/AuthorizationRequestValidator.php @@ -0,0 +1,60 @@ +getHeader(self::AUTH); + if (!$authorizationHeaderValue) { + return; + } + + $headerPieces = explode(' ', $authorizationHeaderValue); + if (count($headerPieces) !== 2 || strtolower($headerPieces[0]) !== self::BEARER) { + return; + } + + try { + $this->tokenValidator->validate($this->tokenReader->read($headerPieces[1])); + } catch (UserTokenException | AuthorizationException $exception) { + throw new GraphQlAuthenticationException(__($exception->getMessage())); + } + } +} diff --git a/vendor/magento/module-customer-graph-ql/etc/graphql/di.xml b/vendor/magento/module-customer-graph-ql/etc/graphql/di.xml index 305e9cd12d676..1f848f2fb6849 100644 --- a/vendor/magento/module-customer-graph-ql/etc/graphql/di.xml +++ b/vendor/magento/module-customer-graph-ql/etc/graphql/di.xml @@ -209,4 +209,11 @@ + + + + Magento\CustomerGraphQl\Controller\HttpRequestValidator\AuthorizationRequestValidator + + + diff --git a/vendor/magento/module-graph-ql/Controller/GraphQl.php b/vendor/magento/module-graph-ql/Controller/GraphQl.php index f20956407c258..e265fce154db7 100644 --- a/vendor/magento/module-graph-ql/Controller/GraphQl.php +++ b/vendor/magento/module-graph-ql/Controller/GraphQl.php @@ -1,14 +1,16 @@ areaList->getArea(Area::AREA_GRAPHQL)->load(Area::PART_TRANSLATE); - - $statusCode = 200; $jsonResult = $this->jsonFactory->create(); - $data = $this->getDataFromRequest($request); - $result = []; - + $data = []; + $result = null; $schema = null; + try { + $data = $this->getDataFromRequest($request); + $query = $data['query'] ?? ''; + /** @var Http $request */ $this->requestProcessor->validateRequest($request); - $query = $data['query'] ?? ''; - $parsedQuery = $this->queryParser->parse($query); - $data['parsedQuery'] = $parsedQuery; - - // We must extract queried field names to avoid instantiation of unnecessary fields in webonyx schema - // Temporal coupling is required for performance optimization - $this->queryFields->setQuery($parsedQuery, $data['variables'] ?? null); - $schema = $this->schemaGenerator->generate(); - - $result = $this->queryProcessor->process( - $schema, - $parsedQuery, - $this->contextFactory->create(), - $data['variables'] ?? [] - ); - } catch (\Exception $error) { - $result['errors'] = isset($result['errors']) ? $result['errors'] : []; - $result['errors'][] = $this->graphQlError->create($error); - $statusCode = ExceptionFormatter::HTTP_GRAPH_QL_SCHEMA_ERROR_STATUS; + $statusCode = $request->getMethod() === self::METHOD_OPTIONS ? 204 : 200; + + if ($request->isGet() || $request->isPost()) { + $parsedQuery = $this->queryParser->parse($query); + $data['parsedQuery'] = $parsedQuery; + + // We must extract queried field names to avoid instantiation of unnecessary fields in webonyx schema + // Temporal coupling is required for performance optimization + $this->queryFields->setQuery($parsedQuery, $data['variables'] ?? null); + $schema = $this->schemaGenerator->generate(); + + $result = $this->queryProcessor->process( + $schema, + $parsedQuery, + $this->contextFactory->create(), + $data['variables'] ?? [] + ); + $statusCode = $this->getHttpResponseCode($result); + } + } catch (Exception $error) { + [$result, $statusCode] = $this->handleGraphQlException($error); } $jsonResult->setHttpResponseCode($statusCode); - $jsonResult->setData($result); + if ($result !== null) { + $jsonResult->setData($result); + } $jsonResult->renderResult($this->httpResponse); // log information about the query, unless it is an introspection query @@ -224,25 +237,76 @@ public function dispatch(RequestInterface $request): ResponseInterface return $this->httpResponse; } + /** + * Handle GraphQL Exceptions + * + * @param Exception $e + * @return array + */ + private function handleGraphQlException(Exception $e): array + { + [$error, $statusCode] = match (true) { + $e instanceof InvalidRequestInterface => [FormattedError::createFromException($e), $e->getStatusCode()], + $e instanceof SyntaxError => [FormattedError::createFromException($e), 400], + $e instanceof GraphQlAuthenticationException => [$this->graphQlError->create($e), 401], + $e instanceof GraphQlAuthorizationException => [$this->graphQlError->create($e), 403], + $e instanceof GraphQlInputException => [FormattedError::createFromException($e), 200], + default => [$this->graphQlError->create($e), ExceptionFormatter::HTTP_GRAPH_QL_SCHEMA_ERROR_STATUS], + }; + + return [['errors' => [$error]], $statusCode]; + } + + /** + * Retrieve http response code based on the error categories + * + * @param array $result + * @return int + */ + private function getHttpResponseCode(array $result): int + { + foreach ($result['errors'] ?? [] as $error) { + if (isset($error['extensions']['category'])) { + return match ($error['extensions']['category']) { + GraphQlAuthenticationException::EXCEPTION_CATEGORY => 401, + GraphQlAuthorizationException::EXCEPTION_CATEGORY => 403, + default => 200, + }; + } + } + + return 200; + } + /** * Get data from request body or query string * * @param RequestInterface $request * @return array + * @throws SyntaxError */ private function getDataFromRequest(RequestInterface $request): array { + $data = []; /** @var Http $request */ - if ($request->isPost()) { - $data = $this->jsonSerializer->unserialize($request->getContent()); + if ($request->isPost() && $request->getContent()) { + $content = $request->getContent(); + try { + $data = $this->jsonSerializer->unserialize($content); + } catch (\InvalidArgumentException) { + $source = new Source($content); + throw new SyntaxError($source, 0, 'Unable to parse the request.'); + } } elseif ($request->isGet()) { $data = $request->getParams(); - $data['variables'] = isset($data['variables']) ? - $this->jsonSerializer->unserialize($data['variables']) : null; - $data['variables'] = is_array($data['variables']) ? - $data['variables'] : null; - } else { - return []; + try { + $data['variables'] = !empty($data['variables']) && is_string($data['variables']) + ? $this->jsonSerializer->unserialize($data['variables']) + : null; + } catch (\InvalidArgumentException) { + $source = new Source($data['variables']); + throw new SyntaxError($source, 0, 'Unable to parse the variables.'); + } } return $data; diff --git a/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/ContentTypeValidator.php b/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/ContentTypeValidator.php index 555048aac6771..a8e6a8478f4ce 100644 --- a/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/ContentTypeValidator.php +++ b/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/ContentTypeValidator.php @@ -1,14 +1,15 @@ isPost() && strpos($headerValue, $requiredHeaderValue) === false ) { - throw new GraphQlInputException( - new \Magento\Framework\Phrase('Request content type must be application/json') + throw new UnsupportedMediaTypeException( + new Phrase('Request content type must be application/json') ); } } diff --git a/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/HttpVerbValidator.php b/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/HttpVerbValidator.php index 56351c7711cec..ff4cb247175f9 100644 --- a/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/HttpVerbValidator.php +++ b/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/HttpVerbValidator.php @@ -1,7 +1,7 @@ isSafe; + } +} diff --git a/vendor/magento/framework/GraphQl/Exception/UnsupportedMediaTypeException.php b/vendor/magento/framework/GraphQl/Exception/UnsupportedMediaTypeException.php new file mode 100644 index 0000000000000..6be650a3eb01b --- /dev/null +++ b/vendor/magento/framework/GraphQl/Exception/UnsupportedMediaTypeException.php @@ -0,0 +1,46 @@ +isSafe; + } +} diff --git a/vendor/magento/framework/GraphQl/Schema/SchemaGenerator.php b/vendor/magento/framework/GraphQl/Schema/SchemaGenerator.php index 250b80defa6dd..2a81b5d32ced2 100644 --- a/vendor/magento/framework/GraphQl/Schema/SchemaGenerator.php +++ b/vendor/magento/framework/GraphQl/Schema/SchemaGenerator.php @@ -1,13 +1,14 @@ $this->typeRegistry->get('Query'), 'mutation' => $this->typeRegistry->get('Mutation'), 'typeLoader' => function ($name) { - return $this->typeRegistry->get($name); + try { + return $this->typeRegistry->get($name); + } catch (GraphQlInputException) { + return null; + } }, 'types' => function () { $typesImplementors = []; diff --git a/vendor/magento/framework/GraphQl/Schema/Type/TypeRegistry.php b/vendor/magento/framework/GraphQl/Schema/Type/TypeRegistry.php index 414e1eebe6531..1b8cb47e4048d 100644 --- a/vendor/magento/framework/GraphQl/Schema/Type/TypeRegistry.php +++ b/vendor/magento/framework/GraphQl/Schema/Type/TypeRegistry.php @@ -1,12 +1,13 @@ types[$typeName])) { - $configElement = $this->config->getConfigElement($typeName); + try { + $configElement = $this->config->getConfigElement($typeName); + } catch (LogicException) { + throw new GraphQlInputException( + new Phrase('Unknown type "%1".', [$typeName]) + ); + } $configElementClass = get_class($configElement); if (!isset($this->configToTypeMap[$configElementClass])) {