src/Core/Framework/Routing/ApiRequestContextResolver.php line 342

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\Routing;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
  5. use Shopware\Core\Defaults;
  6. use Shopware\Core\Framework\Api\Context\AdminApiSource;
  7. use Shopware\Core\Framework\Api\Context\ContextSource;
  8. use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
  9. use Shopware\Core\Framework\Api\Context\SystemSource;
  10. use Shopware\Core\Framework\Api\Exception\MissingPrivilegeException;
  11. use Shopware\Core\Framework\Api\Util\AccessKeyHelper;
  12. use Shopware\Core\Framework\App\Exception\AppNotFoundException;
  13. use Shopware\Core\Framework\Context;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Pricing\CashRoundingConfig;
  15. use Shopware\Core\Framework\Log\Package;
  16. use Shopware\Core\Framework\Routing\Exception\LanguageNotFoundException;
  17. use Shopware\Core\Framework\Uuid\Uuid;
  18. use Shopware\Core\PlatformRequest;
  19. use Symfony\Component\HttpFoundation\Request;
  20. #[Package('core')]
  21. class ApiRequestContextResolver implements RequestContextResolverInterface
  22. {
  23.     use RouteScopeCheckTrait;
  24.     /**
  25.      * @internal
  26.      */
  27.     public function __construct(private readonly Connection $connection, private readonly RouteScopeRegistry $routeScopeRegistry)
  28.     {
  29.     }
  30.     public function resolve(Request $request): void
  31.     {
  32.         if ($request->attributes->has(PlatformRequest::ATTRIBUTE_CONTEXT_OBJECT)) {
  33.             return;
  34.         }
  35.         if (!$this->isRequestScoped($requestApiContextRouteScopeDependant::class)) {
  36.             return;
  37.         }
  38.         $params $this->getContextParameters($request);
  39.         $languageIdChain $this->getLanguageIdChain($params);
  40.         $rounding $this->getCashRounding($params['currencyId']);
  41.         $context = new Context(
  42.             $this->resolveContextSource($request),
  43.             [],
  44.             $params['currencyId'],
  45.             $languageIdChain,
  46.             $params['versionId'] ?? Defaults::LIVE_VERSION,
  47.             $params['currencyFactory'],
  48.             $params['considerInheritance'],
  49.             CartPrice::TAX_STATE_GROSS,
  50.             $rounding
  51.         );
  52.         if ($request->headers->has(PlatformRequest::HEADER_SKIP_TRIGGER_FLOW)) {
  53.             $skipTriggerFlow filter_var($request->headers->get(PlatformRequest::HEADER_SKIP_TRIGGER_FLOW'false'), \FILTER_VALIDATE_BOOLEAN);
  54.             if ($skipTriggerFlow) {
  55.                 $context->addState(Context::SKIP_TRIGGER_FLOW);
  56.             }
  57.         }
  58.         $request->attributes->set(PlatformRequest::ATTRIBUTE_CONTEXT_OBJECT$context);
  59.     }
  60.     protected function getScopeRegistry(): RouteScopeRegistry
  61.     {
  62.         return $this->routeScopeRegistry;
  63.     }
  64.     /**
  65.      * @return array{currencyId: string, languageId: string, systemFallbackLanguageId: string, currencyFactory: float, currencyPrecision: int, versionId: ?string, considerInheritance: bool}
  66.      */
  67.     private function getContextParameters(Request $request): array
  68.     {
  69.         $params = [
  70.             'currencyId' => Defaults::CURRENCY,
  71.             'languageId' => Defaults::LANGUAGE_SYSTEM,
  72.             'systemFallbackLanguageId' => Defaults::LANGUAGE_SYSTEM,
  73.             'currencyFactory' => 1.0,
  74.             'currencyPrecision' => 2,
  75.             'versionId' => $request->headers->get(PlatformRequest::HEADER_VERSION_ID),
  76.             'considerInheritance' => false,
  77.         ];
  78.         $runtimeParams $this->getRuntimeParameters($request);
  79.         /** @var array{currencyId: string, languageId: string, systemFallbackLanguageId: string, currencyFactory: float, currencyPrecision: int, versionId: ?string, considerInheritance: bool} $params */
  80.         $params array_replace_recursive($params$runtimeParams);
  81.         return $params;
  82.     }
  83.     private function getRuntimeParameters(Request $request): array
  84.     {
  85.         $parameters = [];
  86.         if ($request->headers->has(PlatformRequest::HEADER_LANGUAGE_ID)) {
  87.             $langHeader $request->headers->get(PlatformRequest::HEADER_LANGUAGE_ID);
  88.             if ($langHeader !== null) {
  89.                 $parameters['languageId'] = $langHeader;
  90.             }
  91.         }
  92.         if ($request->headers->has(PlatformRequest::HEADER_CURRENCY_ID)) {
  93.             $currencyHeader $request->headers->get(PlatformRequest::HEADER_CURRENCY_ID);
  94.             if ($currencyHeader !== null) {
  95.                 $parameters['currencyId'] = $currencyHeader;
  96.             }
  97.         }
  98.         if ($request->headers->has(PlatformRequest::HEADER_INHERITANCE)) {
  99.             $parameters['considerInheritance'] = true;
  100.         }
  101.         return $parameters;
  102.     }
  103.     private function resolveContextSource(Request $request): ContextSource
  104.     {
  105.         if ($userId $request->attributes->get(PlatformRequest::ATTRIBUTE_OAUTH_USER_ID)) {
  106.             $appIntegrationId $request->headers->get(PlatformRequest::HEADER_APP_INTEGRATION_ID);
  107.             // The app integration id header is only to be used by a privileged user
  108.             if ($this->userAppIntegrationHeaderPrivileged($userId$appIntegrationId)) {
  109.                 $userId null;
  110.             } else {
  111.                 $appIntegrationId null;
  112.             }
  113.             return $this->getAdminApiSource($userId$appIntegrationId);
  114.         }
  115.         if (!$request->attributes->has(PlatformRequest::ATTRIBUTE_OAUTH_ACCESS_TOKEN_ID)) {
  116.             return new SystemSource();
  117.         }
  118.         $clientId $request->attributes->get(PlatformRequest::ATTRIBUTE_OAUTH_CLIENT_ID);
  119.         $keyOrigin AccessKeyHelper::getOrigin($clientId);
  120.         if ($keyOrigin === 'user') {
  121.             $userId $this->getUserIdByAccessKey($clientId);
  122.             return $this->getAdminApiSource($userId);
  123.         }
  124.         if ($keyOrigin === 'integration') {
  125.             $integrationId $this->getIntegrationIdByAccessKey($clientId);
  126.             return $this->getAdminApiSource(null$integrationId);
  127.         }
  128.         if ($keyOrigin === 'sales-channel') {
  129.             $salesChannelId $this->getSalesChannelIdByAccessKey($clientId);
  130.             return new SalesChannelApiSource($salesChannelId);
  131.         }
  132.         return new SystemSource();
  133.     }
  134.     /**
  135.      * @param array{languageId: string, systemFallbackLanguageId: string} $params
  136.      *
  137.      * @return non-empty-array<string>
  138.      */
  139.     private function getLanguageIdChain(array $params): array
  140.     {
  141.         $chain = [$params['languageId']];
  142.         if ($chain[0] === Defaults::LANGUAGE_SYSTEM) {
  143.             return $chain// no query needed
  144.         }
  145.         // `Context` ignores nulls and duplicates
  146.         $chain[] = $this->getParentLanguageId($chain[0]);
  147.         $chain[] = $params['systemFallbackLanguageId'];
  148.         /** @var non-empty-array<string> $filtered */
  149.         $filtered array_filter($chain);
  150.         return $filtered;
  151.     }
  152.     private function getParentLanguageId(?string $languageId): ?string
  153.     {
  154.         if ($languageId === null || !Uuid::isValid($languageId)) {
  155.             throw new LanguageNotFoundException($languageId);
  156.         }
  157.         $data $this->connection->createQueryBuilder()
  158.             ->select(['LOWER(HEX(language.parent_id))'])
  159.             ->from('language')
  160.             ->where('language.id = :id')
  161.             ->setParameter('id'Uuid::fromHexToBytes($languageId))
  162.             ->executeQuery()
  163.             ->fetchFirstColumn();
  164.         if (empty($data)) {
  165.             throw new LanguageNotFoundException($languageId);
  166.         }
  167.         return $data[0];
  168.     }
  169.     private function getUserIdByAccessKey(string $clientId): string
  170.     {
  171.         $id $this->connection->createQueryBuilder()
  172.             ->select(['user_id'])
  173.             ->from('user_access_key')
  174.             ->where('access_key = :accessKey')
  175.             ->setParameter('accessKey'$clientId)
  176.             ->executeQuery()
  177.             ->fetchOne();
  178.         return Uuid::fromBytesToHex($id);
  179.     }
  180.     private function getSalesChannelIdByAccessKey(string $clientId): string
  181.     {
  182.         $id $this->connection->createQueryBuilder()
  183.             ->select(['id'])
  184.             ->from('sales_channel')
  185.             ->where('access_key = :accessKey')
  186.             ->setParameter('accessKey'$clientId)
  187.             ->executeQuery()
  188.             ->fetchOne();
  189.         return Uuid::fromBytesToHex($id);
  190.     }
  191.     private function getIntegrationIdByAccessKey(string $clientId): string
  192.     {
  193.         $id $this->connection->createQueryBuilder()
  194.             ->select(['id'])
  195.             ->from('integration')
  196.             ->where('access_key = :accessKey')
  197.             ->setParameter('accessKey'$clientId)
  198.             ->executeQuery()
  199.             ->fetchOne();
  200.         return Uuid::fromBytesToHex($id);
  201.     }
  202.     private function getAdminApiSource(?string $userId, ?string $integrationId null): AdminApiSource
  203.     {
  204.         $source = new AdminApiSource($userId$integrationId);
  205.         // Use the permissions associated to that app, if the request is made by an integration associated to an app
  206.         $appPermissions $this->fetchPermissionsIntegrationByApp($integrationId);
  207.         if ($appPermissions !== null) {
  208.             $source->setIsAdmin(false);
  209.             $source->setPermissions($appPermissions);
  210.             return $source;
  211.         }
  212.         if ($userId !== null) {
  213.             $source->setPermissions($this->fetchPermissions($userId));
  214.             $source->setIsAdmin($this->isAdmin($userId));
  215.             return $source;
  216.         }
  217.         if ($integrationId !== null) {
  218.             $source->setIsAdmin($this->isAdminIntegration($integrationId));
  219.             $source->setPermissions($this->fetchIntegrationPermissions($integrationId));
  220.             return $source;
  221.         }
  222.         return $source;
  223.     }
  224.     private function isAdmin(string $userId): bool
  225.     {
  226.         return (bool) $this->connection->fetchOne(
  227.             'SELECT admin FROM `user` WHERE id = :id',
  228.             ['id' => Uuid::fromHexToBytes($userId)]
  229.         );
  230.     }
  231.     private function isAdminIntegration(string $integrationId): bool
  232.     {
  233.         return (bool) $this->connection->fetchOne(
  234.             'SELECT admin FROM `integration` WHERE id = :id',
  235.             ['id' => Uuid::fromHexToBytes($integrationId)]
  236.         );
  237.     }
  238.     private function fetchPermissions(string $userId): array
  239.     {
  240.         $permissions $this->connection->createQueryBuilder()
  241.             ->select(['role.privileges'])
  242.             ->from('acl_user_role''mapping')
  243.             ->innerJoin('mapping''acl_role''role''mapping.acl_role_id = role.id')
  244.             ->where('mapping.user_id = :userId')
  245.             ->setParameter('userId'Uuid::fromHexToBytes($userId))
  246.             ->executeQuery()
  247.             ->fetchFirstColumn();
  248.         $list = [];
  249.         foreach ($permissions as $privileges) {
  250.             $privileges json_decode((string) $privilegestrue512\JSON_THROW_ON_ERROR);
  251.             $list array_merge($list$privileges);
  252.         }
  253.         return array_unique(array_filter($list));
  254.     }
  255.     private function getCashRounding(string $currencyId): CashRoundingConfig
  256.     {
  257.         $rounding $this->connection->fetchAssociative(
  258.             'SELECT item_rounding FROM currency WHERE id = :id',
  259.             ['id' => Uuid::fromHexToBytes($currencyId)]
  260.         );
  261.         if ($rounding === false) {
  262.             throw new \RuntimeException(sprintf('No cash rounding for currency "%s" found'$currencyId));
  263.         }
  264.         $rounding json_decode((string) $rounding['item_rounding'], true512\JSON_THROW_ON_ERROR);
  265.         return new CashRoundingConfig(
  266.             (int) $rounding['decimals'],
  267.             (float) $rounding['interval'],
  268.             (bool) $rounding['roundForNet']
  269.         );
  270.     }
  271.     private function fetchPermissionsIntegrationByApp(?string $integrationId): ?array
  272.     {
  273.         if (!$integrationId) {
  274.             return null;
  275.         }
  276.         $privileges $this->connection->fetchOne('
  277.             SELECT `acl_role`.`privileges`
  278.             FROM `acl_role`
  279.             INNER JOIN `app` ON `app`.`acl_role_id` = `acl_role`.`id`
  280.             WHERE `app`.`integration_id` = :integrationId
  281.         ', ['integrationId' => Uuid::fromHexToBytes($integrationId)]);
  282.         if ($privileges === false) {
  283.             return null;
  284.         }
  285.         return json_decode((string) $privilegestrue512\JSON_THROW_ON_ERROR);
  286.     }
  287.     private function fetchIntegrationPermissions(string $integrationId): array
  288.     {
  289.         $permissions $this->connection->createQueryBuilder()
  290.             ->select(['role.privileges'])
  291.             ->from('integration_role''mapping')
  292.             ->innerJoin('mapping''acl_role''role''mapping.acl_role_id = role.id')
  293.             ->where('mapping.integration_id = :integrationId')
  294.             ->setParameter('integrationId'Uuid::fromHexToBytes($integrationId))
  295.             ->executeQuery()
  296.             ->fetchFirstColumn();
  297.         $list = [];
  298.         foreach ($permissions as $privileges) {
  299.             $privileges json_decode((string) $privilegestrue512\JSON_THROW_ON_ERROR);
  300.             $list array_merge($list$privileges);
  301.         }
  302.         return array_unique(array_filter($list));
  303.     }
  304.     private function fetchAppNameByIntegrationId(string $integrationId): ?string
  305.     {
  306.         $name $this->connection->createQueryBuilder()
  307.             ->select(['app.name'])
  308.             ->from('app''app')
  309.             ->innerJoin('app''integration''integration''integration.id = app.integration_id')
  310.             ->where('integration.id = :integrationId')
  311.             ->andWhere('app.active = 1')
  312.             ->setParameter('integrationId'Uuid::fromHexToBytes($integrationId))
  313.             ->executeQuery()
  314.             ->fetchOne();
  315.         if ($name === false) {
  316.             return null;
  317.         }
  318.         return $name;
  319.     }
  320.     /**
  321.      * @throws MissingPrivilegeException
  322.      * @throws AppNotFoundException
  323.      */
  324.     private function userAppIntegrationHeaderPrivileged(string $userId, ?string $appIntegrationId): bool
  325.     {
  326.         if ($appIntegrationId === null) {
  327.             return false;
  328.         }
  329.         $appName $this->fetchAppNameByIntegrationId($appIntegrationId);
  330.         if ($appName === null) {
  331.             throw new AppNotFoundException($appIntegrationId);
  332.         }
  333.         $isAdmin $this->isAdmin($userId);
  334.         if ($isAdmin) {
  335.             return true;
  336.         }
  337.         $permissions $this->fetchPermissions($userId);
  338.         $allAppsPrivileged \in_array('app.all'$permissionstrue);
  339.         $appPrivilegeName \sprintf('app.%s'$appName);
  340.         $specificAppPrivileged \in_array($appPrivilegeName$permissionstrue);
  341.         if (!($specificAppPrivileged || $allAppsPrivileged)) {
  342.             throw new MissingPrivilegeException([$appPrivilegeName]);
  343.         }
  344.         return true;
  345.     }
  346. }