Categories
Web Development

How to fix Symfony FosJsRoutingBundle outputs routes in browser

Yeah I got this problem once too. The routing bundle outputs the routes in your browser on a plain white background, giving the user no options to navigate etc. after they register or login.

A user needs to see a real page

So how do you fix this? Read the last step of this article I wrote How to get URL Routes in your Javascript in Symfony 5+

It is a long article but it explains everything. See the note about the Last Step. I don’t want to repeat it here because I don’t think many will view this article anyways.

Categories
Uncategorized

How to fix Symfony Compile Error: Cannot use Repository as Repository because the name is already in use

So you got this error? WTF does it even mean?

baby what does that even mean meme
What does this error mean?

Well my actual error was this.
Compile Error: Cannot use App\Repository\PageUrlsRepository as PageUrlsRepository because the name is already in use

I oopsied

You can see here what I did is accidentally add a second use statement. I remove that and BOOM all back to normal.

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

Symfony 5.3+ how to use Sessions with RequestStack

So some changes happened in Symfony 5.3. Previously you could get to a session with either Session or SessionInterface. Some didn’t like how that worked so now it is moved to RequestStack. The docs or article are not correct here.

It shows you get to the session like this.


$session = $this->requestStack->getSession();

But that doesn’t work. You will be told that RequestStack doesn’t have a getSession() method. I had to open up the source code to figure out how this works.

You get to the session instead like this now.


 $session = $this->requestStack->getCurrentRequest()->getSession();

Note you have to call getCurrentRequest() then getSession. now you can use sessions like this.


$session = $this->requestStack->getCurrentRequest()->getSession();
        $session->set('key-name', $value);

You will now have access to all of the session methods via $session. Your IDE should now list all of the methods in the Session class that you can access.

How to get the RequestStack?

So how do you get the ReqeustStack? Autowiring.

You simply Autowire it into your Controller route method or the __constructor() method. I prefer the constructor method in my Controllers if more than one route needs it.  But in other services you have no choice, it has to be autowired via the constructor like this.



   private RequestStack $requestStack;

    public function __construct(MysqlConnection $mysqlConnection, RequestStack $requestStack)
    {
        $this->mysqlConnection = $mysqlConnection;
        $this->illegalRequest = 'Sorry. Your request to this API is not allowed';
        $this->requestStack = $requestStack;
    }

Now any method can access the requestStack and through the RequestStack you can access the Session. At least for now.

Here is a link to the actual Symfony Session docs.

Categories
Web Development

Symfony 5+ how to make a form field hidden from display

This is easier than it sounds, but I am writing this in case I need to remember what the answer is.

At first I wasn’t paying attention to all of the many different Symfony form types in this long list.  I totally didn’t see the HiddenType in the list or I didn’t notice it.

I tried to simply add a class using attr in the definition. This kind of worked. It just showed the name of the field in a label, which wouldn’t work for my design and use.

HiddenType works exactly like what I needed.

When building a form in a FormType class you can create hidden fields like this one which hides a nonce for AJAX request validation.

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

Always add ‘mapped’ false for any field you want to tell Symfony to ignore, like this field used for processing AJAX requests. $secretString is just a random 32 character string I am storing in a session on the backend and sending with the AJAX request to make sure the request is coming from my app.

Categories
Web Development

How to change the id for a form input in Symfony 5+

If you create your forms with classes in Symfony 5+ then changing the ID of the form fields is something you are not allowed to do apparently. LOL You can add/change the class and other attributes but not the id. For some reason Symfony ONLY lets you change the id inside the template. I don’t know why.

Yes I am serious

To start with what got me even interested in trying to use attr and row_attr is when I was messing around with some of my forms, I was copying and pasting and moving parts in the template. This lead to issues as I would miss pieces or get things wrong some how. So I started trying to do the whole thing inside the FormType definition class using the methods below.

It has been pointed out that some feel it is better to define class, id etc. in the template. But as I pointed out above, I had issues with that. So below is what I found.

What doesn’t work

If you are like me then you have probably tried changing the ID by using the attr or row_attr attributes of the Type right? That seems logical right?

These two methods  are not even consistent. First off row_attr only accepts some attributes, which ones I have no idea, it ignores placeholder and id apparently. So then I tried attr, it works with placeholder but ignores ID.

Makes sense right?

It sure would be nice if those didn’t ignore the values you sent to them wouldn’t it. This problem is nearly 10 years old. Later I may look over the code update it and do a PR.

If you are like me you are using Javascript to read hidden fields from the form for various reasons. Otherwise the standard naming of ID’s works flawlessly. I had not discovered this until I had this unique use case.

