diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-08-09 23:31:35 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-08-09 23:31:35 +0200 |
| commit | 5690cf4d4e05eafdfd2e270bbdf1a925114d0f76 (patch) | |
| tree | 1315dd1b91433f5287208a4f1b321dd722592966 | |
| parent | 4d8c7290adf8dd41677ccc44a5c5ffe5d1bed2f6 (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.builder | 4 | ||||
| -rw-r--r-- | app/views/rss/updates.rdf.builder | 4 | ||||
| -rw-r--r-- | app/views/rss/updates.xml.builder | 6 | ||||
| -rw-r--r-- | config/initializers/xmlparser.rb | 19 | ||||
| -rw-r--r-- | test/controllers/rss_controller_test.rb | 15 |
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 |
| 27 | end | 27 | end |
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 @@ | |||
| 1 | class XML::Node | ||
| 2 | def replace_with(other) | ||
| 3 | self.next = other | ||
| 4 | remove! | ||
| 5 | end | ||
| 6 | end | ||
| 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. | ||
| 12 | require 'builder' | ||
| 13 | module Builder | ||
| 14 | class XmlBase | ||
| 15 | def _escape(text) | ||
| 16 | text | ||
| 17 | end | ||
| 18 | end | ||
| 19 | end | ||
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 & <b>bold</b>" | ||
| 56 | assert_not_includes @response.body, "<b>bold</b>" | ||
| 57 | assert_not_includes @response.body, "&amp;" | ||
| 58 | end | ||
| 44 | end | 59 | end |
