diff --git a/composer.json b/composer.json index 57b51ec..25a495e 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=7.3", "spryker-shop/quick-order-page-extension": "^1.1.0", "spryker-shop/shop-application": "^1.0.0", - "spryker-shop/shop-ui": "^1.28.1", + "spryker-shop/shop-ui": "^1.41.0", "spryker/application": "^3.0.0", "spryker/cart": "^4.0.0 || ^5.0.0 || ^7.0.0", "spryker/kernel": "^3.52.0", diff --git a/src/SprykerShop/Yves/QuickOrderPage/Controller/QuickOrderController.php b/src/SprykerShop/Yves/QuickOrderPage/Controller/QuickOrderController.php index 0c96694..aee84c5 100644 --- a/src/SprykerShop/Yves/QuickOrderPage/Controller/QuickOrderController.php +++ b/src/SprykerShop/Yves/QuickOrderPage/Controller/QuickOrderController.php @@ -22,6 +22,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\Security\Csrf\CsrfToken; /** * @method \SprykerShop\Yves\QuickOrderPage\QuickOrderPageFactory getFactory() @@ -33,10 +34,12 @@ class QuickOrderController extends AbstractController public const PARAM_ROW_INDEX = 'row-index'; public const PARAM_QUICK_ORDER_FORM = 'quick_order_form'; protected const PARAM_QUICK_ORDER_FILE_TYPE = 'file-type'; + protected const PARAM_FORM_TOKEN = '_token'; protected const MESSAGE_CLEAR_ALL_ROWS_SUCCESS = 'quick-order.message.success.the-form-items-have-been-successfully-cleared'; protected const ERROR_MESSAGE_QUANTITY_INVALID = 'quick-order.errors.quantity-invalid'; protected const MESSAGE_TYPE_WARNING = 'warning'; protected const MESSAGE_PERMISSION_FAILED = 'global.permission.failed'; + protected const MESSAGE_FORM_INVALID_CSRF = 'form.csrf.error.text'; /** * @uses \SprykerShop\Yves\CartPage\Plugin\Router\CartPageRouteProviderPlugin::ROUTE_NAME_CART @@ -48,6 +51,11 @@ class QuickOrderController extends AbstractController */ protected const ROUTE_NAME_CHECKOUT_INDEX = 'checkout-index'; + protected const FLASH_MESSAGE_LIST_TEMPLATE_PATH = '@ShopUi/components/organisms/flash-message-list/flash-message-list.twig'; + + protected const KEY_CODE = 'code'; + protected const KEY_MESSAGES = 'messages'; + /** * @param \Symfony\Component\HttpFoundation\Request $request * @@ -86,15 +94,15 @@ protected function executeQuickOrderFormSubmitAction(Request $request) ->getQuickOrderForm($quickOrderTransfer) ->handleRequest($request); - if ($quickOrderForm->isSubmitted() && $quickOrderForm->isValid()) { - $response = $this->processQuickOrderForm($quickOrderForm, $request); - - if ($response !== null) { - return $response; + if (!$quickOrderForm->isSubmitted() || !$quickOrderForm->isValid()) { + foreach ($quickOrderForm->getErrors(true) as $formError) { + $this->addErrorMessage($formError->getMessage()); } + + return []; } - return []; + return $this->processQuickOrderForm($quickOrderForm, $request) ?? []; } /** @@ -333,6 +341,10 @@ public function deleteRowAction(Request $request) $viewData = $this->executeDeleteRowAction($request); + if (isset($viewData[static::KEY_CODE])) { + return $this->jsonResponse($viewData); + } + return $this->view( $viewData, $this->getFactory()->getQuickOrderPageWidgetPlugins(), @@ -351,8 +363,15 @@ protected function executeDeleteRowAction(Request $request): array { $rowIndex = $request->get(static::PARAM_ROW_INDEX); $formData = $request->get(static::PARAM_QUICK_ORDER_FORM); - $formDataItems = $formData['items'] ?? []; + if (!$this->isQuickOrderFormCsrfTokenValid($formData)) { + return $this->createAjaxErrorResponse( + Response::HTTP_BAD_REQUEST, + [static::MESSAGE_FORM_INVALID_CSRF] + ); + } + + $formDataItems = $formData['items'] ?? []; if (!isset($formDataItems[$rowIndex])) { throw new HttpException(Response::HTTP_BAD_REQUEST, '"row-index" is out of the bound.'); } @@ -672,4 +691,51 @@ protected function transformProductsViewData(array $productConcreteTransfers): a ->createViewDataTransformer() ->transformProductData($productConcreteTransfers, $this->getFactory()->getQuickOrderFormColumnPlugins()); } + + /** + * @param array|null $quickOrderFormData + * + * @return bool + */ + protected function isQuickOrderFormCsrfTokenValid(?array $quickOrderFormData): bool + { + if (!$quickOrderFormData || !isset($quickOrderFormData[static::PARAM_FORM_TOKEN])) { + return false; + } + + $csrfToken = $this->createCsrfToken(static::PARAM_QUICK_ORDER_FORM, $quickOrderFormData[static::PARAM_FORM_TOKEN]); + + return $this->getFactory()->getCsrfTokenManager()->isTokenValid($csrfToken); + } + + /** + * @param string $tokenId + * @param string $value + * + * @return \Symfony\Component\Security\Csrf\CsrfToken + */ + protected function createCsrfToken(string $tokenId, string $value): CsrfToken + { + return new CsrfToken($tokenId, $value); + } + + /** + * @param int $code + * @param string[] $messages + * + * @return array + */ + protected function createAjaxErrorResponse(int $code, array $messages): array + { + foreach ($messages as $message) { + $this->addErrorMessage($message); + } + + $flashMessageListHtml = $this->renderView(static::FLASH_MESSAGE_LIST_TEMPLATE_PATH)->getContent(); + + return [ + static::KEY_CODE => $code, + static::KEY_MESSAGES => $flashMessageListHtml, + ]; + } } diff --git a/src/SprykerShop/Yves/QuickOrderPage/QuickOrderPageDependencyProvider.php b/src/SprykerShop/Yves/QuickOrderPage/QuickOrderPageDependencyProvider.php index 6050fa2..4bb7d34 100644 --- a/src/SprykerShop/Yves/QuickOrderPage/QuickOrderPageDependencyProvider.php +++ b/src/SprykerShop/Yves/QuickOrderPage/QuickOrderPageDependencyProvider.php @@ -44,8 +44,14 @@ class QuickOrderPageDependencyProvider extends AbstractBundleDependencyProvider public const PLUGINS_QUICK_ORDER_UPLOADED_FILE_PARSER = 'PLUGINS_QUICK_ORDER_UPLOADED_FILE_PARSER'; public const PLUGINS_QUICK_ORDER_UPLOADED_FILE_VALIDATOR = 'PLUGINS_QUICK_ORDER_UPLOADED_FILE_VALIDATOR'; public const PLUGINS_QUICK_ORDER_FILE_TEMPLATE = 'PLUGINS_QUICK_ORDER_FILE_TEMPLATE'; + public const SERVICE_UTIL_CSV = 'SERVICE_UTIL_CSV'; + /** + * @uses \Spryker\Yves\Form\Plugin\Application\FormApplicationPlugin::SERVICE_FORM_CSRF_PROVIDER + */ + public const SERVICE_FORM_CSRF_PROVIDER = 'form.csrf_provider'; + /** * @uses \Spryker\Yves\Http\Plugin\Application\HttpApplicationPlugin::SERVICE_REQUEST_STACK */ @@ -66,6 +72,7 @@ public function provideDependencies(Container $container): Container $container = $this->addQuickOrderPageWidgetPlugins($container); $container = $this->addZedRequestClient($container); $container = $this->addQuickOrderUtilCsvService($container); + $container = $this->addCsrfProviderService($container); $container = $this->addQuickOrderItemTransferExpanderPlugins($container); $container = $this->addQuickOrderFormHandlerStrategyPlugins($container); $container = $this->addQuickOrderFormAdditionalDataColumnProviderPlugins($container); @@ -128,6 +135,20 @@ protected function addQuickOrderUtilCsvService(Container $container): Container return $container; } + /** + * @param \Spryker\Yves\Kernel\Container $container + * + * @return \Spryker\Yves\Kernel\Container + */ + protected function addCsrfProviderService(Container $container): Container + { + $container->set(static::SERVICE_FORM_CSRF_PROVIDER, function (Container $container) { + return $container->getApplicationService(static::SERVICE_FORM_CSRF_PROVIDER); + }); + + return $container; + } + /** * @param \Spryker\Yves\Kernel\Container $container * diff --git a/src/SprykerShop/Yves/QuickOrderPage/QuickOrderPageFactory.php b/src/SprykerShop/Yves/QuickOrderPage/QuickOrderPageFactory.php index 3dc6aa3..0c64028 100644 --- a/src/SprykerShop/Yves/QuickOrderPage/QuickOrderPageFactory.php +++ b/src/SprykerShop/Yves/QuickOrderPage/QuickOrderPageFactory.php @@ -53,6 +53,7 @@ use SprykerShop\Yves\QuickOrderPage\ViewDataTransformer\ViewDataTransformerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Validator\Constraints\NotBlank; /** @@ -424,4 +425,12 @@ public function getModuleConfig(): QuickOrderPageConfig { return $this->getConfig(); } + + /** + * @return \Symfony\Component\Security\Csrf\CsrfTokenManagerInterface + */ + public function getCsrfTokenManager(): CsrfTokenManagerInterface + { + return $this->getProvidedDependency(QuickOrderPageDependencyProvider::SERVICE_FORM_CSRF_PROVIDER); + } } diff --git a/src/SprykerShop/Yves/QuickOrderPage/Theme/default/components/molecules/quick-order-form/quick-order-form.ts b/src/SprykerShop/Yves/QuickOrderPage/Theme/default/components/molecules/quick-order-form/quick-order-form.ts index 646c7bf..bf4199e 100644 --- a/src/SprykerShop/Yves/QuickOrderPage/Theme/default/components/molecules/quick-order-form/quick-order-form.ts +++ b/src/SprykerShop/Yves/QuickOrderPage/Theme/default/components/molecules/quick-order-form/quick-order-form.ts @@ -1,6 +1,7 @@ import Component from 'ShopUi/models/component'; import AjaxProvider from 'ShopUi/components/molecules/ajax-provider/ajax-provider'; import { mount } from 'ShopUi/app'; +import { EVENT_UPDATE_DYNAMIC_MESSAGES } from 'ShopUi/components/organisms/dynamic-notification-area/dynamic-notification-area'; export default class QuickOrderForm extends Component { /** @@ -97,7 +98,40 @@ export default class QuickOrderForm extends Component { 'row-index': rowIndex }); const response = await this.removeRowAjaxProvider.fetch(data); + const parsedResponse = this.parseResponse(response); + if (typeof parsedResponse !== 'string') { + this.showFlashMessage(parsedResponse); + + return; + } + + this.updateTableHtml(response); + } + + protected parseResponse(response: string): string|object { + try { + return JSON.parse(response); + } catch { + return response; + } + } + + protected hasMessages(response: object): response is { messages: string } { + return 'messages' in response; + } + + protected async showFlashMessage(response: object): Promise { + if (!this.hasMessages(response)) { + return; + } + const dynamicNotificationCustomEvent = new CustomEvent(EVENT_UPDATE_DYNAMIC_MESSAGES, { + detail: response.messages, + }); + document.dispatchEvent(dynamicNotificationCustomEvent); + } + + protected async updateTableHtml(response: string): Promise { this.rows.innerHTML = response; await mount(); this.registerRemoveRowTriggers();