[Shop] Fix security issues in cart actions

This commit is contained in:
Arminek 2017-01-19 15:20:15 +01:00
parent 442fc57e78
commit bd7744d5c3
20 changed files with 128 additions and 71 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -486,6 +486,8 @@ final class CartContext implements Context
*/
public function myCartSTotalShouldBe($total)
{
$this->summaryPage->open();
Assert::same(
$total,
$this->summaryPage->getCartTotal(),

View file

@ -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();
}
/**

View file

@ -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}
*/

View file

@ -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>

View file

@ -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());
}

View file

@ -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.');
}

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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: {}

View file

@ -48,6 +48,7 @@
throttle: 500
});
$('.sylius-cart-remove-button').removeFromCart();
$('#sylius-product-adding-to-cart').addToCart();
$('#sylius-shipping-address').addressBook();

View file

@ -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 );

View file

@ -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 );

View file

@ -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');
}
});
}

View file

@ -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>

View file

@ -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 %}