vendor/shopware/administration/Controller/AdministrationController.php line 121

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