What does work

So it appears the only way to do this is inconsistentYou have to do it in the form rendering code inside the template. Like this

{{ form_row(registrationForm.ajaxString, { 'id': 'ajaxString'}) }}

You can also change/add other attributes this way, but you can ONLY CHANGE THE ID THIS WAY.

Otherwise Symfony takes it upon itself to name the field for you and ignore your request.

Dictating like…

It would be much easier and consistent if I could just add the ID in the FormType definition class instead of having to add it to the template. Class and other attributes can be added/changed like this, but not id. Just a little confusing that is all.

Categories
Software Development Web Development

Symfony 5+ Twig templates don’t forget to call the parent

Twig templates use inheritance and allows you to create named sections like this.


<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <link rel='icon' href="{{ asset('images/favicon.ico') }}" type='image/x-icon' >
    {% block head_extra %}{% endblock %}
    <title>{% block title %}{{ title }}{% endblock %}</title>
    {% block stylesheets %}
        {# 'app' must match the first argument to addEntry() in webpack.config.js #}
        {{ encore_entry_link_tags('app') }}
    {% endblock %}
</head>
<body>

<main>
    <div class="container">
       {% block navbar %}{{ include('_nav_bar.html.twig') }}{% endblock %}
        {% block body %}{% endblock %}
        {% block javascripts %}
            {# these files are needed for getting url routes in javascript  #}
            <script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
            <script src="{{ path('fos_js_routing_js', { callback: 'fos.Router.setData' }) }}"></script>
            {# 'app' must match the first argument to addEntry() in webpack.config.js #}
            {{ encore_entry_script_tags('app') }}
        {% endblock %}

        {% block javascript_extra %}{% endblock %}
    </div>
</main>
</body>
</html>

And then you can reuse/inherit this template in another template like this.

{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}

{% block stylesheets %}
{{ parent() }}

{% endblock %}
{% block body %}

{{ 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.agreeTerms) }}

{{ form_end(registrationForm) }}

{% endblock %}
{% block javascript_extra %}

{{ encore_entry_script_tags('registration') }}

{% endblock %}

Notice the extends keyword and notice this section

{% block stylesheets %}
{{ parent() }}

{% endblock %}

Notice the use of the “parent” keyword here.

{% block stylesheets %} {{ parent() }} {% endblock %}

Any other css code you want included must come after the parent call like this.


{% block stylesheets %}
    {# 'app' must match the first argument to addEntry() in webpack.config.js #}
    {{ parent() }}
    {{ encore_entry_link_tags('editAboutUser') }}
    {{ encore_entry_link_tags('sogiDraw') }}
{% endblock %}

If I didn’t call “parent()” above then only the CSs for editAboutUser and sogiDraw would be included app would not be included. The CSS for app which is called in the base template in the stylesheets section would not be included without the call to parent().

So if some of your CSS is not working, then the reason is you probably forgot to call parent. This call to parent is saying “Include what was in the parent stylesheet section”. Otherwise without the call you will be missing Styles and be saying WTF?

Don’t forget to call parent
Categories
Web Development

Symfony 5+ how to include page specific javascript or css when using Webpack encore and SASS

In this article I will cover how to do this with Webpack in Symfony with CSS and SASS. This is slightly confusing.

Symfony has it’s own Webpack configuration called encore. Read that documentation article if you need more info, more links at the bottom of the page.

Webpack Config

First lets checkout the file app\webpack.config.js

Mine currently looks like this.


const Encore = require('@symfony/webpack-encore');

// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
    Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}

Encore
    // directory where compiled assets will be stored
    .setOutputPath('public/build/')
    // public path used by the web server to access the output path
    .setPublicPath('/build')
    // only needed for CDN's or sub-directory deploy
    //.setManifestKeyPrefix('build/')

    /*
     * ENTRY CONFIG
     *
     * Add 1 entry for each "page" of your app
     * (including one that's included on every page - e.g. "app")
     *
     * Each entry will result in one JavaScript file (e.g. app.js)
     * and one CSS file (e.g. app.scss) if your JavaScript imports CSS.
     */
    .addEntry('app', './assets/js/app.js')
    .addEntry('modalAction', './assets/js/modal-call-to-action.js')
    .addEntry('registration', './assets/js/registration.js')
    .addEntry('featuredImage','./assets/js/featured-image.js')
    .addEntry('editAboutUser','./assets/js/edit-about-user.js')
    .addEntry('sogiDraw', './assets/js/sogi-draw.js')
    // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
    .splitEntryChunks()

    // will require an extra script tag for runtime.js
    // but, you probably want this, unless you're building a single-page app
    .enableSingleRuntimeChunk()

    /*
     * FEATURE CONFIG
     *
     * Enable & configure other features below. For a full
     * list of features, see:
     * https://symfony.com/doc/current/frontend.html#adding-more-features
     */
    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    // enables hashed filenames (e.g. app.abc123.css)
    .enableVersioning(Encore.isProduction())

    // enables @babel/preset-env polyfills
    .configureBabelPresetEnv((config) => {
        config.useBuiltIns = 'usage';
        config.corejs = 3;
    })

    // enables Sass/SCSS support
    .enableSassLoader()

    // uncomment if you use TypeScript
    //.enableTypeScriptLoader()

    // uncomment to get integrity="..." attributes on your script & link tags
    // requires WebpackEncoreBundle 1.4 or higher
    //.enableIntegrityHashes(Encore.isProduction())

    // uncomment if you're having problems with a jQuery plugin
    .autoProvidejQuery()

    // uncomment if you use API Platform Admin (composer req api-admin)
    //.enableReactPreset()
    //.addEntry('admin', './assets/js/admin.js')
;

module.exports = Encore.getWebpackConfig();

See each of the addEntry() lines. Each one of those is including  the processing of the Javascript files. If you need some CSS for example on every page you would put a line like the following inside the app.js file at the top to import the CSS.

import {DrawingForm} from "../javascript/templates/drawing-form";
import {DrawingActions} from "../javascript/objects/DrawingActions";
import '/assets/css/dialog/styles.scss';

In the last line here I am including styles.scss with an import statement. The .scss indicates the file is a SASS file.

This mixing of JS and CSS imports gets to be REALLY confusing. You see just importing the CSS into the Javascript is not enough. Nope you must also include the CSS and the Matching Javascript files with tags.

If you just link to the Javascript that is not enough and you will see your JS elements don’t have any related CSS. Importing the CSS just tells Webpack to process it and create another file.

homer simpson meme
Many baby steps to get to your CSS

So the import statement inside the Javascript file for the .scss file tells webpack to take that import along with any other .scss imports it finds and combine them all into one .css file. If there are no .scss inports there will be no matching .css file created.

All of this is supposed to make web development easier.

Modern web development keeps getting easier by the day
Look at the SASS files

Now lets look inside the styles.css really quick to see what it is doing.

@import 'variables';
@import 'dialog-search-form';
//@import 'grids';
@import 'forms';
@import 'overlay';
@import 'dialog';
@import 'canvas-color-picker';
@import 'editor-canvas';
@import 'image-selector';

.content {
    margin: 55px auto 0 auto;
    background-color: #fff;
    width: 95%;
    height: auto;
}
.hidden {
    display: none;
}

.clear-fix:after {
    content: "";
    display: table;
    clear: both;
}

#drawingCanvas{
    background-color: #fff;

}
.canvas-border, #drawingCanvas {
    border: 2px solid #000;
    margin: 0px auto;
    display: block;
}
.center-image {
    text-align: center;
    margin: 0px auto;
}

