diff --git a/src/parse.rs b/src/parse.rs index 71f6cd8d..a9953bef 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -470,6 +470,7 @@ pub enum Tag { Load, SimpleTag(SimpleTag), Url(Url), + CsrfToken, } #[derive(PartialEq, Eq)] @@ -1046,6 +1047,7 @@ impl<'t, 'l, 'py> Parser<'t, 'l, 'py> { }; Ok(match self.template.content(tag.at) { "url" => Either::Left(self.parse_url(at, parts)?), + "csrf_token" => Either::Left(TokenTree::Tag(Tag::CsrfToken)), "load" => Either::Left(self.parse_load(at, parts)?), "autoescape" => Either::Left(self.parse_autoescape(at, parts)?), "endautoescape" => Either::Right(EndTag { diff --git a/src/render/tags.rs b/src/render/tags.rs index ed3cda52..ffc60402 100644 --- a/src/render/tags.rs +++ b/src/render/tags.rs @@ -654,6 +654,39 @@ impl Render for Tag { Self::Load => Cow::Borrowed(""), Self::SimpleTag(simple_tag) => simple_tag.render(py, template, context)?, Self::Url(url) => url.render(py, template, context)?, + Self::CsrfToken => match context.get("csrf_token") { + Some(token) => { + let bound_token = token.bind(py); + if bound_token.is_truthy().unwrap_or(false) { + if bound_token.eq("NOTPROVIDED")? { + Cow::Borrowed("") + } else { + Cow::Owned(format!( + r#""#, + html_escape::encode_quoted_attribute(bound_token.str()?.to_str()?) + )) + } + } else { + Cow::Borrowed("") + } + } + None => { + let debug = py + .import("django.conf")? + .getattr("settings")? + .getattr("DEBUG")? + .is_truthy()?; + + if debug { + py.import("warnings")?.call_method1( + "warn", + ("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.",) + )?; + } + + Cow::Borrowed("") + } + }, }) } } diff --git a/tests/tags/test_csrf_token.py b/tests/tags/test_csrf_token.py new file mode 100644 index 00000000..e9af7fec --- /dev/null +++ b/tests/tags/test_csrf_token.py @@ -0,0 +1,133 @@ +import warnings +from django.template import engines +from django.test import override_settings + + +def test_csrf_token_basic(): + template = "{% csrf_token %}" + + django_template = engines["django"].from_string(template) + rust_template = engines["rusty"].from_string(template) + + context = {"csrf_token": "test123"} + expected = '' + + assert django_template.render(context) == expected + assert rust_template.render(context) == expected + + +def test_csrf_token_not_provided(): + template = "{% csrf_token %}" + + django_template = engines["django"].from_string(template) + rust_template = engines["rusty"].from_string(template) + + context = {"csrf_token": "NOTPROVIDED"} + + assert django_template.render(context) == "" + assert rust_template.render(context) == "" + + +def test_csrf_token_missing(): + template = "{% csrf_token %}" + + django_template = engines["django"].from_string(template) + rust_template = engines["rusty"].from_string(template) + + assert django_template.render({}) == "" + assert rust_template.render({}) == "" + + +def test_csrf_token_escaping(): + template = "{% csrf_token %}" + + django_template = engines["django"].from_string(template) + rust_template = engines["rusty"].from_string(template) + + context = {"csrf_token": 'test"with&'} + + django_result = django_template.render(context) + rust_result = rust_template.render(context) + + assert django_result == rust_result + assert 'value="test"with<quotes>&amp;"' in rust_result + + +def test_csrf_token_empty_string(): + template = "{% csrf_token %}" + + django_template = engines["django"].from_string(template) + rust_template = engines["rusty"].from_string(template) + + context = {"csrf_token": ""} + + assert django_template.render(context) == "" + assert rust_template.render(context) == "" + + +def test_csrf_token_none_value(): + template = "{% csrf_token %}" + + django_template = engines["django"].from_string(template) + rust_template = engines["rusty"].from_string(template) + + context = {"csrf_token": None} + + assert django_template.render(context) == "" + assert rust_template.render(context) == "" + + +def test_csrf_token_numeric_value(): + template = "{% csrf_token %}" + django_template = engines["django"].from_string(template) + rust_template = engines["rusty"].from_string(template) + + context = {"csrf_token": 12345} + expected = '' + + assert django_template.render(context) == expected + assert rust_template.render(context) == expected + + +def test_csrf_token_zero_value(): + template = "{% csrf_token %}" + + django_template = engines["django"].from_string(template) + rust_template = engines["rusty"].from_string(template) + + context = {"csrf_token": 0} + + assert django_template.render(context) == "" + assert rust_template.render(context) == "" + + +def test_csrf_token_false_value(): + template = "{% csrf_token %}" + + django_template = engines["django"].from_string(template) + rust_template = engines["rusty"].from_string(template) + + context = {"csrf_token": False} + + assert django_template.render(context) == "" + assert rust_template.render(context) == "" + + +@override_settings(DEBUG=True) +def test_csrf_token_missing_debug_warning(): + template = "{% csrf_token %}" + + django_template = engines["django"].from_string(template) + rust_template = engines["rusty"].from_string(template) + + expected = "A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext." + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert django_template.render({}) == "" + assert str(w[0].message) == expected + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert rust_template.render({}) == "" + assert str(w[0].message) == expected