From 0531abadc085a15a9a48711ae37d2fab272fed0b Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 13 Sep 2025 01:22:34 -0500 Subject: [PATCH 1/5] Add csrf_token template tag --- src/parse.rs | 2 + src/render/tags.rs | 32 ++++++++++ tests/tags/test_csrf_token.py | 111 ++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 tests/tags/test_csrf_token.py 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..89dc4fb6 100644 --- a/src/render/tags.rs +++ b/src/render/tags.rs @@ -654,6 +654,38 @@ 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) { + let token_str = bound_token.to_string(); + if token_str == "NOTPROVIDED" { + Cow::Borrowed("") + } else { + Cow::Owned(format!( + r#""#, + html_escape::encode_quoted_attribute(&token_str) + )) + } + } else { + Cow::Borrowed("") + } + } + None => { + // TODO: When debug mode is accessible during rendering, emit warning. + // + // Example of what this might look like: + // if engine.debug { // or context.debug (however debug flag is exposed) + // 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..e44f25ec --- /dev/null +++ b/tests/tags/test_csrf_token.py @@ -0,0 +1,111 @@ +from django.template import engines + + +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) == "" From fd0068f2279af5c35e10f676ae01901a6ab40947 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 13 Sep 2025 01:39:29 -0500 Subject: [PATCH 2/5] oops forgot to commit this part --- src/render/tags.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/render/tags.rs b/src/render/tags.rs index 89dc4fb6..98a82c83 100644 --- a/src/render/tags.rs +++ b/src/render/tags.rs @@ -672,17 +672,22 @@ impl Render for Tag { } } None => { - // TODO: When debug mode is accessible during rendering, emit warning. - // - // Example of what this might look like: - // if engine.debug { // or context.debug (however debug flag is exposed) - // 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.",) - // )?; - // } + let debug = py + .import("django.conf") + .and_then(|conf| conf.getattr("settings")) + .and_then(|settings| settings.getattr("DEBUG")) + .and_then(|debug| debug.is_truthy()) + .unwrap_or(false); + + if debug { + if let Ok(warnings) = py.import("warnings") { + let _ = 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("") } }, From 093e59eb667366aae621eceeef67c09b19cfda4a Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 13 Sep 2025 01:55:10 -0500 Subject: [PATCH 3/5] add test --- src/render/tags.rs | 2 +- tests/tags/test_csrf_token.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/render/tags.rs b/src/render/tags.rs index 98a82c83..40f61675 100644 --- a/src/render/tags.rs +++ b/src/render/tags.rs @@ -683,7 +683,7 @@ impl Render for Tag { if let Ok(warnings) = py.import("warnings") { let _ = 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.",) + ("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.",) ); } } diff --git a/tests/tags/test_csrf_token.py b/tests/tags/test_csrf_token.py index e44f25ec..e9af7fec 100644 --- a/tests/tags/test_csrf_token.py +++ b/tests/tags/test_csrf_token.py @@ -1,4 +1,6 @@ +import warnings from django.template import engines +from django.test import override_settings def test_csrf_token_basic(): @@ -109,3 +111,23 @@ def test_csrf_token_false_value(): 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 From aeade44451dc57053c79a239e2c16591e5e1c4ce Mon Sep 17 00:00:00 2001 From: Josh Thomas Date: Tue, 16 Sep 2025 22:54:22 -0500 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Lily Acorn --- src/render/tags.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/render/tags.rs b/src/render/tags.rs index 40f61675..a887fc01 100644 --- a/src/render/tags.rs +++ b/src/render/tags.rs @@ -658,10 +658,10 @@ impl Render for Tag { Some(token) => { let bound_token = token.bind(py); if bound_token.is_truthy().unwrap_or(false) { - let token_str = bound_token.to_string(); - if token_str == "NOTPROVIDED" { + if bound_token.eq("NOTPROVIDED") { Cow::Borrowed("") } else { + let token_str = bound_token.str()?; Cow::Owned(format!( r#""#, html_escape::encode_quoted_attribute(&token_str) @@ -672,20 +672,17 @@ impl Render for Tag { } } None => { - let debug = py - .import("django.conf") - .and_then(|conf| conf.getattr("settings")) - .and_then(|settings| settings.getattr("DEBUG")) - .and_then(|debug| debug.is_truthy()) - .unwrap_or(false); + let debug = py + .import("django.conf")? + .getattr("settings")? + .getattr("DEBUG")? + .is_truthy()?; if debug { - if let Ok(warnings) = py.import("warnings") { - let _ = 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.",) - ); - } + 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("") From ce1b984ccd7ccd08d4d0fac7cea077c83b9e6c68 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 16 Sep 2025 23:07:31 -0500 Subject: [PATCH 5/5] fix CI errors --- src/render/tags.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/render/tags.rs b/src/render/tags.rs index a887fc01..ffc60402 100644 --- a/src/render/tags.rs +++ b/src/render/tags.rs @@ -658,13 +658,12 @@ impl Render for Tag { Some(token) => { let bound_token = token.bind(py); if bound_token.is_truthy().unwrap_or(false) { - if bound_token.eq("NOTPROVIDED") { + if bound_token.eq("NOTPROVIDED")? { Cow::Borrowed("") } else { - let token_str = bound_token.str()?; Cow::Owned(format!( r#""#, - html_escape::encode_quoted_attribute(&token_str) + html_escape::encode_quoted_attribute(bound_token.str()?.to_str()?) )) } } else { @@ -672,11 +671,11 @@ impl Render for Tag { } } None => { - let debug = py - .import("django.conf")? - .getattr("settings")? - .getattr("DEBUG")? - .is_truthy()?; + let debug = py + .import("django.conf")? + .getattr("settings")? + .getattr("DEBUG")? + .is_truthy()?; if debug { py.import("warnings")?.call_method1(