src/Administration/Controller/AdministrationController.php line 75

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Administration\Controller;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Administration\Events\PreResetExcludedSearchTermEvent;
  5. use Shopware\Administration\Framework\Routing\KnownIps\KnownIpsCollectorInterface;
  6. use Shopware\Administration\Snippet\SnippetFinderInterface;
  7. use Shopware\Core\Defaults;
  8. use Shopware\Core\DevOps\Environment\EnvironmentHelper;
  9. use Shopware\Core\Framework\Adapter\Twig\TemplateFinder;
  10. use Shopware\Core\Framework\Context;
  11. use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
  12. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\AllowHtml;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  19. use Shopware\Core\Framework\Feature;
  20. use Shopware\Core\Framework\Log\Package;
  21. use Shopware\Core\Framework\Routing\Exception\InvalidRequestParameterException;
  22. use Shopware\Core\Framework\Routing\Exception\LanguageNotFoundException;
  23. use Shopware\Core\Framework\Store\Services\FirstRunWizardService;
  24. use Shopware\Core\Framework\Util\HtmlSanitizer;
  25. use Shopware\Core\Framework\Uuid\Uuid;
  26. use Shopware\Core\Framework\Validation\Exception\ConstraintViolationException;
  27. use Shopware\Core\PlatformRequest;
  28. use Shopware\Core\System\Currency\CurrencyEntity;
  29. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  30. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  31. use Symfony\Component\HttpFoundation\JsonResponse;
  32. use Symfony\Component\HttpFoundation\Request;
  33. use Symfony\Component\HttpFoundation\Response;
  34. use Symfony\Component\Routing\Annotation\Route;
  35. use Symfony\Component\Validator\ConstraintViolation;
  36. use Symfony\Component\Validator\ConstraintViolationList;
  37. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  38. use function version_compare;
  39. #[Route(defaults: ['_routeScope' => ['administration']])]
  40. #[Package('administration')]
  41. class AdministrationController extends AbstractController
  42. {
  43.     private readonly bool $esAdministrationEnabled;
  44.     /**
  45.      * @internal
  46.      *
  47.      * @param array<int, int> $supportedApiVersions
  48.      */
  49.     public function __construct(
  50.         private readonly TemplateFinder $finder,
  51.         private readonly FirstRunWizardService $firstRunWizardService,
  52.         private readonly SnippetFinderInterface $snippetFinder,
  53.         private readonly array $supportedApiVersions,
  54.         private readonly KnownIpsCollectorInterface $knownIpsCollector,
  55.         private readonly Connection $connection,
  56.         private readonly EventDispatcherInterface $eventDispatcher,
  57.         private readonly string $shopwareCoreDir,
  58.         private readonly EntityRepository $customerRepo,
  59.         private readonly EntityRepository $currencyRepository,
  60.         private readonly HtmlSanitizer $htmlSanitizer,
  61.         private readonly DefinitionInstanceRegistry $definitionInstanceRegistry,
  62.         ParameterBagInterface $params
  63.     ) {
  64.         // param is only available if the elasticsearch bundle is enabled
  65.         $this->esAdministrationEnabled $params->has('elasticsearch.administration.enabled')
  66.             ? $params->get('elasticsearch.administration.enabled')
  67.             : false;
  68.     }
  69.     #[Route(path'/%shopware_administration.path_name%'name'administration.index'defaults: ['auth_required' => false], methods: ['GET'])]
  70.     public function index(Request $requestContext $context): Response
  71.     {
  72.         $template $this->finder->find('@Administration/administration/index.html.twig');
  73.         /** @var CurrencyEntity $defaultCurrency */
  74.         $defaultCurrency $this->currencyRepository->search(new Criteria([Defaults::CURRENCY]), $context)->first();
  75.         return $this->render($template, [
  76.             'features' => Feature::getAll(),
  77.             'systemLanguageId' => Defaults::LANGUAGE_SYSTEM,
  78.             'defaultLanguageIds' => [Defaults::LANGUAGE_SYSTEM],
  79.             'systemCurrencyId' => Defaults::CURRENCY,
  80.             'disableExtensions' => EnvironmentHelper::getVariable('DISABLE_EXTENSIONS'false),
  81.             'systemCurrencyISOCode' => $defaultCurrency->getIsoCode(),
  82.             'liveVersionId' => Defaults::LIVE_VERSION,
  83.             'firstRunWizard' => $this->firstRunWizardService->frwShouldRun(),
  84.             'apiVersion' => $this->getLatestApiVersion(),
  85.             'cspNonce' => $request->attributes->get(PlatformRequest::ATTRIBUTE_CSP_NONCE),
  86.             'adminEsEnable' => $this->esAdministrationEnabled,
  87.         ]);
  88.     }
  89.     #[Route(path'/api/_admin/snippets'name'api.admin.snippets'methods: ['GET'])]
  90.     public function snippets(Request $request): Response
  91.     {
  92.         $snippets = [];
  93.         $locale $request->query->get('locale''en-GB');
  94.         $snippets[$locale] = $this->snippetFinder->findSnippets((string) $locale);
  95.         if ($locale !== 'en-GB') {
  96.             $snippets['en-GB'] = $this->snippetFinder->findSnippets('en-GB');
  97.         }
  98.         return new JsonResponse($snippets);
  99.     }
  100.     #[Route(path'/api/_admin/known-ips'name'api.admin.known-ips'methods: ['GET'])]
  101.     public function knownIps(Request $request): Response
  102.     {
  103.         $ips = [];
  104.         foreach ($this->knownIpsCollector->collectIps($request) as $ip => $name) {
  105.             $ips[] = [
  106.                 'name' => $name,
  107.                 'value' => $ip,
  108.             ];
  109.         }
  110.         return new JsonResponse(['ips' => $ips]);
  111.     }
  112.     #[Route(path'/api/_admin/reset-excluded-search-term'name'api.admin.reset-excluded-search-term'defaults: ['_acl' => ['system_config:update''system_config:create''system_config:delete']], methods: ['POST'])]
  113.     public function resetExcludedSearchTerm(Context $context): JsonResponse
  114.     {
  115.         $searchConfigId $this->connection->fetchOne('SELECT id FROM product_search_config WHERE language_id = :language_id', ['language_id' => Uuid::fromHexToBytes($context->getLanguageId())]);
  116.         if ($searchConfigId === false) {
  117.             throw new LanguageNotFoundException($context->getLanguageId());
  118.         }
  119.         $deLanguageId $this->fetchLanguageIdByName('de-DE'$this->connection);
  120.         $enLanguageId $this->fetchLanguageIdByName('en-GB'$this->connection);
  121.         switch ($context->getLanguageId()) {
  122.             case $deLanguageId:
  123.                 $defaultExcludedTerm = require $this->shopwareCoreDir '/Migration/Fixtures/stopwords/de.php';
  124.                 break;
  125.             case $enLanguageId:
  126.                 $defaultExcludedTerm = require $this->shopwareCoreDir '/Migration/Fixtures/stopwords/en.php';
  127.                 break;
  128.             default:
  129.                 /** @var PreResetExcludedSearchTermEvent $preResetExcludedSearchTermEvent */
  130.                 $preResetExcludedSearchTermEvent $this->eventDispatcher->dispatch(new PreResetExcludedSearchTermEvent($searchConfigId, [], $context));
  131.                 $defaultExcludedTerm $preResetExcludedSearchTermEvent->getExcludedTerms();
  132.         }
  133.         $this->connection->executeStatement(
  134.             'UPDATE `product_search_config` SET `excluded_terms` = :excludedTerms WHERE `id` = :id',
  135.             [
  136.                 'excludedTerms' => json_encode($defaultExcludedTerm\JSON_THROW_ON_ERROR),
  137.                 'id' => $searchConfigId,
  138.             ]
  139.         );
  140.         return new JsonResponse([
  141.             'success' => true,
  142.         ]);
  143.     }
  144.     #[Route(path'/api/_admin/check-customer-email-valid'name'api.admin.check-customer-email-valid'methods: ['POST'])]
  145.     public function checkCustomerEmailValid(Request $requestContext $context): JsonResponse
  146.     {
  147.         $params = [];
  148.         if (!$request->request->has('email')) {
  149.             throw new \InvalidArgumentException('Parameter "email" is missing.');
  150.         }
  151.         $email = (string) $request->request->get('email');
  152.         $boundSalesChannelId $request->request->get('bound_sales_channel_id');
  153.         if ($boundSalesChannelId !== null && !\is_string($boundSalesChannelId)) {
  154.             throw new InvalidRequestParameterException('bound_sales_channel_id');
  155.         }
  156.         if ($this->isEmailValid((string) $request->request->get('id'), $email$context$boundSalesChannelId)) {
  157.             return new JsonResponse(
  158.                 ['isValid' => true]
  159.             );
  160.         }
  161.         $message 'The email address {{ email }} is already in use';
  162.         $params['{{ email }}'] = $email;
  163.         if ($boundSalesChannelId !== null) {
  164.             $message .= ' in the sales channel {{ salesChannelId }}';
  165.             $params['{{ salesChannelId }}'] = $boundSalesChannelId;
  166.         }
  167.         $violations = new ConstraintViolationList();
  168.         $violations->add(new ConstraintViolation(
  169.             str_replace(array_keys($params), array_values($params), $message),
  170.             $message,
  171.             $params,
  172.             null,
  173.             null,
  174.             $email,
  175.             null,
  176.             '79d30fe0-febf-421e-ac9b-1bfd5c9007f7'
  177.         ));
  178.         throw new ConstraintViolationException($violations$request->request->all());
  179.     }
  180.     #[Route(path'/api/_admin/sanitize-html'name'api.admin.sanitize-html'methods: ['POST'])]
  181.     public function sanitizeHtml(Request $requestContext $context): JsonResponse
  182.     {
  183.         if (!$request->request->has('html')) {
  184.             throw new \InvalidArgumentException('Parameter "html" is missing.');
  185.         }
  186.         $html = (string) $request->request->get('html');
  187.         $field = (string) $request->request->get('field');
  188.         if ($field === '') {
  189.             return new JsonResponse(
  190.                 ['preview' => $this->htmlSanitizer->sanitize($html)]
  191.             );
  192.         }
  193.         [$entityName$propertyName] = explode('.'$field);
  194.         $property $this->definitionInstanceRegistry->getByEntityName($entityName)->getField($propertyName);
  195.         if ($property === null) {
  196.             throw new \InvalidArgumentException('Invalid field property provided.');
  197.         }
  198.         $flag $property->getFlag(AllowHtml::class);
  199.         if ($flag === null) {
  200.             return new JsonResponse(
  201.                 ['preview' => strip_tags($html)]
  202.             );
  203.         }
  204.         if ($flag instanceof AllowHtml && !$flag->isSanitized()) {
  205.             return new JsonResponse(
  206.                 ['preview' => $html]
  207.             );
  208.         }
  209.         return new JsonResponse(
  210.             ['preview' => $this->htmlSanitizer->sanitize($html, [], false$field)]
  211.         );
  212.     }
  213.     private function fetchLanguageIdByName(string $isoCodeConnection $connection): ?string
  214.     {
  215.         $languageId $connection->fetchOne(
  216.             '
  217.             SELECT `language`.id FROM `language`
  218.             INNER JOIN locale ON language.translation_code_id = locale.id
  219.             WHERE `code` = :code',
  220.             ['code' => $isoCode]
  221.         );
  222.         return $languageId === false null Uuid::fromBytesToHex($languageId);
  223.     }
  224.     private function getLatestApiVersion(): ?int
  225.     {
  226.         $sortedSupportedApiVersions array_values($this->supportedApiVersions);
  227.         usort($sortedSupportedApiVersions, fn (int $version1int $version2) => version_compare((string) $version1, (string) $version2));
  228.         return array_pop($sortedSupportedApiVersions);
  229.     }
  230.     /**
  231.      * @throws InconsistentCriteriaIdsException
  232.      */
  233.     private function isEmailValid(string $customerIdstring $emailContext $context, ?string $boundSalesChannelId): bool
  234.     {
  235.         $criteria = new Criteria();
  236.         $criteria->addFilter(new EqualsFilter('email'$email));
  237.         $criteria->addFilter(new EqualsFilter('guest'false));
  238.         $criteria->addFilter(new NotFilter(
  239.             NotFilter::CONNECTION_AND,
  240.             [new EqualsFilter('id'$customerId)]
  241.         ));
  242.         $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_OR, [
  243.             new EqualsFilter('boundSalesChannelId'null),
  244.             new EqualsFilter('boundSalesChannelId'$boundSalesChannelId),
  245.         ]));
  246.         return $this->customerRepo->searchIds($criteria$context)->getTotal() === 0;
  247.     }
  248. }