Index: trac/db_default.py
===================================================================
--- trac/db_default.py	(revision 2763)
+++ trac/db_default.py	(working copy)
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2006 Edgewall Software
 # Copyright (C) 2003-2005 Daniel Lundin <daniel@edgewall.com>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -394,6 +395,7 @@
   ('trac', 'metanav', 'login,logout,settings,help,about'),
   ('trac', 'mainnav', 'wiki,timeline,roadmap,browser,tickets,newticket,search'),
   ('trac', 'permission_store', 'DefaultPermissionStore'),
+  ('trac', 'rss_escape_html', 'false'),
   ('logging', 'log_type', 'none'),
   ('logging', 'log_file', 'trac.log'),
   ('logging', 'log_level', 'DEBUG'),
Index: trac/ticket/web_ui.py
===================================================================
--- trac/ticket/web_ui.py	(revision 2763)
+++ trac/ticket/web_ui.py	(working copy)
@@ -240,7 +240,7 @@
                 yield ('ticket_details', 'Ticket details', False)
 
     def get_timeline_events(self, req, start, stop, filters):
-        rss = req.args.get('format') == 'rss' # Kludge
+        format = req.args.get('format')
 
         status_map = {'new': ('newticket', 'created'),
                       'reopened': ('newticket', 'reopened'),
@@ -268,15 +268,15 @@
             kind, verb = status_map[status]
             title = util.Markup('Ticket <em title="%s">#%s</em> (%s) %s by %s',
                                 summary, id, type, verb, author)
-            href = rss and self.env.abs_href.ticket(id) \
-                   or self.env.href.ticket(id)
+            href = format == 'rss' and self.env.abs_href.ticket(id) or \
+                   self.env.href.ticket(id)
 
             if status == 'new':
-                message = util.escape(summary)
+                message = summary
             else:
                 message = util.Markup(info)
                 if comment:
-                    if rss:
+                    if format == 'rss':
                         message += wiki_to_html(comment, self.env, req, db,
                                                 absurls=True)
                     else:
Index: trac/ticket/report.py
===================================================================
--- trac/ticket/report.py	(revision 2763)
+++ trac/ticket/report.py	(working copy)
@@ -1,7 +1,8 @@
 # -*- coding: iso-8859-1 -*-
 #
-# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2006 Edgewall Software
 # Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2006 Christian Boos <cboos@neuf.fr>
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -254,7 +255,8 @@
 
         title, description, sql = self.get_info(db, id, args)
 
-        if req.args.get('format') == 'sql':
+        format = req.args.get('format')
+        if format == 'sql':
             self._render_sql(req, id, title, description, sql)
             return
 
@@ -344,7 +346,11 @@
                         id_val = row[id_cols[0]]
                         value['ticket_href'] = self.env.href.ticket(id_val)
                 elif column == 'description':
-                    value['parsed'] = wiki_to_html(cell, self.env, req, db)
+                    descr = wiki_to_html(cell, self.env, req, db,
+                                         absurls=(format == 'rss'))
+                    if format == 'rss':
+                        descr = util.rss_escape_html(descr, self.config)
+                    value['parsed'] = descr
                 elif column == 'reporter' and cell.find('@') != -1:
                     value['rss'] = cell
                 elif column == 'report':
@@ -363,9 +369,7 @@
             row_idx += 1
         req.hdf['report.numrows'] = row_idx
 
-        format = req.args.get('format')
         if format == 'rss':
-            self._render_rss(req)
             return 'report_rss.cs', 'application/rss+xml'
         elif format == 'csv':
             self._render_csv(req, cols, rows)
@@ -480,18 +484,6 @@
                                        .replace('\r',' ')
             req.write(sep.join(map(sanitize, row)) + '\r\n')
 
-    def _render_rss(self, req):
-        # Escape HTML in the ticket summaries
-        item = req.hdf.getObj('report.items')
-        if item:
-            item = item.child()
-            while item:
-                for col in ('summary', 'description.parsed'):
-                    nodename = 'report.items.%s.%s' % (item.name(), col)
-                    value = req.hdf.get(nodename, '')
-                    req.hdf[nodename] = value
-                item = item.next()
-
     def _render_sql(self, req, id, title, description, sql):
         req.perm.assert_permission('REPORT_SQL_VIEW')
         req.send_response(200)
Index: trac/ticket/roadmap.py
===================================================================
--- trac/ticket/roadmap.py	(revision 2763)
+++ trac/ticket/roadmap.py	(working copy)
@@ -316,13 +316,13 @@
                 title = Markup('Milestone <em>%s</em> completed', name)
                 if format == 'rss':
                     href = self.env.abs_href.milestone(name)
-                    message = wiki_to_html(description or '--', self.env,
-                                           req, db, absurls=True)
+                    message = wiki_to_html(description, self.env, db,
+                                           absurls=True)
                 else:
                     href = self.env.href.milestone(name)
                     message = wiki_to_oneliner(description, self.env, db,
                                                shorten=True)
-                yield 'milestone', href, title, completed, None, message
+                yield 'milestone', href, title, completed, None, message or '--'
 
     # IRequestHandler methods
 
Index: trac/ticket/query.py
===================================================================
--- trac/ticket/query.py	(revision 2763)
+++ trac/ticket/query.py	(working copy)
@@ -1,7 +1,8 @@
 # -*- coding: iso-8859-1 -*-
 #
-# Copyright (C) 2004-2005 Edgewall Software
+# Copyright (C) 2004-2006 Edgewall Software
 # Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -21,7 +22,7 @@
 from trac.perm import IPermissionRequestor
 from trac.ticket import Ticket, TicketSystem
 from trac.util import escape, unescape, format_datetime, http_date, \
-                      shorten_line, CRLF, Markup
+                      shorten_line, CRLF, Markup, rss_escape_html
 from trac.web import IRequestHandler
 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
 from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiMacroProvider, \
@@ -590,13 +591,11 @@
             result['href'] = self.env.abs_href.ticket(result['id'])
             if result['reporter'].find('@') == -1:
                 result['reporter'] = ''
-            if result['description']:
-                # str() cancels out the Markup() returned by wiki_to_html
-                result['description'] = str(wiki_to_html(result['description'] or '',
-                                                         self.env, req, db,
-                                                         absurls=1))
             if result['time']:
                 result['time'] = http_date(result['time'])
+            if result['description']:
+                result['description'] = rss_escape_html(result['description'],
+                                                        self.config)
         req.hdf['query.results'] = results
         req.hdf['query.href'] = self.env.abs_href.query(group=query.group,
                 groupdesc=query.groupdesc and 1 or None,
Index: trac/versioncontrol/web_ui/util.py
===================================================================
--- trac/versioncontrol/web_ui/util.py	(revision 2763)
+++ trac/versioncontrol/web_ui/util.py	(working copy)
@@ -1,7 +1,8 @@
 # -*- coding: iso-8859-1 -*-
 #
-# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2006 Edgewall Software
 # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -13,12 +14,13 @@
 # history and logs, available at http://projects.edgewall.com/trac/.
 #
 # Author: Jonas Borgström <jonas@edgewall.com>
+#         Christian Boos <cboos@neuf.fr>
 
 import re
 import urllib
 
 from trac.util import escape, format_datetime, pretty_timedelta, shorten_line, \
-                      TracError, Markup
+                      TracError, Markup, rss_escape_html
 from trac.wiki import wiki_to_html, wiki_to_oneliner
 
 __all__ = ['get_changes', 'get_path_links', 'get_path_rev_line',
@@ -30,26 +32,24 @@
     for rev in revs:
         changeset = repos.get_changeset(rev)
         message = changeset.message or '--'
-        files = None
+        shortlog = wiki_to_oneliner(message, env, db, shorten=True)
         if format == 'changelog':
             files = [change[0] for change in changeset.get_changes()]
-        elif message:
-            if not full:
-                message = wiki_to_oneliner(message, env, db,
-                                           shorten=True)
-            else:
-                message = wiki_to_html(message, env, req, db,
-                                       absurls=(format == 'rss'),
-                                       escape_newlines=True)
-        if not message:
-            message = '--'
+        else:
+            files = None
+            message = full and wiki_to_html(message, env, req, db,
+                                            absurls=(format == 'rss'),
+                                            escape_newlines=True) or shortlog
+            if format == 'rss':
+                shortlog = rss_escape_html(shortlog)
+                message = rss_escape_html(message, env.config)
         changes[rev] = {
             'date_seconds': changeset.date,
             'date': format_datetime(changeset.date),
             'age': pretty_timedelta(changeset.date),
             'author': changeset.author or 'anonymous',
             'message': message,
-            'shortlog': shorten_line(message),
+            'shortlog': shortlog,
             'files': files
         }
     return changes
Index: trac/versioncontrol/web_ui/changeset.py
===================================================================
--- trac/versioncontrol/web_ui/changeset.py	(revision 2763)
+++ trac/versioncontrol/web_ui/changeset.py	(working copy)
@@ -1,8 +1,9 @@
 # -*- coding: iso-8859-1 -*-
 #
-# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2006 Edgewall Software
 # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
 # Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -114,8 +115,8 @@
             for chgset in repos.get_changesets(start, stop):
                 message = chgset.message or '--'
                 if format == 'rss':
-                    title = util.Markup('Changeset <em>[%s]</em>: %s',
-                                        chgset.rev, util.shorten_line(message))
+                    title = 'Changeset [%s]: %s' % \
+                            (chgset.rev, util.shorten_line(message))
                     href = self.env.abs_href.changeset(chgset.rev)
                     message = wiki_to_html(message, self.env, req, db,
                                            absurls=True)
@@ -129,14 +130,14 @@
                     files = []
                     for chg in chgset.get_changes():
                         if show_files > 0 and len(files) >= show_files:
-                            files.append('...')
+                            files.append('&#133;') # &hellip;
                             break
-                        files.append('<span class="%s">%s</span>'
-                                     % (chg[2], util.escape(chg[0])))
-                    message = '<span class="changes">' + ', '.join(files) +\
-                              '</span>: ' + message
+                        files.append('<span class="%s">%s</span>' %
+                                     (chg[2], util.escape(chg[0])))
+                    message = util.Markup('<span class="changes">%s</span>: %s',
+                                          ', '.join(files), message)
                 yield 'changeset', href, title, chgset.date, chgset.author,\
-                      util.Markup(message)
+                      message
 
     # Internal methods
 
Index: trac/versioncontrol/web_ui/log.py
===================================================================
--- trac/versioncontrol/web_ui/log.py	(revision 2763)
+++ trac/versioncontrol/web_ui/log.py	(working copy)
@@ -1,7 +1,8 @@
 # -*- coding: iso-8859-1 -*-
 #
-# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2006 Edgewall Software
 # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -157,7 +158,6 @@
                 if email:
                     email_map[username] = email
             for cs in changes.values():
-                cs['shortlog'] = cs['shortlog'].replace('\n', ' ')
                 # For RSS, author must be an email address
                 author = cs['author']
                 author_email = ''
Index: trac/Timeline.py
===================================================================
--- trac/Timeline.py	(revision 2763)
+++ trac/Timeline.py	(working copy)
@@ -1,8 +1,9 @@
 # -*- coding: iso-8859-1 -*-
 #
-# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2006 Edgewall Software
 # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
 # Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -21,7 +22,8 @@
 
 from trac.core import *
 from trac.perm import IPermissionRequestor
-from trac.util import format_date, format_time, http_date, Markup
+from trac.util import format_date, format_time, http_date, Markup, \
+                      rss_escape_html
 from trac.web import IRequestHandler
 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
 
@@ -158,10 +160,8 @@
 
             if format == 'rss':
                 # Strip/escape HTML markup
-                if isinstance(title, Markup):
-                    event['title'] = title.striptags()
-                else:
-                    event['title'] = title
+                event['title'] = rss_escape_html(title)
+                event['message'] = rss_escape_html(message, self.config)
 
                 if author:
                     # For RSS, author must be an email address
Index: trac/util.py
===================================================================
--- trac/util.py	(revision 2763)
+++ trac/util.py	(working copy)
@@ -246,6 +246,15 @@
         return text
     return text.unescape()
 
+ENTITIES = re.compile(r"&(?:\w+|#\d+);")
+def rss_escape_html(text, config=None):
+    if isinstance(text, Markup) and \
+           not (config and config.getbool('trac', 'rss_escape_html')):
+        return re.sub(ENTITIES, '', text.striptags())
+    else:
+        return str(text)
+
+
 def to_utf8(text, charset='iso-8859-15'):
     """Convert a string to UTF-8, assuming the encoding is either UTF-8, ISO
     Latin-1, or as specified by the optional `charset` parameter."""

