Categories
Software Development Web Development

Symfony 5+ how to view dump output with AJAX requests

So the idea is to be able to use the Symfony VarDumper component to be able to dump values when sending an AJAX request and view them for debugging while in development mode, but not in production mode.

Create a new directory

To do this you will need to create an event subscriber and listen for the kernel.response event. To do this you create a new directory inside your root directory, I called mine EventSubscribers, the name doesn’t matter you could call yous BlueWaffle and it will work exactly the same.

Create a new file

Inside this new directory create a new subscriber class, I called mine AjaxDumpSubscriber. You can make your subscriber implement

 implements EventSubscriberInterface

I am not sure if that is needed, I saw it somewhere in the docs. They never mentioned if it was needed so I am assuming it is maybe needed.

Then add the method stub for

getSubscribedEvents()

Here is what the github source code comment says about this method

interface EventSubscriberInterface
{
    /**
     * Returns an array of event names this subscriber wants to listen to.
     *
     * The array keys are event names and the value can be:
     *
     *  * The method name to call (priority defaults to 0)
     *  * An array composed of the method name to call and the priority
     *  * An array of arrays composed of the method names to call and respective
     *    priorities, or 0 if unset
     *
     * For instance:
     *
     *  * ['eventName' => 'methodName']
     *  * ['eventName' => ['methodName', $priority]]
     *  * ['eventName' => [['methodName1', $priority], ['methodName2']]]
     *
     * The code must not depend on runtime state as it will only be called at compile time.
     * All logic depending on runtime state must be put into the individual methods handling the events.
     *
     * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
     */
    public static function getSubscribedEvents();
}

The full class

The entire class should look like this, from what I can tell. This is working code.

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class AjaxDumpSubscriber  implements EventSubscriberInterface
{

    /**
     * @return array
     */
    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::RESPONSE];
    }

    public function onKernelResponse(ResponseEvent $event)
    {
        if (!$event->getKernel()->isDebug()) {
            return;
        }
        $request = $event->getRequest();

        if (!$request->isXmlHttpRequest()) {
            return;
        }
        $response = $event->getResponse();
        //set this one value only if this is in development mode
        $response->headers->set('Symfony-Debug-Toolbar-Replace', 1);
    }
}

That is it, now you can dump() values like normal… but you can’t view them like normal, not in Symfony 5.4, not for me anyways .

This only partially works, maybe I missed something or I expect something? Most of the time when you use dump() there will be an icon in the profiler bar that indicates debug output was output aka. dump() was used. Usually you can click on it and quickly see the values that were dumped. This feature doesn’t work with  AJAX requests at the moment, from what I can observe.

Instead you need to click the response when it indicates in the profiler then click the Debug tab on the left panel to see your dump() values.

Links

The VarDumper component. -> Docs link you must install this before you can dump anything.

The profiler component -> Docs link you need this to see dumped stuff, even though it doesn’t work properly with the VarDumper

Creating an EventSubscriber -> Docs link

Using event subscribers -> docs link

 

Categories
Software Development Web Development

Symfony how to get query string values sent by AJAX calls

When using a Symfony API endpoint for getting values such as maybe something like an auto-complete feature you will need to be able to send some text(what the user enters) to the backend. Then have the backend return a response based on that.

For example with an auto complete feature, you might want to send 3 characters to an API endpoint and have it only return a list of words that start with those three characters, instead of returning every last word in the database. This will use less of your servers resources and the users while speeding your app up.

The way I suggest to do this is to first use FosJsRouting bundle in your Javascript to create the URL’s for you. This makes it much easier, faster ( Once you get it installed etc ) and consistent. Here is a long article I wrote about installing and using FosJsRouting bundle How to get URL Routes in your Javascript in Symfony 5+.

This is a short example and doesn’t include checking if the user is logged in ( has rights) if it is an AJAX request, check headers etc. it is just for this example.



/**
     * returns a list of the users image collection names
     * @Route("/get_image_collections", name="get_image_collections", options={"expose"=true},  methods={"GET"})
     */
    public function get_image_collections(Request $request): JsonResponse
    {
        $query = $request->getQueryString();
      $text2 = $request->query->get('text');
      $text =  $request->get("text");
        $values = array(
            'first',
            'second',
            'third',
            'fan',
            'free',
            'narcotics',
            'arse',
            'tardigrade',
            'tinnitus',
            'monkey',
            'mall',
            'doppleganger',
            'ballocks',
            'zoo',
            $query,
            $text,
            $text2
        );
        return new JsonResponse($values);
    }

