diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-09 15:29:40 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-09 15:29:40 +0200 |
| commit | 9c3217df50d462d6be4399e3e654d591bf704df7 (patch) | |
| tree | 627e474b273789667bca9eadffa0e7f626d002bb /app/models | |
| parent | 8310751d4db2854597d6379b24e346c65622972d (diff) | |
Fix malformed-HTML fallout across RSS, link rewriting, and flash messaging
Editors' TinyMCE output can contain unclosed void elements like <br>,
which is valid HTML5 but invalid XML -- three different places assumed
the stricter rule and broke or silently misbehaved on the looser one.
The Atom feed's <content type="xhtml"> block required real, well-formed
XML structure but was handed a raw, unescaped body string; switched to
type="html" with CGI.escapeHTML, matching how title/summary already
handle the same content. rewrite_links_in_body used libxml's strict XML
parser to rewrite internal links to be locale-prefixed, which raised on
exactly this class of malformed markup -- silently, since the whole
method was wrapped in rescue; nil, meaning the link rewrite (not the
save) quietly failed with no error anywhere. Replaced with Nokogiri's
lenient HTML parser, which repairs malformed void elements rather than
rejecting them; also drops the bare rescue now that the actual failure
mode it was guarding against shouldn't occur, and fixes two adjacent
bugs found while in this method: a typo'd /sytem/uploads/ regex that
could never match, and a missing https:// exclusion alongside the
existing http:// one.
Also addresses stale flash messaging surfaced while testing the above:
update's save confirmation was being clobbered by edit's own "locked
and ready" notice on the very next request, since nothing distinguished
a fresh lock acquisition from a redirect back after saving. The save
confirmation now names the next step (publish from Status) and flags a
stale translation if one exists, using Page#outdated_translations?,
already present but previously unused by any controller.
Diffstat (limited to 'app/models')
| -rw-r--r-- | app/models/page.rb | 33 |
1 files changed, 12 insertions, 21 deletions
diff --git a/app/models/page.rb b/app/models/page.rb index fff044e..1a98e08 100644 --- a/app/models/page.rb +++ b/app/models/page.rb | |||
| @@ -243,31 +243,22 @@ class Page < ApplicationRecord | |||
| 243 | end | 243 | end |
| 244 | 244 | ||
| 245 | def rewrite_links_in_body | 245 | def rewrite_links_in_body |
| 246 | begin | 246 | return unless self.body |
| 247 | if self.body | ||
| 248 | tmp_body = "<div>#{self.body}</div>" | ||
| 249 | xml_string = XML::Parser.string( tmp_body ) | ||
| 250 | xml_doc = xml_string.parse | ||
| 251 | links = xml_doc.find("//a[not(starts-with(@href, 'http://'))]") | ||
| 252 | links = links.reject { |l| l[:href] =~ /system\/uploads/ } | ||
| 253 | locales = I18n.available_locales.reject {|l| l == :root} | ||
| 254 | 247 | ||
| 255 | links.each do |link| | 248 | doc = Nokogiri::HTML::DocumentFragment.parse(self.body) |
| 256 | unless locales.include? link[:href].slice(1,2).to_sym | 249 | locales = I18n.available_locales.reject { |l| l == :root } |
| 257 | unless link[:href] =~ /sytem\/uploads/ | ||
| 258 | link[:href] = link[:href].sub(/^\//, "/#{I18n.locale}/") | ||
| 259 | end | ||
| 260 | end | ||
| 261 | end | ||
| 262 | 250 | ||
| 263 | tmp_body = xml_doc.to_s.gsub(/(\n\<div\>|\<\/div\>\n)/, "") | 251 | doc.css('a').each do |link| |
| 264 | tmp_body.gsub!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "") | 252 | href = link['href'] |
| 253 | next unless href | ||
| 254 | next if href.start_with?('http://', 'https://') | ||
| 255 | next if href =~ /system\/uploads/ | ||
| 265 | 256 | ||
| 266 | self.body = tmp_body | 257 | unless locales.include?(href.slice(1, 2)&.to_sym) |
| 258 | link['href'] = href.sub(/^\//, "/#{I18n.locale}/") | ||
| 267 | end | 259 | end |
| 268 | rescue | ||
| 269 | nil | ||
| 270 | end | 260 | end |
| 271 | end | ||
| 272 | 261 | ||
| 262 | self.body = doc.to_html | ||
| 263 | end | ||
| 273 | end | 264 | end |
