summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-08-09 23:31:35 +0200
committererdgeist <erdgeist@erdgeist.org>2026-08-09 23:31:35 +0200
commit5690cf4d4e05eafdfd2e270bbdf1a925114d0f76 (patch)
tree1315dd1b91433f5287208a4f1b321dd722592966
parent4d8c7290adf8dd41677ccc44a5c5ffe5d1bed2f6 (diff)
Escape feed content with Builder rather than by hand
Builder escapes by default; the three feed templates no longer call CGI.escapeHTML. This fixes two sites that never escaped at all: the tag feed's externally supplied :tag segment, interpolated into its title, self link and id, and dc:creator in the RDF template. Subscribers see one difference: quotes and apostrophes arrive raw, which is valid in element text. config/initializers/xmlparser.rb, which redefined Builder::XmlBase#_escape as the identity function, is gone. XML::Node#replace_with went with it, no callers.
-rw-r--r--app/views/rss/tag_updates.xml.builder4
-rw-r--r--app/views/rss/updates.rdf.builder4
-rw-r--r--app/views/rss/updates.xml.builder6
-rw-r--r--config/initializers/xmlparser.rb19
-rw-r--r--test/controllers/rss_controller_test.rb15
5 files changed, 22 insertions, 26 deletions
diff --git a/app/views/rss/tag_updates.xml.builder b/app/views/rss/tag_updates.xml.builder
index 810ee9aa..9fde2a55 100644
--- a/app/views/rss/tag_updates.xml.builder
+++ b/app/views/rss/tag_updates.xml.builder
@@ -12,7 +12,7 @@ xml.feed(:xmlns => "http://www.w3.org/2005/Atom", "xml:base" => @host) do
12 12
13 @items.each do |item| 13 @items.each do |item|
14 xml.entry do 14 xml.entry do
15 xml.title(CGI.escapeHTML(item.title.to_s)) 15 xml.title(item.title.to_s)
16 xml.link( 16 xml.link(
17 :href => content_url(:page_path => item.node.unique_path), 17 :href => content_url(:page_path => item.node.unique_path),
18 :rel => "alternate", 18 :rel => "alternate",
@@ -21,7 +21,7 @@ xml.feed(:xmlns => "http://www.w3.org/2005/Atom", "xml:base" => @host) do
21 xml.id(content_url(:page_path => item.node.feed_id)) 21 xml.id(content_url(:page_path => item.node.feed_id))
22 xml.updated(item.updated_at.xmlschema) 22 xml.updated(item.updated_at.xmlschema)
23 xml.published(item.published_at.xmlschema) 23 xml.published(item.published_at.xmlschema)
24 xml.summary(CGI.escapeHTML(item.abstract.to_s)) 24 xml.summary(item.abstract.to_s)
25 end 25 end
26 end 26 end
27end 27end
diff --git a/app/views/rss/updates.rdf.builder b/app/views/rss/updates.rdf.builder
index b4fecdb0..699e9c87 100644
--- a/app/views/rss/updates.rdf.builder
+++ b/app/views/rss/updates.rdf.builder
@@ -17,9 +17,9 @@ xml.tag!("rdf:RDF", "xmlns:rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
17 17
18 @items.each do |item| 18 @items.each do |item|
19 xml.item("rdf:about" => content_url(:page_path => item.node.unique_path)) do 19 xml.item("rdf:about" => content_url(:page_path => item.node.unique_path)) do
20 xml.title(CGI.escapeHTML(item.title.to_s)) 20 xml.title(item.title.to_s)
21 xml.link(content_url(:page_path => item.node.unique_path)) 21 xml.link(content_url(:page_path => item.node.unique_path))
22 xml.description(CGI.escapeHTML(item.abstract.to_s)) 22 xml.description(item.abstract.to_s)
23 xml.tag!("dc:creator", (item.user ? item.user.login : "CCC")) 23 xml.tag!("dc:creator", (item.user ? item.user.login : "CCC"))
24 xml.tag!("dc:date", item.published_at.xmlschema) 24 xml.tag!("dc:date", item.published_at.xmlschema)
25 end 25 end
diff --git a/app/views/rss/updates.xml.builder b/app/views/rss/updates.xml.builder
index a2c277d6..f09d6a74 100644
--- a/app/views/rss/updates.xml.builder
+++ b/app/views/rss/updates.xml.builder
@@ -12,7 +12,7 @@ xml.feed(:xmlns => "http://www.w3.org/2005/Atom", "xml:base" => @host) do
12 12
13 @items.each do |item| 13 @items.each do |item|
14 xml.entry do 14 xml.entry do
15 xml.title(CGI.escapeHTML(item.title.to_s)) 15 xml.title(item.title.to_s)
16 xml.link( 16 xml.link(
17 :href => content_url(:page_path => item.node.unique_path), 17 :href => content_url(:page_path => item.node.unique_path),
18 :rel => "alternate", 18 :rel => "alternate",
@@ -21,8 +21,8 @@ xml.feed(:xmlns => "http://www.w3.org/2005/Atom", "xml:base" => @host) do
21 xml.id(content_url(:page_path => item.node.feed_id)) 21 xml.id(content_url(:page_path => item.node.feed_id))
22 xml.updated(item.updated_at.xmlschema) 22 xml.updated(item.updated_at.xmlschema)
23 xml.published(item.published_at.xmlschema) 23 xml.published(item.published_at.xmlschema)
24 xml.summary(CGI.escapeHTML(item.abstract.to_s)) 24 xml.summary(item.abstract.to_s)
25 xml.content(CGI.escapeHTML(item.body.to_s), :type => "html") 25 xml.content(item.body.to_s, :type => "html")
26 end 26 end
27 27
28 end 28 end
diff --git a/config/initializers/xmlparser.rb b/config/initializers/xmlparser.rb
deleted file mode 100644
index 1d5e06d9..00000000
--- a/config/initializers/xmlparser.rb
+++ /dev/null
@@ -1,19 +0,0 @@
1class XML::Node
2 def replace_with(other)
3 self.next = other
4 remove!
5 end
6end
7
8# Builder 3.x escapes content by default. Override _escape to pass text
9# through raw, preserving existing behaviour from the Rails 2 era.
10# Note: require builder first to ensure XmlBase < BasicObject is already
11# defined before we reopen it.
12require 'builder'
13module Builder
14 class XmlBase
15 def _escape(text)
16 text
17 end
18 end
19end
diff --git a/test/controllers/rss_controller_test.rb b/test/controllers/rss_controller_test.rb
index 00224119..4393c5fc 100644
--- a/test/controllers/rss_controller_test.rb
+++ b/test/controllers/rss_controller_test.rb
@@ -41,4 +41,19 @@ class RssControllerTest < ActionController::TestCase
41 assert_includes @response.body, "feed-inside" 41 assert_includes @response.body, "feed-inside"
42 assert_not_includes @response.body, "feed-outside" 42 assert_not_includes @response.body, "feed-outside"
43 end 43 end
44
45 test "the feed escapes markup characters in a title" do
46 updates = Node.root.children.find_by(:slug => "updates")
47 node = updates.children.create!(:slug => "feed-escaping")
48 node.reload.draft.update!(:title => %{Fnord & <b>bold</b> "quoted"},
49 :tag_list => "update")
50 node.publish_draft!
51
52 get :updates, params: { :format => :xml }
53
54 assert_response :success
55 assert_includes @response.body, "Fnord &amp; &lt;b&gt;bold&lt;/b&gt;"
56 assert_not_includes @response.body, "<b>bold</b>"
57 assert_not_includes @response.body, "&amp;amp;"
58 end
44end 59end