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')
- check that the user is fully authenticated,
- 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
- The cookies are courtesy of Nikita Golubev, Smashicons, Freepik from Flaticon, licensed by CC 3.0 BY
- The title image is from Pixabay, by jyliagorbacheva
- The happy new year is by the talented VladStudio