2

After a thorough search im not seeing the problem.
I have a form im submitting, which isnt tied to an object, its just an emailer form. I want to validate the data. according to the docs the alternative way is to do this.

use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

$builder
   ->add('firstName', 'text', array(
       'constraints' => new Length(array('min' => 3)),
   ))
   ->add('lastName', 'text', array(
       'constraints' => array(
           new NotBlank(),
           new Length(array('min' => 3)),
       ),
   ))
;

now thats done, but i CANNOT call $form->isValid on this in any shape or form (pun intended), because even though it passes the constraint violations, something seemingly invisible is still causing it to return invalid.

i feel like i might need to extract the form from the post first, and pass that through isValid() but i cant be sure.

heres my method code

/**
 * @Route("/share", name="email_share")
 * @Method({"POST"})
 * @return \Symfony\Component\HttpFoundation\Response
 */
public function shareAction( Request $request, array $lessons){

    if(!$lessons || !is_array($lessons))
    {
        throw new HttpException(404, "Whoops! shareAction didnt get the lessons");
    }

    //set up our form defaults here
    $defaultData = array('comment' => 'Type your comment here');

    //build the form here
    $form = $this->createFormBuilder($defaultData)
        ->setAction($this->generateUrl('email_share'))
        ->add("emails", 'email', array(
            'label' => "Recipient's email address (separate with a comma)",
            'constraints' => array(
                new Length(array('min' => 6, 'max' => 2040)), 
                new NotBlank(),
            ),
        ))
        ->add('comment', 'textarea', array(
            'label' => "Leave a comment",

        ))
        ->add('name', 'text', array(
            'label' => "Your name",
            'constraints' => array(
                new Length(array('min' => 3), 'max' => 254)),
                new NotBlank(),
            ),
        ))
        ->add('email', 'email', array(
            'label' => "Your email address",
            'constraints' => array(
                new Length(array('min' => 6, 'max' => 254)),
                new NotBlank(),
            ),
        ))
        ->add('copy', 'checkbox', array(
            'label' => "Send me a copy",
            'required' => false,
        ))
        ->add('cancel', 'submit', array(
            'label' => "Cancel",
        ))
        ->add('save', 'submit', array(
            'label' => "Email Resources",
        ))
        ->getForm();

    if ($this->getRequest()->isMethod('POST')) {

        //data is already validated by constraints added when the form was created since we are not attaching this particular form to any object
        $form->handleRequest($request);

        //alternatively (makes no differene from the former)
        //$form->submit($request->request->get($form->getName()));

        if ($form->isValid()) 
        {
            //have YET to see this
            echo 'valid';
            exit;
        }
        else{

            //echo 'not fuckin valie, WHY?';
            //exit;

            // get a ConstraintViolationList
            $errors = $this->get('validator')->validate( $form );

            $result = '';

            //nothing returns here when form is valid against constraints, its just empty
            echo $errors;

            // iterate on it
            foreach( $errors as $error )
            {
                $error->getPropertyPath() : the field that caused the error
                $error->getMessage() : the error message

            }
        }

        $data = $form->getData();

        return $this->emailUser($data);
    }

    return $this->render('ResourceBundle:Default:resources.html.twig', array(
        'form' => $form->createView(),
    ));
}

This is how im posting the data

function postForm($form, callback) {

    /*
     * Get all form values
     */
    var values = {};
    $.each($form.serializeArray(), function (i, field) {
        values[field.name] = field.value;
    });

    /*
     * Throw the form values to the server!
     */
    $.ajax({
        type: 'POST',
        url: '/share',
        data: values,
        success: function (data) {

            callback(data);
        }
    });
}

$(document).ready(function () {
    //bind an event to submit on 'email resources' button
    $('div#share form').submit(function (e) {

        //disable symfonys default submit button hehaviour
        e.preventDefault();

        postForm($(this), function (response) {
            //replace html here
            // Is this where im going wrong? Do i need to replace the form here?
        });
    });
});

EDIT: Here is the pertinent portion of the main template code that calls the action in the first place

<div id="share" class="hidden" >
    <h2>Share Resources</h2>
    {% render url('email_share') %}
</div>

here is the form template code thats rendered in the shareAction (in its entirety currently)

{{ form(form) }}

was

{% if form | default %}
    {{ form(form) }}
{% endif %}
{% if mail_response | default %}
    {{ dump(mail_response) }}
{% endif %}

The hidden token input portion of the form

<input id="form__token" class="form-control" type="hidden" value="8QWLo8xaPZFCKHBJbuc6CGNIcfmpWyT-yFdWScrsiJs" name="form[_token]">

The two underscores worry me a bit (form__token)

EDIT2: The problem is in the CSRF token somewhere. it could be the form input name, the token itself is already expired, or something else.

I pretty much narrowed it down by constructing my own form module like this

    //set up our form defaults here
    $defaultData = array('comment' => 'Type your comment here');

    $session = new Session();
    $secret = '123xyz';
    $vendorDir = realpath(__DIR__ . '/../vendor');
    $vendorFormDir = $vendorDir . '/symfony/form/Symfony/Component/Form';
    $vendorValidatorDir =
    $vendorDir . '/symfony/validator/Symfony/Component/Validator';

    // create the validator - details will vary
    $validator = Validation::createValidator();

    $formFactory = Forms::createFormFactoryBuilder()
        ->addExtension(new HttpFoundationExtension())
        //->addExtension(new CsrfExtension(new SessionCsrfProvider($session, $secret)))
        ->addExtension(new ValidatorExtension($validator))
        ->getFormFactory();

    //build the form here
    $form = $formFactory->createBuilder('form', $defaultData)
        ->setAction($this->generateUrl('email_share'))
        ->setMethod('POST')
        ->add("emails", 'email', array(
    //......
    //same as above for the rest......

The form FINALLY passes validation like this, and when i uncomment the line ->addExtension(new CsrfExtension(new SessionCsrfProvider($session, $secret))) i get the same error as i did before, that the CSRF token is invalid.

To me, this is pretty much pointing to somewhere in this module, or im not calling something, or extending something right, or the javascript is returning a form that is older than the CSRF module is expecting, or the hidden token form input has a name that is other than that what the CSRF module is looking for. I dont know enough about symfony's internals to diagnose this, this is why i come here for help. Does anybody see a potential issue?

EDIT:3 i feel like i shouldnt be using isValid(), as mentioned, i am not passing an object, im passing an array. see this URL http://symfony.com/doc/current/book/validation.html#validating-values-and-arrays. Im trying to figure out how to properly check against the constraints, and im thinking isValid() is NOT the way to go after all, or else im missing something fundamental.. I just cant figure if i only check against constraint errors, how can i use the CSRFprotection still, or is that only for objects or something?? Do i need to pass this in manually since im not using an object?

EDIT 4: It looks like i might have uncovered the crux of the problem, yet i cant figure out how to solve it yet.

on this file Symfony\Component\Form\Extension\Csrf\CsrfProvider\DefaultCsrfProvider i put some output to track the token, and it appears that the token is regenerating for the comparison, which seems like IT SHOULD NOT be the case I would expect that it would compare the passed down token to one in memory, but in fact, its being generated twice, once for the form, then once again for the comparison.

At first i suspected it was possible that the browser and the ajax are running from two different sessions, and a mismatch can be caused by this because i was using SessionCsrfProvider(), but after switching to ->addExtension(new CsrfExtension(new DefaultCsrfProvider($secret))) i had the same problem.

Is this a bug, am i going crazy, or am i missing something as simple as the form id in the building of the form or something?

heres the code, and the results i found from that code.

//Symfony\Component\Form\Extension\Csrf\CsrfProvider\DefaultCsrfProvider

public function isCsrfTokenValid($intention, $token)
{

    echo '<pre>Warning, Symfony\Component\Form\Extension\Csrf\CsrfProvider\isCsrfTokenValid';
    echo'<br>, here is out token handed down to compare<br>';
    var_dump($token);
    echo '<br>the new generated token thats being compared to is<br>';
    var_dump($this->generateCsrfToken($intention));
    echo '</pre>';

    return $token === $this->generateCsrfToken($intention);
}

returns

//the form array(6) { ["emails"]=> string(19) "email@email.net"
["comment"]=> string(2) "yo" ["name"]=> string(5) "me"
["email"]=> string(19) "email@email.net" ["copy"]=>
string(1) "1" ["_token"]=> string(40) "a11e10eb323f7a4d19577e6d07e68be951ceb569" }

Warning, Symfony\Component\Form\Extension\Csrf\CsrfProvider\isCsrfTokenValid , here is out token handed down to compare string(40) "a11e10eb323f7a4d19577e6d07e68be951ceb569"

the new generated token thats being compared to is string(40) "e83cdf94b15e63e822520b62402eb66e0b1f03d3"

The CSRF token is invalid. Please try to resubmit the form.

Blockquote

EDIT 5: the problem has been traced to here, look at this code in the DefaultCsrfProvider

public function generateCsrfToken($intention)
{
    return sha1($this->secret.$intention.$this->getSessionId());
}
public function isCsrfTokenValid($intention, $token)
{
    return $token === $this->generateCsrfToken($intention);
}

The token can never be valid during an ajax call, unless a param is set in the generateCsrfToken() token method to allow passing of the session, to which you would want to pass that via ajax, like this

public function generateCsrfToken($intention, $session)
{
    if(!$session)
    { 
        $session = $this->getSessionId()
    }
        return sha1($this->secret.$intention.$session);
}

which i would think would completely reduce teh security of the whole idea of the CSRF in the first place.

is there another provide i can use for ajax calls in within the symfony framework? If were depending on the sesion, this pretty much leaves out both the SessionCsrfProvider class and the DefaultCsrfProvider class to process this, unless im missing something very obvious... should i just grab, pass, then reset the session on the ajax call????
Ok, after i figured this out thus far, i just found this post Symfony CSRF and Ajax ill see if i can make heads or tails from it.

Community
  • 1
  • 1
blamb
  • 4,220
  • 4
  • 32
  • 50
  • i just found this post, which seems to touch on the CSRF problem im having. could it be because im running my views as a SPA? http://stackoverflow.com/questions/15922531/symfony2-and-single-webpage-applications-using-a-framework-like-angularjs?answertab=votes#tab-top – blamb Aug 02 '14 at 01:33

1 Answers1

2

To see errors you should render that form. In your code when form is invalid the method returns $this->emailUser($data); but it should render the form. Try this:

if ($this->getRequest()->isMethod('POST')) {

    //data is already validated by constraints added when the form was created since we are not attaching this particular form to any object
    $form->handleRequest($request);

    //alternatively (makes no differene from the former)
    //$form->submit($request->request->get($form->getName()));

    if ($form->isValid()) 
    {
        //have YET to see this
        echo 'valid';

        //this is the place to process data 
        //$data = $form->getData();

        //return $this->emailUser($data);

        exit;
    }

}

return $this->render('ResourceBundle:Default:resources.html.twig', array(
    'form' => $form->createView(),
));

That should render invalid form again on submit and show error

You can also try this

    if ($form->isValid()) 
    {
        //have YET to see this
        echo 'valid';
        exit;
    }else{
        echo '<pre>';
        \Doctrine\Common\Util\Debug::dump($form->getErrorsAsString(), 9);
        echo '</pre>';
    }

To show errors in debug style if form is invalid.

pazulx
  • 2,369
  • 1
  • 17
  • 17
  • ok i failed to mention, which is now seemingly relative, that im calling this action via an ajax call. when i do your first suggestion, in the ajax response, i get "CSRF token not valid". When i do your 2nd suggestion i get "string(215) "ERROR: The CSRF token is invalid. Please try to resubmit the form." HOw do i pass the correct token to ajax? ill update my original post to include how im passing the data. – blamb Apr 17 '14 at 18:30
  • by the way, i know the returning of the data needs to go inside the isValid() check, i just put it below to get things working for now. sorry for not pointing that out. – blamb Apr 17 '14 at 18:45
  • try to do use serialize() instead of serializeArray() in your script, like "data: $form.serialize()" – pazulx Apr 17 '14 at 19:03
  • thanks for pointing that out, i forgot that i left it to serializeArray(). removing the setting of the values array, and just putting data: $form.serialize(), although works the same, still results in CSRF token invalid in ajax result. – blamb Apr 17 '14 at 19:52
  • do you render form by using {{ form(form) }} – pazulx Apr 17 '14 at 19:54
  • this is the data' before the callback `value="mzrHJlU798bkBnPCFMhe2UAyvDKkUJ4iD7GIiUuwUM4"` the form response `value="mzrHJlU798bkBnPCFMhe2UAyvDKkUJ4iD7GIiUuwUM4"` cant figure why its invalid. – blamb Apr 17 '14 at 19:55
  • the $('form').serialize() should return somthing like this: somefield=xxx&_csrf_token=991dbe4a785ec4b1046e34dea029db7dbd5a8a7c – pazulx Apr 17 '14 at 19:57
  • really? imjust seeing the form itself, as posted. when ran like this `$.ajax({ type: 'POST', url: '/share/resources', data: $form.serialize(), success: function (data) { callback( data ); } });` – blamb Apr 17 '14 at 19:58
  • what is "mail_response"? – pazulx Apr 17 '14 at 20:14
  • mail_response is the return of the method ran here `$this->emailUser($data);` after the mail is sent successfully. so for now its only a sentence `$response = 'set up a response message here';` which is returned, and i DO see that when i run the emailUser() outside of the isValid() call, but never gets to that point when ran from within the isValid() call. essentially tghe problem seems symfony is getting the from CSRF token, or is caching the wrong one or something.... any ideas there? – blamb Apr 17 '14 at 21:25
  • notice, theres no `return` on the ajax, i added a return there, and added a success function to the end of the postForm() call `postForm().success()`, and inside that i did a `replaceWith()` on the form div, and i now see the result from form, with the CSRF error, but this is not the problem, the problem is why is the CSRF mismatching? i wish i could paste my latest code here somewhere, without overloading the comments any worse. should i add it to the OP? – blamb Apr 17 '14 at 21:52
  • does anything look wrong with the code here on the hidden token input? See OP for latest code additions.. – blamb Apr 17 '14 at 22:10
  • Ok i traced the problem right back to teh session, how is one supposed to make an ajax post if the CSRF token is generated on the fly from `$this->getSessionId()` when it checks if valid `isCsrfTokenValid()`. is this a bug? look at the code i added in teh OP to see where the problem is coming from. i need a solution to fix it if anyone sees it... – blamb Apr 18 '14 at 17:49
  • just a note, i couldnt solve this problem, im definitely open to suggestions, but im not willing to spend any more time on it unless there is something concrete. it seems i need a CSRF token manager specifically for ajax because the form as loaded via the browser initially loads a token based on session but when submitted via ajax the token is regenerated, using a new session, which it can never match this way. thanks for any help, would be great if someone can chime in. Note, my reputation needs points, any help there is appreciated, i still need 50 to even answer questions. – blamb Apr 19 '14 at 23:12