This is very simple, it doesn’t even hit a database, it is just for this article and testing. All this does right now is return the list and add some text and query so I can see what was sent. In production you might even want to use caching so that your database only gets hit if it has to.

As you can see there appears to be two ways to get the value of the query. Both worked for me, but I am thinking that going through the query method is the proper way like follows.

$request->query->get('text');

The query being used for this endpoint looks like this

"http://sogi-test/get_image_collections?text=fa"

In the future I’ll update the code to include security checks, to return a matching list compiled from a database query and caching.

But notice the (Request $request) that is Dependency Injected ( see symfony dependency injection )

I should also add, below is how I am building the URL with FosJsBundle

let url = Routing.generate('get_image_collections', {text: text});

That is where the “text” named query key is coming from. You can name the key anything you want. If you have more than one named value then use a comma and add it after the first, like in the FosJsRoutingBundle docs under how to use above link.
For example say you want to include something like a page number for paging through results or limiting them etc. You could create the
url route like this

let url = Routing.generate('get_image_collections', {text: text, limit: limit});

This Javascript JSON looks confusing, the first part is the name(key) the second is a(value) variable containing the value
Then to get the values in your controller route you do this.


$text = $request->query->get('text');
$limit = $request->query->get('limit');

You can send as many values as you need to this way. Forms work a little differently. I’ll write an article about those soon too.

And that is how you get the values you send.
baby how it is done meme
That is how it is done

Interesting Error story

I figured I would mention this debugging story I had while sending an ajax request with an improperly formed URL ( aka not matching the route)

I kept getting errors when trying to use Promise.json() with the value returned from my AJAX call. What happened is the server was returning HTML instead of JSON which results in an error when Promise.json() is called.

A while of that and thinking it was my JS code but not really sure of WTF was going on. I finally started outputting my URL to see what was being created and low and behold that was it. I was even watching the Request/Response in the browser. But, it was not obvious until I really started inspecting the created URL vs the route output that I figured out my route and url didn’t match and that was the reason for the redirect.

How to view Symfony routes

 

My route didn’t match because at some point I changed how I wanted the route to look and I had not rerun the FosJsRouting bundle as mentioned in that article to renew the routes in it’s json file.  So it created a URL that didn’t match, the route was created like /route/{text} but the api route expected /route&text=….

I didn’t get an error. Nope and nothing was recorded that I could find in the error logs. Symfony simply redirected ( 301) sent back to my ajax which then somehow did another request to the page I was using the javascript in,  returning the pages HTML.

I was like

Uhm, wait… what?

I have no idea why Symfony redirects when the route doesn’t match.  I don’t even know what setting to adjust where to change this, I know I did something somewhere at sometime… I’ll have to dig into what I did.

Why this happend is because I didn’t re-update the FosJsRouting json file and then restart webpack so it pulls in the new file. But I know there is some sort of setting somewhere for Symfony for the redirect action I am seeing and I know I set it somewhere somehow.

Can I have hamburger now???

Categories
Web Development Web Security

How to secure individual Symfony AJAX api routes without using API Platform

Creating the Symfony route is easy. Checking if the request was sent by AJAX is again easy. But what stops a mischievous hacker from hitting that endpoint and trying to get a list of used emails or something else with a script?

What if you have routes that you want to access with AJAX without API Platform? With Symfony, standard forms created with the Form Component, your forms are CSRF protected. But, when you are sending an AJAX request to an endpoint without a form how do you protect it?

There is probably some Symfony approved way I am not aware of.

If you send the whole form you can use a different procedure and use the CSRF string stored in the form.

However, for simple situations where you need to randomly access a route you can do something similar to the CSRF form protection by generating a unique string and saving in a Session cookie and to the page/form.

Where you save the string in the page is up to you, but it should be a hidden element. This element needs a unique ID in the page so that you can access it with Javascript. A hidden input element in a form works great, otherwise use a hidden span element.(use css to hide the element).

When you need to make a request to the route you use javascript to get the value you hid in the element. Make sure it is just the unique string that you fetch not the entire element html or this wont work. Include this string with the data you are sending to the route.

Inside your route fetch the unique string that you sent in your AJAX. Then try to fetch the same unique string from your session cookies. If the string exists and matches process the request.

There are tricks you can try to use with the header like checking the users browser agent. But that is useless as it can be easily spoofed by a good hacker using something like Curl.

This unique string trick isn’t 100% hacker proof. But it makes it a hell of a lot harder.  More on CSRF attacks here.

NOTE

