5

We have a Craft site in which we include a partial that includes in several different places. CSRF is enabled, and we're appending {{ getCsrfInput() }} to the form.

Because of the way we've set up the site, and are including this partial in so many places, nearly all of the forms we're rendering are using cached CSRF tokens, which invalidates them.

Is there a way to prevent Craft's {% cache %} tag from including the generated CSRF token without ending the cache and starting it up again each time? (eg, {% endcache %}{{ getCsrfInpt() }}{% cache %})

I worry doing so would be risky, because we may include this partial in a part of the site that is not cached, so there'd be no cache tag to end.

Thanks!

Update: I failed to mention that our site works right now without Javascript, so I'd like to avoid it. Otherwise, Lindsey's answer is perfect.

5 Answers 5

11

You can do something like this:

// First, set all your cached content to a variable:
{% set myContent %}
  {% cache %}
    {# YOUR CACHED CONTENT #}
    %%MY_CSRF%%
  {% endcache %}
{% endset %}

// Then, run a replace on your cached content to insert the new CSRF
{{ myContent|replace({'%%MY_CSRF%%': getCsrfInput()}) }}
3
  • Also, courtesy hat tip to this post for the general idea of the second solution: craftcms.stackexchange.com/questions/4782/… Commented Feb 23, 2016 at 20:51
  • 1
    Removed the first solution since it wouldn't work. But the |replace tag method would definitely work. Commented Feb 24, 2016 at 0:42
  • 1
    I believe the |raw filter is required. See my answer for an example. Commented Apr 9, 2018 at 18:46
9

The trick is to inject the CSRF field using JavaScript.

  1. Omit the standard getCsrfInput Twig tag.
  2. Give your form(s) the class of csrf.
  3. Add this Twig code outside of the cached area:
{% includejs %}
    $(function() {
        $('form.csrf').prepend('<input type="hidden" name="{{ craft.config.get('csrfTokenName') }}" value="{{ craft.request.getCsrfToken }}" />');
    });
{% endincludejs %}

This will dynamically inject the correct CSRF input field into every form with the csrf class.


* Disclaimer: This example assumes you're using jQuery. A similar solution can be enacted without jQuery, but some adjustments would need to be made.

1
  • This is IMHO the prettiest solution.
    – KSPR
    Commented Oct 3, 2019 at 6:45
3

Further to Ben’s answer, it may be inconvenient to set and replace every time the partial with the CSRF token is required, in which case the replacing can instead be done in one go for each page’s content in the layout.

_some_partial.twig:

<form method="post" action="" accept-charset="UTF-8">
    ${getCsrfInput}
    # ...
</form>

some_page.twig:

{% block content %}
    {% include '_some_partial.twig' %}
{% endblock %}

_layout.twig:

{{ block('content')|replace({'${getCsrfInput}': getCsrfInput()})|raw }}
2

I was just looking for a way to do something similar without Javascript. Someone has released a cache escaping extension giving you the ability to wrap content you want esacped with a {% nocache %} tag.

Basic example:

{% cache %}
    This will be cached
    {% nocache %}
        This won't be
    {% endnocache %}
{% endcache %}

Extension: https://github.com/benjamminf/craft-nocache

0

Thanks Lindsey and Ben!

I was hoping to avoid using Javascript to handle CSRF, because our site otherwise is not dependent on it. I took Ben's suggestion of creating a variable outside of the cache, and using it within the cache. I ran a quick test to verify that it indeed would use the uncached value.

I wound up making a Twig extension to get uncached CSRF input, which we can set in our _layout.html:

{# in craft/templates/_layout.html #}
{% set csrfToken = craft.request.getCsrfToken() %}

Then we build the twig extension to access the csrfToken from the Twig context, and render a hidden input element:

// craft/plugins/ourplugin/twigextensions/OurPluginTwigExtension.php

public function getFunctions() {
    return [
        'getUncachedCsrfInput' => new \Twig_Function_Method($this, 'getUncachedCsrfInput', ['needs_context' => true])
    ];
}

public function getUncachedCsrfInput($context) {
    if (craft()->config->get('enableCsrfProtection') === true) {
        $charset = craft()->templates->getTwig()->getCharset();
        //access the csrfToken variable from the Twig context,
        //    or fall back on craft()->request otherwise
        $csrf = array_key_exists('csrfToken', $context) ? $context['csrfToken'] : craft()->request->getCsrfToken();

        return new \Twig_Markup('<input type="hidden" name="'.craft()->config->get('csrfTokenName').'" value="'. $csrf .'">', $charset);
    }
    return '';
}

Then, we can get an uncached <input /> element in all of our forms using:

{# any form #}
<form action="" method="POST">
    ...
    {{ getUncachedCsrfInput }}
    ...
</form>

Thanks for your help!

1
  • 1
    If {{ getUncachedCsrfInput() }} is happening inside a {% cache %} tag, this won't work since the {% cache %} tag will cache its entire output and use that cached output on subsequent requests (and each user session gets its own CSRF token value). Also not sure what the point of all this added complexity over just using getCsrfInput(). Commented Feb 24, 2016 at 0:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.