AJAX requests with CakePHP's SecurityComponent

For one of my current CakePHP projects, I am builing an invoice module, in which I want the users to be able to add new invoice lines to an invoice using AJAX requests, so the page doesn't have to be reloaded for every new line.

The problem

In itself, that's not a very hard task. But with Cake's SecurityComponent with CSRF protection enabled, this becomes a different story. You see, the SecurityComponent generates a token for each form that is created with the FormHelper, to prevent CSRF attacks. This becomes problematic when you want to allow a user to use the same form more then once (in my case, have them add more than 1 invoice line). Because the second time you submit the (same) form with an AJAX request, the SecurityComponent will notice you are using an expired CSRF token and thus blackholes the request (and rightly so!). Now, as you can tell, I am actually pretty fond of this security measure and even though you can disable it for certain controllers or (since CakePHP 2.3) even specific actions within a controller, this didn't feel right to me. I wanted to keep this feature enabled and use it for my AJAX calls as well.

The solution

It took me some tries to figure out there is a pretty simple and effective solution to get the SecurityComponent to allow your form more than once. In my case, I used the jQueryUI modal dialog functionality to present the form to the user. So, I took the content of this dialog (which is basically just the form) and created a seperate view for it (InvoiceLines/add). At first, I was thinking of an element, but the problem is that you can't call an element directly by URL, so I couldn't use jQuery's load() method to retrieve it. After creating the seperate view, I was now able to "reload" the content after a successful submission by using jQuery:

$("#NewInvoiceLineDialog").load("' . $this->webroot .
    'invoice_lines/add/' . $invoice['Invoice']['id'] . '");

By reloading the view, I am firing a new request to the FormHelper and SecurityComponent, making it create a new form with a fresh token that will be accepted! The add action in the InvoiceLines controller can be very simplistic:

public function add($invoice_id) {
        // Use AJAX layout
        $this->layout = 'ajax';

        $this->set(compact('invoice_id'));
    }

And in the view, we just add the form:

<?php
echo $this->Form->create('InvoiceLine', array('action' => 'add'));
echo $this->Form->input('invoice_id', array(
    'type' => 'hidden', 'value' => $invoice_id
));
echo $this->Form->input('amount');
echo $this->Form->input('product_id', array(
    'label' => __('Product')
));
echo $this->Form->input('description');
echo $this->Form->input('price');
echo $this->Form->end();

Then, finally in the success clause of our AJAX call, we reload the form in the dialog box and we're all set. My complete dialog code looked like this in the end:

$this->Js->buffer('
    // Load the view intially, so it is visible for the first line
    $("#NewInvoiceLineDialog").load("' . $this->webroot .
        'invoice_lines/add/' . $invoice['Invoice']['id'] . '");
    $("#NewInvoiceLineDialog").dialog({
        autoOpen: false,
        buttons: {
          "' . __('Add line') . '": function() {
            $.ajax({
              type: "POST",
              // The ajax_add method is just a save()
              // With $this->autoRender = false;
              // (since it has no seperate view).
                url: "' . $this->webroot . 'invoice_lines/ajax_add",
                data: $("#InvoiceLineAddForm").serialize(),
                  success: function() {
                    // To make the user actually see the new line
                    // without reloading, append the HTML
                      $("#lines tbody").append(
                        "<tr>" +
                          "<td>" +
                            $("#InvoiceLineAmount").val() +
                          "</td>" +
                          "<td>" +
                            $("#InvoiceLineProductId").val() +
                          "</td>" +
                          "<td>" +
                            $("#InvoiceLineDescription").val() +
                          "</td>" +
                          "<td>' . CURRENCY_SYMBOL . ' " +
                            $("#InvoiceLinePrice").val() +
                          "</td>" +
                          "<td>' . CURRENCY_SYMBOL . ' " +
                            parseInt(
                              $("#InvoiceLineAmount").val() *
                              $("#InvoiceLinePrice").val()
                            ) +
                          "</td>" +
                        "</tr>"
                      );
                      // Close the dialog
                      $("#NewInvoiceLineDialog").dialog("close");
                      // And here, we load a brand spanking new form
                      // with a fresh token
                      $("#NewInvoiceLineDialog").load("
                        ' . $this->webroot . 'invoice_lines/add/' .
                        $invoice['Invoice']['id'] . '
                      ");
                    }
                  });
                },
                "' . __('Close') . '": function() {
                  $(this).dialog("close");
                }
        },
        closeText: "' . __('Close window') . '",
        hide: "clip",
        modal: true
    });
');

And that's it! I hope it helps others facing the same issue. Like always, drop any questions or comments you might have in the comment box below!