Notice all of the import statements. This is because I am using SASS. Each of those files contains a small amount of SCSS and Webpack uses SASS to compile it all into a  single file in the end. The import statement lets Webpack do tree shaking and remove unused code.

borat nice meme
Very nice!!!

So to include a page specific CSS you should create a javascript file inside any folder, this is what gets confusing.( you can use an existing JS file) I keep mine in assets\css\ sometimes in further folders like Dialog or Overlay etc. Then inside that file you import the CSS file like this

import ‘/assets/css/dialog/styles.scss’;

replacing styles.scss with your stylesheet or SASS file name.

But that is not all. You still need to include the final css file that webpack creates and saves in public\build\ folder in your template.

If you imported anything in the .scss file webpack compiles it and includes it in the matching .css file. Webpack will create a matching JS and CSS file based on your addEntry() method calls and store them in the build folder.

So you never make changes, EVER to the files in public\build you store all of your files in the assets folder and add an entryPoint definition in the Webpack Encore file.

So to get your CSS working, (actually included in the page) you need to add a line like this in the template. In your templates head section add  a line to include the CSS file like this.

{{ encore_entry_link_tags('styles') }}

That line is in my base.html.twig file which is the main page template that all of my other page templates inherit from.  So public\build\app.css will be available to every page in my app. Notice it does not include the file type. Twig template knows what to do since encore_entry_link_tags is a twig function.

Page specific CSS only.

So to summarize. If you wanted a CSS file included in only one page of your app, you would do the following. This is a dialog I want to include in only one page of my application.

Step #1 create the css

