Symfony Simple and Secure Ajax Call for Checkboxes

Almost a year ago, I wrote one of my first blog articles, explaining how to use ajax to validate form fields before submitting the actual form. Ajax is a great way to improve user experience, but one needs to be extra careful not compromise the security of a webpage. So here is a simple and secure way to use Ajax for tasks like saving user preferences, validating choices and many more.

Aims of this tutorial :

  • Create a simple and secure method that allows updating user input data silently

Symfony version: 3.4 (LTS), but should work with all 3+ versions

Let’s get started!

Part 1: Context

Let us consider a fun example for this one. We need the user to say which cookies he wants to eat, by checking the corresponding checkbox. Of course, we don’t want to bother him with a validation button. So we need to save his choices silently, using Ajax.

Part 2: Entities

All we need for this part is to know that we have a Cookie entity and that it has a toEat member.

<?php
class Cookie
{
    /**
     * @ORM\Column(name="to_eat", type="boolean")
     */
	private $toEat;
}

Part 3: View

For each cookie, we need a checkbox:

{% for cookie in cookies %} 
    <input type='checkbox' onchange='eatTheCookie({{cookie.id}}, $(this))' {% if cookie.toEat %}checked{% endif %}>
{% endfor %}

Each input is either checked or not, based on the state of cookie.toEat member. When the state of the checkbox changes, the eatTheCookie(...) method is called.

function eatTheCookie(cookie, ob)
{
	if (ob.prop('checked'))
		checked = 1;
	else
		checked = 2;
 
	$.ajax({
	    type: "POST",
	    url: "{{ path('cookiejar_cookie_eat') }}",
	    data: {
			cookie_id: cookie,
		 	checked: checked
		}
	})
	.done(function(data){
		if (data.status == "Done")
		{
			// Inform the user that all went well
		}
		else
		{
			// Alert the user that something went wrong
		}
	})
	.fail(function()
	{
		// Alert the user that something went wrong
	});
}

As you can see, this is pretty straightforward. The method uses an Ajax call to send two pieces of information: the cookie id and the state of the checkbox. This is because we allow the user to change his mind and deselect cookies he no longer wants to eat.

Part 4: Routing

cookiejar_cookie_eat:
    path: /cookie/eat
    defaults:
        _controller: CookiejarBundle:Cookie:eat

Part 5: Controller

The eat method from the CookieController needs to do take the following steps:

  • Check that the request type is correct (lines 7-8)
  • Check that the data sent from the user is correct (lines 15-16). We know that both parameters are integers, so any other type of data should be considered unsafe. That is why I convert the data to an integer using the intval method.
  • Check that the cookie object exists (lines 23-26)
  • Check that the user has the required permissions to do the requested action (lines 29-30). If you don’t want or need to implement a voter to check for specific permission, you should at least
    • check that the user is fully authenticated,$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')
    • or that the user has a specific role $this->get('security.authorization_checker')->isGranted('ROLE_COOKIE_MONSTER')
  • If all the above is in order, update the cookie object (lines 36-40)
<?php
use Symfony\Component\HttpFoundation\JsonResponse;
 
public function eatAction(Request $request)
{
    // Is it an Ajax Request ?
    if (!$request->isXmlHttpRequest())
        return new JsonResponse(array('status' => 'Error'),400);

    // Request has request data ?
    if (!isset($request->request))
        return new JsonResponse(array('status' => 'Error'),400);

    // Get data
    $checked = intval($request->request->get('checked'));
    $cookie_id = intval($request->request->get('cookie_id'));

    // Is the data correct ?
    if ($checked != 1 && $checked != 2)
        return new JsonResponse(array('status' => 'Error'),400);

    // Does the cookie object exist ?
    $cookie = $em->getRepository('CookiejarBundle:Cookie')->findOneById($cookie_id);

    if ($cookie === null)
        return new JsonResponse(array('status' => 'Error'),400);
    
    // Does the user have permission to eat the cookie ?
    if (!$this->get('security.authorization_checker')->isGranted('eat_cookie', $cookie))
        return new JsonResponse(array('status' => 'Error'),403);

    // All seems fine ! Eat the cookie !
    $cookie->setToEat($checked%2);

    // Save the data to the database
    $this->entityManager->persist($cookie);
    $this->entityManager->flush();

    // Inform user that all went well
    return new JsonResponse(array('status' => 'Done'),200);
}

That’s all!

Part 6: Random thoughts

You probably noticed that my checked variables range from 1 to 2, instead of the traditional 0 to 1. I admit I have some sort of weird “zero-phobia”! I mean, the zero-null-false-empty confusions happen so often, that I prefer handling solid, not ambiguous values, such as 1 and 2. That way, when I use the intval() method I am sure to obtain a zero result only if the initial variable is unsafe or corrupt. However, this is a personal preference, so feel free to replace my 2 value with a 0, if that suits you more 😉

I hope this was useful && …

Credits

One thought to “Symfony Simple and Secure Ajax Call for Checkboxes”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.