diff --git a/vendor/magento/module-graph-ql/Controller/GraphQl.php b/vendor/magento/module-graph-ql/Controller/GraphQl.php index f03079c89bc68..ab766de9d83a6 100644 --- a/vendor/magento/module-graph-ql/Controller/GraphQl.php +++ b/vendor/magento/module-graph-ql/Controller/GraphQl.php @@ -1,14 +1,14 @@ response = $response; $this->schemaGenerator = $schemaGenerator; @@ -157,6 +168,7 @@ public function __construct( $this->logDataHelper = $logDataHelper ?: ObjectManager::getInstance()->get(LogData::class); $this->loggerPool = $loggerPool ?: ObjectManager::getInstance()->get(LoggerPool::class); $this->areaList = $areaList ?: ObjectManager::getInstance()->get(AreaList::class); + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); } /** @@ -172,36 +184,49 @@ public function dispatch(RequestInterface $request): ResponseInterface $statusCode = 200; $jsonResult = $this->jsonFactory->create(); - $data = $this->getDataFromRequest($request); - $result = []; - + $data = []; + $result = null; $schema = null; + $query = ''; + try { + $data = $this->getDataFromRequest($request); + $query = $data['query'] ?? ''; + /** @var Http $request */ $this->requestProcessor->validateRequest($request); + if ($request->isGet() || $request->isPost()) { + $parsedQuery = $this->queryParser->parse($query); + $data['parsedQuery'] = $parsedQuery; - $query = $data['query'] ?? ''; - $variables = $data['variables'] ?? null; - - // 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($query, $variables); - $schema = $this->schemaGenerator->generate(); - - $result = $this->queryProcessor->process( - $schema, - $query, - $this->contextFactory->create(), - $data['variables'] ?? [] - ); + // 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 (SyntaxError|GraphQlInputException $error) { + $result = [ + 'errors' => [FormattedError::createFromException($error)], + ]; + $statusCode = 400; } catch (\Exception $error) { - $result['errors'] = isset($result['errors']) ? $result['errors'] : []; - $result['errors'][] = $this->graphQlError->create($error); + $result = [ + 'errors' => [$this->graphQlError->create($error)], + ]; $statusCode = ExceptionFormatter::HTTP_GRAPH_QL_SCHEMA_ERROR_STATUS; } $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 @@ -218,20 +243,25 @@ public function dispatch(RequestInterface $request): ResponseInterface * * @param RequestInterface $request * @return array + * @throws GraphQlInputException */ private function getDataFromRequest(RequestInterface $request): array { - /** @var Http $request */ - if ($request->isPost()) { - $data = $this->jsonSerializer->unserialize($request->getContent()); - } 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 { + /** @var Http $request */ + if ($request->isPost()) { + $data = $request->getContent() ? $this->jsonSerializer->unserialize($request->getContent()) : []; + } 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 { + $data = []; + } + } catch (\InvalidArgumentException $e) { + throw new GraphQlInputException(__('Unable to parse the request.'), $e); } return $data; diff --git a/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/HttpVerbValidator.php b/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/HttpVerbValidator.php index d42676f5dd1b4..56351c7711cec 100644 --- a/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/HttpVerbValidator.php +++ b/vendor/magento/module-graph-ql/Controller/HttpRequestValidator/HttpVerbValidator.php @@ -9,12 +9,12 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\Parser; -use GraphQL\Language\Source; use GraphQL\Language\Visitor; use Magento\Framework\App\HttpRequestInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\Http; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\QueryParser; use Magento\Framework\Phrase; use Magento\GraphQl\Controller\HttpRequestValidatorInterface; @@ -23,6 +23,19 @@ */ class HttpVerbValidator implements HttpRequestValidatorInterface { + /** + * @var QueryParser + */ + private $queryParser; + + /** + * @param QueryParser|null $queryParser + */ + public function __construct(QueryParser $queryParser = null) + { + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); + } + /** * Check if request is using correct verb for query or mutation * @@ -37,9 +50,9 @@ public function validate(HttpRequestInterface $request): void $query = $request->getParam('query', ''); if (!empty($query)) { $operationType = ''; - $queryAst = Parser::parse(new Source($query ?: '', 'GraphQL')); + $parsedQuery = $this->queryParser->parse($query); Visitor::visit( - $queryAst, + $parsedQuery, [ 'leave' => [ NodeKind::OPERATION_DEFINITION => function (Node $node) use (&$operationType) { diff --git a/vendor/magento/module-graph-ql/Helper/Query/Logger/LogData.php b/vendor/magento/module-graph-ql/Helper/Query/Logger/LogData.php index 91e2518bfc634..fd45ef93cf13b 100644 --- a/vendor/magento/module-graph-ql/Helper/Query/Logger/LogData.php +++ b/vendor/magento/module-graph-ql/Helper/Query/Logger/LogData.php @@ -8,13 +8,14 @@ namespace Magento\GraphQl\Helper\Query\Logger; use GraphQL\Error\SyntaxError; +use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\Parser; -use GraphQL\Language\Source; use GraphQL\Language\Visitor; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Response\Http as HttpResponse; +use Magento\Framework\GraphQl\Query\QueryParser; use Magento\Framework\GraphQl\Schema; use Magento\GraphQl\Model\Query\Logger\LoggerInterface; @@ -23,6 +24,19 @@ */ class LogData { + /** + * @var QueryParser + */ + private $queryParser; + + /** + * @param QueryParser|null $queryParser + */ + public function __construct(QueryParser $queryParser = null) + { + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); + } + /** * Extracts relevant information about the request * @@ -43,7 +57,7 @@ public function getLogData( $logData = array_merge($logData, $this->gatherRequestInformation($request)); try { - $complexity = $this->getFieldCount($data['query'] ?? ''); + $complexity = $this->getFieldCount($data['parsedQuery'] ?? $data['query'] ?? ''); $logData[LoggerInterface::COMPLEXITY] = $complexity; if ($schema) { $logData = array_merge($logData, $this->gatherQueryInformation($schema)); @@ -114,18 +128,20 @@ private function gatherResponseInformation(HttpResponse $response) : array * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @param string $query + * @param DocumentNode|string $query * @return int * @throws SyntaxError - * @throws /Exception + * @throws \Exception */ - private function getFieldCount(string $query): int + private function getFieldCount(DocumentNode|string $query): int { if (!empty($query)) { $totalFieldCount = 0; - $queryAst = Parser::parse(new Source($query ?: '', 'GraphQL')); + if (is_string($query)) { + $query = $this->queryParser->parse($query); + } Visitor::visit( - $queryAst, + $query, [ 'leave' => [ NodeKind::FIELD => function (Node $node) use (&$totalFieldCount) { diff --git a/vendor/magento/framework/GraphQl/Query/Fields.php b/vendor/magento/framework/GraphQl/Query/Fields.php index 78062effe3d41..5732f9d75d985 100644 --- a/vendor/magento/framework/GraphQl/Query/Fields.php +++ b/vendor/magento/framework/GraphQl/Query/Fields.php @@ -7,8 +7,10 @@ namespace Magento\Framework\GraphQl\Query; +use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; +use Magento\Framework\App\ObjectManager; /** * This class holds a list of all queried fields and is used to enable performance optimization for schema loading. @@ -20,21 +22,36 @@ class Fields */ private $fieldsUsedInQuery = []; + /** + * @var QueryParser + */ + private $queryParser; + + /** + * @param QueryParser|null $queryParser + */ + public function __construct(QueryParser $queryParser = null) + { + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); + } + /** * Set Query for extracting list of fields. * - * @param string $query + * @param DocumentNode|string $query * @param array|null $variables * * @return void */ - public function setQuery($query, array $variables = null) + public function setQuery(DocumentNode|string $query, array $variables = null) { $queryFields = []; try { - $queryAst = \GraphQL\Language\Parser::parse(new \GraphQL\Language\Source($query ?: '', 'GraphQL')); + if (is_string($query)) { + $query = $this->queryParser->parse($query); + } \GraphQL\Language\Visitor::visit( - $queryAst, + $query, [ 'leave' => [ NodeKind::NAME => function (Node $node) use (&$queryFields) { diff --git a/vendor/magento/framework/GraphQl/Query/QueryComplexityLimiter.php b/vendor/magento/framework/GraphQl/Query/QueryComplexityLimiter.php index 9a2396f5d2f3d..f4359bfc9e457 100644 --- a/vendor/magento/framework/GraphQl/Query/QueryComplexityLimiter.php +++ b/vendor/magento/framework/GraphQl/Query/QueryComplexityLimiter.php @@ -7,10 +7,9 @@ namespace Magento\Framework\GraphQl\Query; -use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\FieldNode; -use GraphQL\Language\Parser; -use GraphQL\Language\Source; +use GraphQL\Language\AST\NodeKind; use GraphQL\Language\Visitor; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Validator\DocumentValidator; @@ -47,6 +46,11 @@ class QueryComplexityLimiter */ private $introspectionConfig; + /** + * @var QueryParser + */ + private $queryParser; + /** * @var MaximumAliasConfiguration */ @@ -56,17 +60,20 @@ class QueryComplexityLimiter * @param int $queryDepth * @param int $queryComplexity * @param IntrospectionConfiguration $introspectionConfig + * @param QueryParser|null $queryParser * @param MaximumAliasConfiguration|null $maximumAliasConfiguration */ public function __construct( int $queryDepth, int $queryComplexity, IntrospectionConfiguration $introspectionConfig, - MaximumAliasConfiguration $maximumAliasConfiguration = null + ?QueryParser $queryParser = null, + ?MaximumAliasConfiguration $maximumAliasConfiguration = null ) { $this->queryDepth = $queryDepth; $this->queryComplexity = $queryComplexity; $this->introspectionConfig = $introspectionConfig; + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); $this->maximumAliasConfiguration = $maximumAliasConfiguration ?: ObjectManager::getInstance()->get(MaximumAliasConfiguration::class); } @@ -92,16 +99,19 @@ public function execute(): void * This is necessary for performance optimization, as extremely large queries require a substantial * amount of time to fully validate and can affect server performance. * - * @param string $query + * @param DocumentNode|string $query * @throws GraphQlInputException */ - public function validateFieldCount(string $query): void + public function validateFieldCount(DocumentNode|string $query): void { if (!empty($query)) { $totalFieldCount = 0; - $queryAst = Parser::parse(new Source($query ?: '', 'GraphQL')); + if (is_string($query)) { + $query = $this->queryParser->parse($query); + } + Visitor::visit( - $queryAst, + $query, [ 'leave' => [ NodeKind::FIELD => function () use (&$totalFieldCount) { @@ -127,18 +137,16 @@ public function validateFieldCount(string $query): void * This is necessary for performance optimization, as extremely large number of alias in a request * require a substantial amount of resource can affect server performance. * - * @param string $query + * @param DocumentNode $query * @return void * @throws GraphQlInputException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function validateAliasCount(string $query): void + public function validateAliasCount(DocumentNode $query): void { if ($this->maximumAliasConfiguration->isMaximumAliasLimitEnabled()) { $aliasCount = 0; - $query = Parser::parse(new Source($query ?: '', 'GraphQL')); foreach ($query->definitions as $definition) { - if (property_exists($definition, 'selectionSet')) { $aliasCount += $this->countAliasesInSelectionSet($definition->selectionSet); } @@ -157,10 +165,10 @@ public function validateAliasCount(string $query): void /** * Performs counting of aliases in a graphql request * - * @param SelectionSetNode $selectionSet + * @param ?SelectionSetNode $selectionSet * @return int */ - private function countAliasesInSelectionSet(SelectionSetNode $selectionSet): int + private function countAliasesInSelectionSet(?SelectionSetNode $selectionSet): int { if ($selectionSet === null) { return 0; @@ -179,6 +187,7 @@ private function countAliasesInSelectionSet(SelectionSetNode $selectionSet): int } } } + return $aliasCount; } } diff --git a/vendor/magento/framework/GraphQl/Query/QueryParser.php b/vendor/magento/framework/GraphQl/Query/QueryParser.php new file mode 100644 index 0000000000000..0fe7ca1482f6b --- /dev/null +++ b/vendor/magento/framework/GraphQl/Query/QueryParser.php @@ -0,0 +1,39 @@ +parsedQueries[$cacheKey])) { + $this->parsedQueries[$cacheKey] = Parser::parse(new Source($query, 'GraphQL')); + } + return $this->parsedQueries[$cacheKey]; + } +} diff --git a/vendor/magento/framework/GraphQl/Query/QueryProcessor.php b/vendor/magento/framework/GraphQl/Query/QueryProcessor.php index 03265548300ac..460ed71e7f729 100644 --- a/vendor/magento/framework/GraphQl/Query/QueryProcessor.php +++ b/vendor/magento/framework/GraphQl/Query/QueryProcessor.php @@ -9,6 +9,8 @@ use GraphQL\Error\DebugFlag; use GraphQL\GraphQL; +use GraphQL\Language\AST\DocumentNode; +use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Exception\ExceptionFormatter; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; @@ -16,6 +18,7 @@ /** * Wrapper for GraphQl execution of a schema + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class QueryProcessor { @@ -34,27 +37,35 @@ class QueryProcessor */ private $errorHandler; + /** + * @var QueryParser + */ + private $queryParser; + /** * @param ExceptionFormatter $exceptionFormatter * @param QueryComplexityLimiter $queryComplexityLimiter * @param ErrorHandlerInterface $errorHandler + * @param QueryParser|null $queryParser * @SuppressWarnings(PHPMD.LongVariable) */ public function __construct( ExceptionFormatter $exceptionFormatter, QueryComplexityLimiter $queryComplexityLimiter, - ErrorHandlerInterface $errorHandler + ErrorHandlerInterface $errorHandler, + QueryParser $queryParser = null ) { $this->exceptionFormatter = $exceptionFormatter; $this->queryComplexityLimiter = $queryComplexityLimiter; $this->errorHandler = $errorHandler; + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); } /** * Process a GraphQl query according to defined schema * * @param Schema $schema - * @param string $source + * @param DocumentNode|string $source * @param ContextInterface|null $contextValue * @param array|null $variableValues * @param string|null $operationName @@ -63,11 +74,14 @@ public function __construct( */ public function process( Schema $schema, - string $source, + DocumentNode|string $source, ContextInterface $contextValue = null, array $variableValues = null, string $operationName = null ): array { + if (is_string($source)) { + $source = $this->queryParser->parse($source); + } if (!$this->exceptionFormatter->shouldShowDetail()) { $this->queryComplexityLimiter->validateAliasCount($source); $this->queryComplexityLimiter->validateFieldCount($source);