If you are using the Symfony forms with CSRF activated then you can use Javascript to fetch the value of the nonce hidden in the _token input element. However, if your code will make multiple ajax requests, then you might want to create the custom hidden field and generate a new unique string each time and replace it in the custom field.

Step #1 create the field

To create the field add it in the FormType definition like this. The entire class is too long so I’ll show just the add section.


->add('ajaxString', HiddenType::class, [
                'mapped' => false,
                'attr' => ['class' => 'hidden-field', 'value' => $secretString]
            ])

Notice mapped is false so that I don’t get errors.

Step #2 Build the form

Now you build the form inside the Template for the form. Mine looks like this.


{{ form_start(registrationForm) }}
        {{ form_errors(registrationForm) }}
        {{ form_row(registrationForm.email) }}
        {{ form_row(registrationForm.emailMatch) }}
        {{ form_row(registrationForm.plainPassword) }}
        {{ form_row(registrationForm.passwordMatch) }}
        {{ form_row(registrationForm.userAlias) }}
        {{ form_row(registrationForm.ajaxString, { 'id': 'ajaxString'}) }}
        {{ form_row(registrationForm.agreeTerms) }}

        <div class="d-flex justify-content-center">
            <button type="submit" class="btn btn-lg btn-success">Register</button>
        </div>

        {{ form_end(registrationForm) }}

Notice how I have the id : ajaxString line. This is currently the only way to change the ID of a form field in Symfony see How to change the id for a form input in Symfony 5+

Step #3 add initial value

Inside the controller you must add the initial value for the field and store it in a session cookie.

For this I am using a simple class which generates semi random/unique strings. This doesn’t need to be super top notch secure, it is just to make sure the request is coming from a form my app built.

To access the Session Cookie in Symfony 5.3+ you must now use RequestStack instead of Session or SessionInterface for some odd reason. It just makes it more obscure and harder to figure out how to get to sessions.


$session = $this->requestStack->getCurrentRequest()->getSession();
        $secretString = RandomStringGenerator::lowercaseUppercaseNumberString(32);
        $session->set('secretString', $secretString);

 

To check the value in the Controller route endpoint I do like this.