Create a css file in app\assets\css\dialog\dialog.scss directory for example. In my case I will be using SASS so I will create app\assets\css\dialog\dialog.scss Inside there I import the other tiny SASS .scss files I need.

Step #2 create the javascript

Create a Javascript file or import the css in an existing Javascript file. This is just how Webpack works, you need to include your CSS in your JS files and it does everything else (tree shaking), at least for now. So I create the Javascript file app\assets\js\dialog.js Inside that file I import the css like this

import '/assets/css/dialog/dialog.scss';

Now with this in place, Webpack will compile both the css and the javascript from that file and save it in app\public\build\dialog.css and app\public\build\dialog.js These are the files we must include with template specific functions, not the .scss files. (show below) Webpack will take all .scss import calls and do tree shaking to only include the code that is used.

Step #3 register the Entry point

Now you must register dialog.js (whatever your Javascript file name is) inside the webpack.encore.js file shown above add a line like this.

.addEntry('my-page', './assets/js/dialog.js')

Now restart webpack. In my case I am running webpack with watch so I use the following after I ctrl + c to stop the current watch

yarn run encore dev --watch

Webpack Encore will now create the new Javascript and CSS imported in it to the matching file names inside the public\build\ directory.

Now you must include both the Javascript and the CSS files. This is what tripped me up. I was like WTF why is the related CSS not working but the javascript is?

Uhm, wait… what?

Step #4 include the CSS file

So this is one of the most important steps. You have to include the CSS file located in the public\build\ directory, in the head area of your Twig template like this.

{{ encore_entry_link_tags('dialog') }}

Notice there is no .css it is just the name of the file. also note encore_entry_link_tags is used for the CSS only and javascript has it’s own function covered next. This CSS file was created by Webpack thanks to it being imported in dialog.js. The addEntry() function will create the Javascript file plus a CSS file for each import statement it finds. It stores all the imported scss in a single file that matches the javscript file name but with .css tile type.

Step #5 include the Javascript

Now you must include the matching Javascript that uses the imported CSS file. This is the Javascript Webpack Encore created due to the addEntry() line in the Webpack configuration above.

{{ encore_entry_script_tags('dialog') }}

This should be placed in the section where you include all of your Javascripts, mine are included before the closing body tag. Notice this looks exactly the same as including the CSS, except for the function name is now encore_entry_script_tags

I only have a few page specific Javascripts.  Which is why I can never remember this process.

Next up “Don’t forget to call your templates parent” More fun I discovered while using Twig templates.

Links

Learn how to activate SASS in Symfony webpack encore.

Managing CSS and Javascript from Symfony Documentation.

How to activate CSS preprocessors in Symfony 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.

Categories
rants

Why Amazon search sucks and how it can be fixed

A while back Amazon search used to work really well. But now it sucks. Why? Well a video is worth more than 1,000,000 words.

First off note the difference in result count, 658 vs 3000  when I choose to sort by price low to high. It seems like 3000 is a hard coded random number not an actual real result.

You will never ever be able to view 3,000 products, this search is FAKE.

Also notice once I switch to lowest to highest, all of the  sellers with their unrelated garbage pops up. So now you get page after page after page of non-relevant garbage results.

awesome sauce meme
Isn’t this just awesome

So if you don’t filter you get pages of randomly priced items. That is never helpful to me on any site EVER. That pretends like the price doesn’t matter. How can anyone find what they are looking for if they don’t compare prices? As prices go up, objects often get more features. Plus what kind of sense does it make to have $38 next to or after $299 or vice versa?

And WTF is with letting all the unrelated items that have nothing to do with dehumidifiers? Do you really think I am looking for shirts and plugs and random crap when I type “dehumidifier”?

amazon search meme
Amazons search algorithm is all like

And don’t you just love how it stops at page 7? THAT IS 100% TOTALLY AWESOME PROGRAMMING THERE AMAZON. So now you get less results than if you didn’t filter, and the results you get are 100% UTTER SHIT. Hell even if you don’t choose any of the filters, you get 7 pages of results. THAT IS IT. WTF? WOW REALLY GUYS?

Searching on Amazon is fun
How to fix this

I have no idea how Amazon is designed. It is probably a technological mess of one piece added onto another in a patch quilt type manner. Their programmers are not that great. Any developer that tries AWS services will tell you working with that mess is a nightmare. You get moronic error messages and much more. High school kids could do better at this point.

It would be nice though if they could get these features working, since they have no real competition. I mean more than 7 pages of search results would be nice. Why show 3k search results but only allow pagination of 7 pages for a total of less than 400 products?

Makes sense right?