Index: trac/ticket/query.py
===================================================================
--- trac/ticket/query.py	(revision 2403)
+++ trac/ticket/query.py	(working copy)
@@ -590,12 +590,12 @@
     def _format_link(self, formatter, ns, query, label):
         if query[0] == '?':
             return '<a class="query" href="%s">%s</a>' \
-                   % (escape(formatter.href.query()) + query.replace(' ', '+'),
+                   % (escape(formatter.href.query() + query.replace(' ', '+')),
                       label)
         else:
             from trac.ticket.query import Query, QuerySyntaxError
             try:
-                query = Query.from_string(formatter.env, unescape(query))
+                query = Query.from_string(formatter.env, query)
                 return '<a class="query" href="%s">%s</a>' \
                        % (escape(query.get_href()), label)
             except QuerySyntaxError, e:
Index: trac/Search.py
===================================================================
--- trac/Search.py	(revision 2403)
+++ trac/Search.py	(working copy)
@@ -237,8 +237,7 @@
 
     def _format_link(self, formatter, ns, query, label):
         if query and query[0] == '?':
-            href = formatter.href.search() + \
-                   query.replace('&amp;', '&').replace(' ', '+')
+            href = formatter.href.search() + query.replace(' ', '+')
         else:
             href = formatter.href.search(q=query)
         return '<a class="search" href="%s">%s</a>' % (escape(href), label)
Index: trac/wiki/tests/wiki-tests.txt
===================================================================
--- trac/wiki/tests/wiki-tests.txt	(revision 2403)
+++ trac/wiki/tests/wiki-tests.txt	(working copy)
@@ -115,6 +115,7 @@
 ticket:1
 This ticket is the first one
 changeset:123>
+changeset:123&
 ------------------------------
 <p>
 Add-on to <a class="missing changeset" href="/changeset/123" rel="nofollow">changeset:123</a>:
@@ -122,6 +123,7 @@
 <a class="missing ticket" href="/ticket/1" rel="nofollow">ticket:1</a>
 This ticket is the first one
 <a class="missing changeset" href="/changeset/123" rel="nofollow">changeset:123</a>&gt;
+<a class="missing changeset" href="/changeset/123" rel="nofollow">changeset:123</a>&amp;
 </p>
 ------------------------------
 Add-on to <a class="missing changeset" href="/changeset/123" rel="nofollow">changeset:123</a>:
@@ -129,6 +131,7 @@
 <a class="missing ticket" href="/ticket/1" rel="nofollow">ticket:1</a>
 This ticket is the first one
 <a class="missing changeset" href="/changeset/123" rel="nofollow">changeset:123</a>&gt;
+<a class="missing changeset" href="/changeset/123" rel="nofollow">changeset:123</a>&amp;
 ==============================
 CamelCase AlabamA ABc AlaBamA FooBar
 ------------------------------
Index: trac/wiki/formatter.py
===================================================================
--- trac/wiki/formatter.py	(revision 2403)
+++ trac/wiki/formatter.py	(working copy)
@@ -138,7 +138,7 @@
     QUOTED_STRING = r"'[^']+'|\"[^\"]+\""
 
     SHREF_TARGET_FIRST = r"[\w/?!#@]"
-    SHREF_TARGET_MIDDLE = r"(?:\|(?=[^|\s])|&(?!lt;|gt;)|[^|&\s])"
+    SHREF_TARGET_MIDDLE = r"(?:\|(?=[^|\s])|[^|<>\s])"
     SHREF_TARGET_LAST = r"[a-zA-Z0-9/=]" # we don't want "_"
 
     LHREF_RELATIVE_TARGET = r"[/.][^\s[\]]*"
@@ -148,6 +148,7 @@
     # between _pre_rules and _post_rules
 
     _pre_rules = [
+        r"(?P<htmlescape>[&<>])",
         # Font styles
         r"(?P<bolditalic>%s)" % BOLDITALIC_TOKEN,
         r"(?P<bold>%s)" % BOLD_TOKEN,
@@ -297,16 +298,19 @@
 
     def _make_link(self, ns, target, match, label):
         if ns in self.link_resolvers:
-            return self.link_resolvers[ns](self, ns, target, label)
+            return self.link_resolvers[ns](self, ns, target,
+                                           util.escape(label, False))
         elif target.startswith('//') or ns == "mailto":
             return self._make_ext_link(ns+':'+target, label)
         else:
-            return match
+            return util.escape(match)
 
     def _make_ext_link(self, url, text, title=''):
-        title_attr = title and ' title="%s"' % title or ''
+        url = util.escape(url)
+        title_attr = title and ' title="%s"' % util.escape(title) or ''
         if Formatter.img_re.search(url) and self.flavor != 'oneliner':
-            return '<img src="%s" alt="%s" />' % (url, title or text)
+            return '<img src="%s" alt="%s" />' % (url,
+                                                  util.escape(title or text))
         if not url.startswith(self._local):
             return '<a class="ext-link" href="%s"%s><span class="icon">' \
                    '</span>%s</a>' % (url, title_attr, text)
@@ -314,6 +318,7 @@
             return '<a href="%s"%s>%s</a>' % (url, title_attr, text)
 
     def _make_relative_link(self, url, text):
+        url, text = util.escape(url), util.escape(text)
         if Formatter.img_re.search(url) and self.flavor != 'oneliner':
             return '<img src="%s" alt="%s" />' % (url, text)
         if url.startswith('//'): # only the protocol will be kept
@@ -365,12 +370,14 @@
         # the tickethref regexp
         return match
 
+    def _htmlescape_formatter(self, match, fullmatch):
+        return match == "&" and "&amp;" or match == "<" and "&lt;" or "&gt;"
+
     def _macro_formatter(self, match, fullmatch):
         name = fullmatch.group('macroname')
         if name in ['br', 'BR']:
             return '<br />'
         args = fullmatch.group('macroargs')
-        args = util.unescape(args)
         try:
             macro = WikiProcessor(self.env, name)
             return macro.process(self.req, args, 1)
@@ -390,8 +397,7 @@
         depth = min(len(fullmatch.group('hdepth')), 5)
         heading = match[depth + 1:len(match) - depth - 1]
 
-        text = wiki_to_oneliner(util.unescape(heading), self.env, self.db,
-                                self._absurls)
+        text = wiki_to_oneliner(heading, self.env, self.db, self._absurls)
         sans_markup = re.sub(r'</?\w+(?: .*?)?>', '', text)
 
         anchor = self._anchor_re.sub('', sans_markup.decode('utf-8'))
@@ -600,7 +606,6 @@
                 self.close_def_list()
                 continue
 
-            line = util.escape(line, False)
             if escape_newlines:
                 line += ' [[BR]]'
             self.in_list_item = False
@@ -643,8 +648,10 @@
     # Override a few formatters to disable some wiki syntax in "oneliner"-mode
     def _list_formatter(self, match, fullmatch): return match
     def _indent_formatter(self, match, fullmatch): return match
-    def _heading_formatter(self, match, fullmatch): return match
-    def _definition_formatter(self, match, fullmatch): return match
+    def _heading_formatter(self, match, fullmatch):
+        return util.escape(match, False)
+    def _definition_formatter(self, match, fullmatch):
+        return util.escape(match, False)
     def _table_cell_formatter(self, match, fullmatch): return match
     def _last_table_cell_formatter(self, match, fullmatch): return match
 
@@ -664,7 +671,7 @@
         self.out = out
         self._open_tags = []
 
-        result = re.sub(self.rules, self.replace, util.escape(text.strip(), False))
+        result = re.sub(self.rules, self.replace, text.strip())
         # Close all open 'one line'-tags
         result += self.close_tag(None)
         out.write(result)
@@ -712,8 +719,7 @@
         depth = min(len(fullmatch.group('hdepth')), 5)
         heading = match[depth + 1:len(match) - depth - 1]
         anchor = self._anchors[-1]
-        text = wiki_to_oneliner(util.unescape(heading), self.env, self.db,
-                                self._absurls)
+        text = wiki_to_oneliner(heading, self.env, self.db, self._absurls)
         text = re.sub(r'</?a(?: .*?)?>', '', text) # Strip out link tags
         self.outline.append((depth, '<a href="#%s">%s</a>' % (anchor, text)))
 
