mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-14 09:02:12 +00:00
[Shop] Fix security issues in cart actions
This commit is contained in:
parent
442fc57e78
commit
bd7744d5c3
20 changed files with 128 additions and 71 deletions
|
|
@ -9,21 +9,21 @@ Feature: Removing cart item from cart
|
|||
And the store has a product "T-shirt banana" priced at "$12.54"
|
||||
And I added product "T-shirt banana" to the cart
|
||||
|
||||
@ui
|
||||
@ui @javascript
|
||||
Scenario: Removing cart item
|
||||
When I see the summary of my cart
|
||||
And I remove product "T-shirt banana" from the cart
|
||||
Then my cart should be empty
|
||||
And my cart's total should be "$0.00"
|
||||
|
||||
@ui
|
||||
@ui @javascript
|
||||
Scenario: Removing cart item when the store has defined default shipping method
|
||||
Given the store has "UPS" shipping method with "$20.00" fee
|
||||
When I remove product "T-shirt banana" from the cart
|
||||
Then my cart should be empty
|
||||
And my cart's total should be "$0.00"
|
||||
|
||||
@ui
|
||||
@ui @javascript
|
||||
Scenario: Checking cart's total after removing one item
|
||||
Given the store has a product "T-shirt strawberry" priced at "$17.22"
|
||||
And I add this product to the cart
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Feature: Reapplying promotion on cart change
|
|||
And there is a promotion "Holiday promotion"
|
||||
And I am a logged in customer
|
||||
|
||||
@ui
|
||||
@ui @javascript
|
||||
Scenario: Not receiving discount on shipping after removing last item from cart
|
||||
Given the store has "DHL" shipping method with "$10.00" fee
|
||||
And the promotion gives "100%" discount on shipping to every order
|
||||
|
|
@ -32,7 +32,7 @@ Feature: Reapplying promotion on cart change
|
|||
Then my cart total should be "$100.00"
|
||||
And my cart shipping should be for free
|
||||
|
||||
@ui
|
||||
@ui @javascript
|
||||
Scenario: Receiving discount after removing an item from the cart and then adding another one
|
||||
Given the store has a product "Symfony T-Shirt" priced at "$150.00"
|
||||
And the promotion gives "$10.00" discount to every order
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Feature: Reverting previously applied discount on cart
|
|||
And the store has a product "PHP Mug" priced at "$20.00"
|
||||
And there is a promotion "Christmas promotion"
|
||||
|
||||
@ui
|
||||
@ui @javascript
|
||||
Scenario: Reverting discount applied from total item quantity based promotion
|
||||
Given this promotion gives "$10.00" discount to every order with quantity at least 2
|
||||
And I have product "PHP Mug" in the cart
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ Feature: Apply correct taxes for items with the same tax rate
|
|||
Then my cart total should be "$442.80"
|
||||
And my cart taxes should be "$82.80"
|
||||
|
||||
@ui
|
||||
@ui @javascript
|
||||
Scenario: Proper taxes after removing one of the item
|
||||
Given I have 3 products "PHP T-Shirt" in the cart
|
||||
And I have 2 products "Symfony Hat" in the cart
|
||||
|
|
|
|||
|
|
@ -486,6 +486,8 @@ final class CartContext implements Context
|
|||
*/
|
||||
public function myCartSTotalShouldBe($total)
|
||||
{
|
||||
$this->summaryPage->open();
|
||||
|
||||
Assert::same(
|
||||
$total,
|
||||
$this->summaryPage->getCartTotal(),
|
||||
|
|
|
|||
|
|
@ -123,7 +123,8 @@ class SummaryPage extends SymfonyPage implements SummaryPageInterface
|
|||
public function removeProduct($productName)
|
||||
{
|
||||
$itemElement = $this->getElement('product_row', ['%name%' => $productName]);
|
||||
$itemElement->find('css', 'a.sylius-cart-remove-button')->click();
|
||||
$itemElement->find('css', 'button.sylius-cart-remove-button')->press();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace Sylius\Behat\Page\Shop\Product;
|
|||
|
||||
use Behat\Mink\Driver\Selenium2Driver;
|
||||
use Sylius\Behat\Page\SymfonyPage;
|
||||
use Sylius\Behat\Page\UnexpectedPageException;
|
||||
use Sylius\Component\Product\Model\ProductInterface;
|
||||
use Sylius\Component\Product\Model\ProductOptionInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
|
@ -273,6 +274,28 @@ class ShowPage extends SymfonyPage implements ShowPageInterface
|
|||
return null !== $products->find('css', sprintf('.sylius-product-name:contains("%s")', $productName));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function open(array $urlParameters = [])
|
||||
{
|
||||
$start = microtime(true);
|
||||
$end = $start + 5;
|
||||
do {
|
||||
try {
|
||||
parent::open($urlParameters);
|
||||
$isOpen = true;
|
||||
} catch (UnexpectedPageException $exception) {
|
||||
$isOpen = false;
|
||||
sleep(1);
|
||||
}
|
||||
} while(!$isOpen && microtime(true) < $end);
|
||||
|
||||
if (!$isOpen) {
|
||||
throw new UnexpectedPageException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -18,13 +18,6 @@
|
|||
{{ buttons.create(path('sylius_admin_taxon_create_for_parent', { 'id': taxon.id }), 'sylius.ui.add'|trans) }}
|
||||
{{ buttons.edit(path('sylius_admin_taxon_update', { 'id': taxon.id }), null, null, false) }}
|
||||
{{ buttons.delete(path('sylius_admin_taxon_delete', { 'id': taxon.id }), null, false, taxon.id) }}
|
||||
<form action="{{ url }}" method="post">
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<button class="ui red {% if labeled %}labeled {% endif %}icon button" type="submit" data-requires-confirmation>
|
||||
<i class="icon trash"></i> {{ (message is empty and labeled) ? 'sylius.ui.delete'|trans : message }}
|
||||
</button>
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token(resourceId) }}" />
|
||||
</form>
|
||||
<a class="ui icon button sylius-taxon-move-up" data-url="{{ path('sylius_admin_ajax_taxon_move', { id: taxon.id }) }}" data-id="{{ taxon.id }}" data-position="{{ taxon.position }}">
|
||||
<i class="icon arrow up"></i>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -118,6 +118,10 @@ class OrderItemController extends ResourceController
|
|||
|
||||
$event = $this->eventDispatcher->dispatchPreEvent(CartActions::REMOVE, $configuration, $orderItem);
|
||||
|
||||
if ($configuration->isCsrfProtectionEnabled() && !$this->isCsrfTokenValid($orderItem->getId(), $request->request->get('_csrf_token'))) {
|
||||
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.');
|
||||
}
|
||||
|
||||
if ($event->isStopped() && !$configuration->isHtmlRequest()) {
|
||||
throw new HttpException($event->getErrorCode(), $event->getMessage());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -356,7 +356,7 @@ class ResourceController extends Controller
|
|||
$this->isGrantedOr403($configuration, ResourceActions::DELETE);
|
||||
$resource = $this->findOr404($configuration);
|
||||
|
||||
if ($configuration->isCsrfProtectionEnabled() && !$this->isCsrfTokenValid($resource->getId(), $request->get('_csrf_token'))) {
|
||||
if ($configuration->isCsrfProtectionEnabled() && !$this->isCsrfTokenValid($resource->getId(), $request->request->get('_csrf_token'))) {
|
||||
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
|||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
|
@ -1175,7 +1176,7 @@ final class ResourceControllerSpec extends ObjectBehavior
|
|||
$requestConfigurationFactory->create($metadata, $request)->willReturn($configuration);
|
||||
$configuration->hasPermission()->willReturn(true);
|
||||
$configuration->getPermission(ResourceActions::DELETE)->willReturn('sylius.product.delete');
|
||||
$request->get('_csrf_token')->willReturn('xyz');
|
||||
$request->request = new ParameterBag(['_csrf_token' => 'xyz']);
|
||||
|
||||
$container->has('security.csrf.token_manager')->willReturn(true);
|
||||
$container->get('security.csrf.token_manager')->willReturn($csrfTokenManager);
|
||||
|
|
@ -1223,7 +1224,7 @@ final class ResourceControllerSpec extends ObjectBehavior
|
|||
$requestConfigurationFactory->create($metadata, $request)->willReturn($configuration);
|
||||
$configuration->hasPermission()->willReturn(true);
|
||||
$configuration->getPermission(ResourceActions::DELETE)->willReturn('sylius.product.delete');
|
||||
$request->get('_csrf_token')->willReturn('xyz');
|
||||
$request->request = new ParameterBag(['_csrf_token' => 'xyz']);
|
||||
|
||||
$container->has('security.csrf.token_manager')->willReturn(true);
|
||||
$container->get('security.csrf.token_manager')->willReturn($csrfTokenManager);
|
||||
|
|
@ -1271,7 +1272,7 @@ final class ResourceControllerSpec extends ObjectBehavior
|
|||
$requestConfigurationFactory->create($metadata, $request)->willReturn($configuration);
|
||||
$configuration->hasPermission()->willReturn(true);
|
||||
$configuration->getPermission(ResourceActions::DELETE)->willReturn('sylius.product.delete');
|
||||
$request->get('_csrf_token')->willReturn('xyz');
|
||||
$request->request = new ParameterBag(['_csrf_token' => 'xyz']);
|
||||
|
||||
$container->has('security.csrf.token_manager')->willReturn(true);
|
||||
$container->get('security.csrf.token_manager')->willReturn($csrfTokenManager);
|
||||
|
|
@ -1318,7 +1319,7 @@ final class ResourceControllerSpec extends ObjectBehavior
|
|||
$requestConfigurationFactory->create($metadata, $request)->willReturn($configuration);
|
||||
$configuration->hasPermission()->willReturn(true);
|
||||
$configuration->getPermission(ResourceActions::DELETE)->willReturn('sylius.product.delete');
|
||||
$request->get('_csrf_token')->willReturn('xyz');
|
||||
$request->request = new ParameterBag(['_csrf_token' => 'xyz']);
|
||||
|
||||
$container->has('security.csrf.token_manager')->willReturn(true);
|
||||
$container->get('security.csrf.token_manager')->willReturn($csrfTokenManager);
|
||||
|
|
@ -1369,7 +1370,7 @@ final class ResourceControllerSpec extends ObjectBehavior
|
|||
$requestConfigurationFactory->create($metadata, $request)->willReturn($configuration);
|
||||
$configuration->hasPermission()->willReturn(true);
|
||||
$configuration->getPermission(ResourceActions::DELETE)->willReturn('sylius.product.delete');
|
||||
$request->get('_csrf_token')->willReturn('xyz');
|
||||
$request->request = new ParameterBag(['_csrf_token' => 'xyz']);
|
||||
|
||||
$container->has('security.csrf.token_manager')->willReturn(true);
|
||||
$container->get('security.csrf.token_manager')->willReturn($csrfTokenManager);
|
||||
|
|
|
|||
|
|
@ -19,3 +19,12 @@ sylius_shop_ajax_cart_add_item:
|
|||
route: sylius_shop_cart_summary
|
||||
parameters: {}
|
||||
flash: sylius.cart.add_item
|
||||
|
||||
sylius_shop_ajax_cart_item_remove:
|
||||
path: /{id}/remove
|
||||
methods: [DELETE]
|
||||
defaults:
|
||||
_controller: sylius.controller.order_item:removeAction
|
||||
_format: json
|
||||
_sylius:
|
||||
flash: sylius.cart.remove_item
|
||||
|
|
|
|||
|
|
@ -28,12 +28,3 @@ sylius_shop_cart_clear:
|
|||
_controller: sylius.controller.order:clearAction
|
||||
_sylius:
|
||||
redirect: sylius_shop_cart_summary
|
||||
|
||||
sylius_shop_cart_item_remove:
|
||||
path: /{id}/remove
|
||||
methods: [DELETE]
|
||||
defaults:
|
||||
_controller: sylius.controller.order_item:removeAction
|
||||
_sylius:
|
||||
redirect: sylius_shop_cart_summary
|
||||
flash: sylius.cart.remove_item
|
||||
|
|
|
|||
|
|
@ -23,3 +23,6 @@ sylius_shop_partial_cart_add_item:
|
|||
type: Sylius\Bundle\CoreBundle\Form\Type\Order\AddToCartType
|
||||
options:
|
||||
product: expr:service('sylius.repository.product').find($productId)
|
||||
redirect:
|
||||
route: sylius_shop_cart_summary
|
||||
parameters: {}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
throttle: 500
|
||||
});
|
||||
|
||||
$('.sylius-cart-remove-button').removeFromCart();
|
||||
$('#sylius-product-adding-to-cart').addToCart();
|
||||
|
||||
$('#sylius-shipping-address').addressBook();
|
||||
|
|
|
|||
|
|
@ -12,44 +12,36 @@
|
|||
|
||||
$.fn.extend({
|
||||
addToCart: function () {
|
||||
$(this).on('submit', function(event) {
|
||||
refresh(this, event);
|
||||
var element = $(this);
|
||||
var href = $(element).attr('action');
|
||||
var redirectUrl = $(element).data('redirect');
|
||||
var validationElement = $('#sylius-cart-validation-error');
|
||||
|
||||
$(element).api({
|
||||
method: 'POST',
|
||||
on: 'submit',
|
||||
cache: false,
|
||||
url: href,
|
||||
beforeSend: function (settings) {
|
||||
settings.data = $(this).serialize();
|
||||
|
||||
return settings;
|
||||
},
|
||||
onSuccess: function (response) {
|
||||
validationElement.addClass('hidden');
|
||||
window.location.replace(redirectUrl);
|
||||
},
|
||||
onFailure: function (response) {
|
||||
validationElement.removeClass('hidden');
|
||||
var validationMessage = '';
|
||||
|
||||
$.each(response.errors.errors, function (key, message) {
|
||||
validationMessage += message;
|
||||
});
|
||||
validationElement.html(validationMessage);
|
||||
$(element).removeClass('loading');
|
||||
},
|
||||
});
|
||||
},
|
||||
disable: function() {
|
||||
$('button[type=submit]', this).attr('disabled', 'disabled');
|
||||
},
|
||||
enable: function() {
|
||||
$('button[type=submit]', this).removeAttr('disabled');
|
||||
}
|
||||
});
|
||||
|
||||
function refresh(element, event) {
|
||||
event.preventDefault();
|
||||
var data = $(element).serialize();
|
||||
var href = $(element).attr('action');
|
||||
var redirectUrl = $(element).data('redirect');
|
||||
var validationElement = $('#sylius-cart-validation-error');
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: href,
|
||||
data: data,
|
||||
cache: false,
|
||||
success: function (response) {
|
||||
validationElement.addClass('hidden');
|
||||
window.location.replace(redirectUrl);
|
||||
},
|
||||
error: function (response) {
|
||||
validationElement.removeClass('hidden');
|
||||
var validationMessage = '';
|
||||
$.each(response.responseJSON.errors.errors, function (key, message) {
|
||||
validationMessage += message;
|
||||
});
|
||||
validationElement.html(validationMessage);
|
||||
$(element).removeClass('loading');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
})( jQuery );
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Paweł Jędrzejewski
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
(function ( $ ) {
|
||||
'use strict';
|
||||
|
||||
$.fn.extend({
|
||||
removeFromCart: function () {
|
||||
$.each($(this), function (index, element) {
|
||||
var redirectUrl = $(element).data('redirect');
|
||||
var csrfToken = $(element).data('csrf-token');
|
||||
|
||||
$(element).api({
|
||||
method: 'DELETE',
|
||||
on: 'click',
|
||||
beforeSend: function (settings) {
|
||||
settings.data = {
|
||||
_csrf_token: csrfToken
|
||||
};
|
||||
|
||||
return settings;
|
||||
},
|
||||
onSuccess: function (response) {
|
||||
window.location.replace(redirectUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})( jQuery );
|
||||
|
|
@ -34,10 +34,10 @@ function handleProductOptionsChange() {
|
|||
|
||||
if ($price !== undefined) {
|
||||
$('#product-price').text($price);
|
||||
$('#sylius-product-adding-to-cart').enable();
|
||||
$('button[type=submit]').removeAttr('disabled');
|
||||
} else {
|
||||
$('#product-price').text($('#sylius-variants-pricing').attr('data-unavailable-text'));
|
||||
$('#sylius-product-adding-to-cart').disable();
|
||||
$('button[type=submit]').attr('disabled', 'disabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<span class="sylius-quantity">{{ form_widget(form.quantity) }}</span>
|
||||
</td>
|
||||
<td class="center aligned">
|
||||
<a href="{{ path('sylius_shop_cart_item_remove', {'id': item.id}) }}" class="ui circular icon button sylius-cart-remove-button"><i class="remove icon"></i></a>
|
||||
<button type="button" data-redirect="{{ path('sylius_shop_cart_summary') }}" data-url="{{ path('sylius_shop_ajax_cart_item_remove', {'id': item.id}) }}" class="ui circular icon button sylius-cart-remove-button" data-csrf-token="{{ csrf_token(item.id) }}"><i class="remove icon"></i></button>
|
||||
</td>
|
||||
<td class="right aligned">
|
||||
<span class="sylius-total">{{ money.convertAndFormat(item.subtotal) }}</span>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{% form_theme form 'SyliusUiBundle:Form:theme.html.twig' %}
|
||||
|
||||
<div class="ui segment" id="sylius-product-selecting-variant">
|
||||
{{ form_start(form, {'action': path('sylius_shop_ajax_cart_add_item', {'productId': product.id}), 'attr': {'id': 'sylius-product-adding-to-cart', 'class': 'ui loadable form', 'novalidate': 'novalidate', 'data-redirect': path('sylius_shop_cart_summary')}}) }}
|
||||
{{ form_start(form, {'action': path('sylius_shop_ajax_cart_add_item', {'productId': product.id}), 'attr': {'id': 'sylius-product-adding-to-cart', 'class': 'ui loadable form', 'novalidate': 'novalidate', 'data-redirect': path(configuration.getRedirectRoute('summary'))}}) }}
|
||||
{{ form_errors(form) }}
|
||||
<div class="ui red label bottom pointing hidden sylius-validation-error" id="sylius-cart-validation-error"></div>
|
||||
{% if not product.simple %}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue