How to submit form via AJAX and validate CSRF?

bernhard
Member | 40
+
0
-

Hi,

I want to submit a nette form via AJAX and did not find any information on the internet how that can be done… This is what I have so far: Using isValid() works as long as I do not add a CSRF token to the form! This is how I'm checking the form:

$form = ...
$form->addText(...);
...

if($config->ajax) {
  $values = json_decode(file_get_contents('php://input'));
  $form->setValues($values);
  if($form->isValid()) {
    bd('success');
    bd($form->getValues());
    return;
  }
}
echo $form->render();

This is how I send the ajax request (using the uikit js microframework):

let formData = new FormData(form);
let data = Object.fromEntries(formData.entries());
util.ajax("./", {
  responseType: 'json',
  method: 'POST',
  headers: {
    'X-Requested-With': 'XMLHttpRequest',
    'X-_token_': data['_token_'],
  },
  data: JSON.stringify(data),
}).then(function(xhr) {
  let r = xhr.response;
  console.log(r);
}).catch(err => {
  const errStatus = err.response ? err.response.status : 500;
  console.log(err.response);
});

Can anybody point me into the right direction please?

Thx in advance!

Rick Strafy
Nette Blogger | 65
+
0
-

Are you using forms only and not whole nette/application? o.O
Try to look at https://naja.js.org, if you use that, you can add ajax class to your form and it will work.
Btw, use onSuccess callback if you can, for example https://github.com/…resenter.php#L62 (form is created in SnippetFormFactory)

Last edited by Rick Strafy (2021-06-11 16:43)

bernhard
Member | 40
+
0
-

Hi Rick,

thx for your answer! Yes, I forgot that to mention… I'm using Nette Forms standalone combined with the ProcessWire CMS. Thx also for pointing me to naja.js but that seems like overkill if all I want to do is submitting a form via ajax? Also I'm not a JS guru, so all those modern tools are a bit hard to understand – especially without having examples :(

What do you mean with “use onSuccess callback”? What would I use it for? How would that work? I don't understand the syntax $form->onSuccess[] = … :(

Is there a way to send form data via AJAX and validate on the backend? Why would I need an additional framework/library for that??

Sorry, I'm a little lost and would really appreciate some help!

jiri.pudil
Nette Blogger | 1028
+
+1
-

Hi, the CsrfProtection control intentionally ignores the setValue() method.

You should be able to:

  1. Submit the form using the standard application/x-www-form-urlencoded content type. The easiest way to achieve this is to pass the FormData instance directly as the request's data. I don't know if UIkit supports this, but both XMLHttpRequest and Fetch API do, so I would be surprised if UIkit did not.
  2. Then let nette/forms handle the data processing: just remove the $form->setValues() call and replace $form->isValid() with $form->isSuccess() – it checks if the form has been submitted and loads the data from the HTTP request automatically.

Last edited by jiri.pudil (2021-06-15 16:46)

bernhard
Member | 40
+
0
-

thx jiri.pudil!

The first part of your message worked :)

But unfortunately part 2 does not… checking $form->isSuccess() returns false all the time.

Checking the content of php://input shows this: https://i.imgur.com/L4INvbJ.png

So the data is there, but how can I bring that data into the form and check for successful submission?

This is what I'm using on JS – but I think the problem is on the server side…

  // intercept submission of ajax forms
  util.on(document, 'submit', 'form[rf-ajax]', function (e) {
    e.preventDefault();
    let form = e.target;

    // send ajax request
    let formData = new FormData(form);
    console.log(formData);
    let method = util.attr(form, 'method');
    util.ajax("./", {
      responseType: 'json',
      method: method,
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      data: formData,
    }).then(function(xhr) {
      let r = xhr.response;
      console.log(r);
    }).catch(err => {
      const errStatus = err.response ? err.response.status : 500;
      console.log(err.response);
      // UIkit.modal.alert('Fehler beim Laden der Daten - bitte wenden Sie sich an den Support (Fehlercode ajax#'+errStatus+')');
    });;

    return false;
  });

Last edited by bernhard (2021-06-15 18:07)

jiri.pudil
Nette Blogger | 1028
+
0
-

Well, apparently the client-side solution sends the request as multipart/form-data. Try omitting the explicit Content-Type header, I believe the client should be able to populate the header automatically based on what kind of data you pass to it.

Server-side, nette/forms loads the values from $_POST automatically. You don't need to (and really shouldn't) setValues() from stdin. As long as PHP can correctly populate the $_POST superglobal, it should all be working with just the isSuccess() condition I've mentioned before.

bernhard
Member | 40
+
0
-

Hey Jiri :)

Thank you!! That was the problem!! Great. Now I have a super simple setup without any external dependencies and super simple form code on the PHP side :)))

For everybody else interested in this topic this is my working solution:

CLIENT

  // intercept submission of ajax forms having rf-ajax attribute
  util.on(document, 'submit', 'form[rf-ajax]', function (e) {
    e.preventDefault();
    let form = e.target;
    let formData = new FormData(form); // use FormData (not a json string or the like)!
    let method = util.attr(form, 'method');

    // send ajax request to current page
    util.ajax("./", {
      responseType: 'json',
      method: method,
      headers: {
        // set header to make ProcessWire + TracyDebugger ajax bar work
        'X-Requested-With': 'XMLHttpRequest',
      },

      // set formData as request data
      data: formData,
    }).then(function(xhr) {
      let r = xhr.response;
      console.log(r);
    }).catch(err => {
      const errStatus = err.response ? err.response.status : 500;
      console.log(err.response);
    });

    return false;
  });

SERVER

/** @var RockForms $rockforms */
$rockforms = $this->wire->modules->get('RockForms');
$form = $rockforms->form('rueckruf');
$form->setHtmlAttribute("rf-ajax");

$form->addText('phone')
  ->setHtmlAttribute('placeholder', 'Telefon')
  ->setRequired("Bitte geben Sie Ihre Telefonnummer an!");
$form->addText('name')
  ->setHtmlAttribute('placeholder', 'Name (optional)');
$form->addTextArea('comment')
  ->setHtmlAttribute('placeholder', 'Kommentar (optional)');

$form->addSubmit('submit', "Rückruf anfordern");

if($form->isSubmitted()) {
  // echo json response
  if($form->isSuccess()) [...]
  else [...]
}

echo $form->render();

As easy as that :)