$secretString = $request->query->get('secretString');
        $secretString = DataSanitizer::sanitizeString($secretString);
        $string = $this->requestStack->getCurrentRequest()->get('secretString');

        if ($request->isXmlHttpRequest() && $secretString === $string) {

Note that secretString is the value sent by the AJAX request. This was the value I hid in the form field to use for this purpose.
The other line

$string=$this->requestStack->getCurrentRequest()->get(‘secretString’);

gets the value I stored in the Session Cookie. Then the if statement makes sure the two values match before processing the request. If the two strings match we know that my app built the form, added the string and my Javascript copied the string and sent it to my server. This prevents people from randomly hitting your route endpoints.

&& $secretString === $string

Links

Here is a good link to Symfony Casts about API Platform. There are many symfony casts here to learn more. I was going to post each but this link contains all of them with pretty pictures and descriptions. LOL

More about CSRF in symfony forms here in the documentation.

Categories
Web Development

How to make AJAX requests to Symfony 5+ controllers

I couldn’t find anything in the documentation about this and there is little on the internet about it too.  There is probably a Bundle somewhere for this or some Symfony way, but I didn’t find anything.

I did find this SymfonyCast about submitting a whole form. If you need to test your Controller route read this. If you want to know more read this in the docs about Browserkit.

But what if you need to just send a simple AJAX request to a controller route? Well that is what this is about. This is very basic, there are settings you can add to the request for mime type for example and more.

How to send a simple AJAX request?

First you need the javascript to make a request to your controller endpoint. To do this you will need access to your routes, in your javascript. Read my article How to get URL Routes in your Javascript in Symfony 5+   to find out how.

Once you have your routing setup and you understand how that works you need to build your AJAX request. I use a custom class I built called EzAjax.js which is a wrapper around the jquery ajax function shown below.

The AJAX solution
import {Utils} from "./Utils";

class EZAjax {
    /**
     *
     * @param errorCallback
     * @param successCallback
     * @param requestUrl
     * @param data
     * @param ajaxOptions
     */
    constructor(errorCallback, successCallback, requestUrl, data, ajaxOptions = {}) {

        this.ajaxObj = null;
        this.ajaxOptionsObj = ajaxOptions;
        this.attempts = 0;
        this.delay = 300;
        this.errorCallback = errorCallback;
        this.successCallback = successCallback;
        //add the settings to the ajaxOptions
        this.ajaxOptionsObj.url = requestUrl;
        this.ajaxOptionsObj.data = data;
        this.ajaxOptionsObj.error = this.ajaxError.bind(this);
        this.ajaxOptionsObj.success = this.ajaxSuccess.bind(this);

    }

    ajaxError(jqXHR, errorString, errorThrown) {
        console.log('error string ' + errorString + ' error thrown ' + errorThrown);
        if (this.attempts <= 3) {
            console.log('attempts is ' + this.attempts + ' delay ' + this.delay);
            setTimeout(() => {
                this.performRequest();
            }, this.delay *= 2);
        } else {
            this.errorCallback(jqXHR, errorString, errorThrown);
        }
    }

    ajaxSuccess(response) {
        this.successCallback(response);
    }

    performRequest() {

        this.attempts++;
        console.log('performing request ' + this.attempts);
        //prevent multiple requests
        if (this.ajaxObj) {
            this.ajaxObj.abort();
        }
        console.log('the ajax options ' + JSON.stringify(this.ajaxOptionsObj));
        this.ajaxObj = $.ajax(this.ajaxOptionsObj);
    }

}

export {EZAjax};

This makes it easier to work with. I just create a simple fail and success function and pass the names in like this.

let ezAjax = new EZAjax(emailExistsError, emailExistsSuccess, checkEmailUrl, data);
ezAjax.performRequest();
Testing inside Javascript

The below code goes inside a class you place inside app\assets\javascript\ directory I called mine registration.js  Below is a snippet of the code for checking if the email exists.


//code for comparing email fields before submit and make sure the email doesn't exist
$(emailMatchField).focusout(function (event) {
    let email = $(emailField).val();
    let emailConfirm = $(emailMatchField).val();

    if (email !== emailConfirm) {

        $('#noEmailMatch').remove();
        $(emailMatchField).css('background-color', redBgColor);
        let matchHtml = '<small id=' + emailMatchErrorId + '" class="red-text" >Email fields must match</small>';
        $(emailMatchField).after(matchHtml);

    }
  if (email === emailConfirm) {
        let data = {"email" : email};
        // if the email fields match make sure the email does not exist in the system
        let checkEmailUrl = Routing.generate( 'email_exists');
        let ezAjax = new EZAjax(emailExistsError, emailExistsSuccess, checkEmailUrl, data);
        console.log('the route is ' + checkEmailUrl);
        ezAjax.performRequest();
        $('#noEmailMatch').remove();
        $(emailMatchField).css('background-color', whiteBgColor);
    }
});

The first if statement checks if the fields match if not it shows an error. The second if statement checks if the email exists only if both fields match first.

The emailExistsError function will open a dialog to inform the user the email they entered exists and give them a login link instead. emailExistsSuccess does nothing I could just put function() in it’s place if I wanted.

Notice this line though

let data = {"email" : email};

That is the data we send via the EzAJAX class. That is the value we need in the controller.

The Symfony controller

Now we need to be able to work with the values sent in the request.

public function checkEmailExists(Request $request)
{
    $json = array();

    if ($request->isXmlHttpRequest()) {
        //check if the email exists
        $email = $request->query->get('email');
        $email = DataSanitizer::sanitizeEmail($email);
        $exists = $this->getDoctrine()->getRepository(User::class)->checkEmailExists($email);
        if($exists){
            $loginUrl = $this->generateUrl('app_login');
            $linkAttr = array(
                AnchorTagAttributes::HREF_ATTRIBUTE => $loginUrl,
                GlobalHtmlAttributes::CLASS_ATTRIBUTE => 'h5'
            );
            $loginLink = AnchorTag::getAnchorTag('login', $linkAttr);
            $json['error'] = "A user with this email exists. Please $loginLink instead. Or use another email.";
        }
    } else {
        $json['error'] = $this->illegalRequest;
    }
    return new JsonResponse($json);
}

To get the value sent via AJAX in the controller I use:

$email = $request->query->get(’email’);

Which matches the value I sent in the data JSON object to this controller.

As you can see in the javascript I am passing a value for email and in the controller I am fetching that value. I didn’t add any mime type info to the request, so the default for jquery ajax is 'application/x-www-form-urlencoded; charset=UTF-8'. To change the MIME type set the contentType on the ajax method.

Also note that this way of working with an Ajax request in the controller is not secured. Anyone from any IP can send a request to this, which can flood the server or allow them access to things they shouldn’t be allowed to access.

Soon I will dig into how to best secure the Symfony Contoller routes and write an article about that.