changeset 5004:494d255043c9

Display errors containing HTML with RejectRaw (issue2550847) In general outputting un-escaped HTML in a message to the user is an unsafe operation, which is why error message are escaped by default. In some cases though it is desirable for a detector to include HTML within an error message. For these cases where HTML is required the RejectRaw exception can be used within the detector.
author John Kristensen <john@jerrykan.com>
date Sat, 10 Oct 2015 23:35:51 +1100
parents a9e0ef739241
children a86b0c02940d
files CHANGES.txt doc/customizing.txt roundup/cgi/actions.py roundup/cgi/client.py roundup/exceptions.py
diffstat 5 files changed, 41 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Sat Oct 10 23:07:17 2015 +1100
+++ b/CHANGES.txt	Sat Oct 10 23:35:51 2015 +1100
@@ -57,6 +57,9 @@
   (similar to RFC 2822), e.g. +0200 for CEST or -0500 for EST. This also
   works in the XMLRPC interface. For examples see roundup.date.Date.
   (Ralf Schlatterbeck)
+- Add RejectRaw exception to allow unescaped HTML error messages to be
+  displayed to the user (thanks Ezio Melotti for the initial patch)
+  (John Kristensen)
 
 Fixed:
 
--- a/doc/customizing.txt	Sat Oct 10 23:07:17 2015 +1100
+++ b/doc/customizing.txt	Sat Oct 10 23:35:51 2015 +1100
@@ -928,7 +928,13 @@
 
 And then when your rejection criteria have been detected, simply::
 
-   raise Reject
+   raise Reject('Description of error')
+
+Error messages raised with ``Reject`` automatically have any HTML content
+escaped before being displayed to the user. To display an error message to the
+user without performing any HTML escaping the ``RejectRaw`` should be used. All
+security implications should be carefully considering before using
+``RejectRaw``.
 
 
 Generating email from Roundup
--- a/roundup/cgi/actions.py	Sat Oct 10 23:07:17 2015 +1100
+++ b/roundup/cgi/actions.py	Sat Oct 10 23:35:51 2015 +1100
@@ -3,9 +3,9 @@
 from roundup import hyperdb, token, date, password
 from roundup.actions import Action as BaseAction
 from roundup.i18n import _
-import roundup.exceptions
 from roundup.cgi import exceptions, templating
 from roundup.mailgw import uidFromAddress
+from roundup.exceptions import Reject, RejectRaw
 from roundup.anypy import io_, urllib_
 
 __all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
@@ -106,7 +106,7 @@
         """Retire the context item."""
         # ensure modification comes via POST
         if self.client.env['REQUEST_METHOD'] != 'POST':
-            raise roundup.exceptions.Reject(self._('Invalid request'))
+            raise Reject(self._('Invalid request'))
 
         # if we want to view the index template now, then unset the itemid
         # context info (a special-case for retire actions on the index page)
@@ -285,7 +285,7 @@
         """
         # ensure modification comes via POST
         if self.client.env['REQUEST_METHOD'] != 'POST':
-            raise roundup.exceptions.Reject(self._('Invalid request'))
+            raise Reject(self._('Invalid request'))
 
         # figure the properties list for the class
         cl = self.db.classes[self.classname]
@@ -606,7 +606,7 @@
         """
         # ensure modification comes via POST
         if self.client.env['REQUEST_METHOD'] != 'POST':
-            raise roundup.exceptions.Reject(self._('Invalid request'))
+            raise Reject(self._('Invalid request'))
 
         user_activity = self.lastUserActivity()
         if user_activity:
@@ -620,10 +620,10 @@
         # handle the props
         try:
             message = self._editnodes(props, links)
-        except (ValueError, KeyError, IndexError,
-                roundup.exceptions.Reject), message:
+        except (ValueError, KeyError, IndexError, Reject) as message:
+            escape = not isinstance(message, RejectRaw)
             self.client.add_error_message(
-                self._('Edit Error: %s') % str(message))
+                self._('Edit Error: %s') % str(message), escape=escape)
             return
 
         # commit now that all the tricky stuff is done
@@ -652,7 +652,7 @@
         '''
         # ensure modification comes via POST
         if self.client.env['REQUEST_METHOD'] != 'POST':
-            raise roundup.exceptions.Reject(self._('Invalid request'))
+            raise Reject(self._('Invalid request'))
 
         # parse the props from the form
         try:
@@ -666,10 +666,11 @@
         try:
             # when it hits the None element, it'll set self.nodeid
             messages = self._editnodes(props, links)
-        except (ValueError, KeyError, IndexError,
-                roundup.exceptions.Reject), message:
+        except (ValueError, KeyError, IndexError, Reject) as message:
+            escape = not isinstance(message, RejectRaw)
             # these errors might just be indicative of user dumbness
-            self.client.add_error_message(_('Error: %s') % str(message))
+            self.client.add_error_message(_('Error: %s') % str(message),
+                                          escape=escape)
             return
 
         # commit now that all the tricky stuff is done
@@ -833,7 +834,7 @@
         """
         # ensure modification comes via POST
         if self.client.env['REQUEST_METHOD'] != 'POST':
-            raise roundup.exceptions.Reject(self._('Invalid request'))
+            raise Reject(self._('Invalid request'))
 
         # parse the props from the form
         try:
@@ -849,10 +850,11 @@
             try:
                 # when it hits the None element, it'll set self.nodeid
                 messages = self._editnodes(props, links)
-            except (ValueError, KeyError, IndexError,
-                    roundup.exceptions.Reject), message:
+            except (ValueError, KeyError, IndexError, Reject) as message:
+                escape = not isinstance(message, RejectRaw)
                 # these errors might just be indicative of user dumbness
-                self.client.add_error_message(_('Error: %s') % str(message))
+                self.client.add_error_message(_('Error: %s') % str(message),
+                                              escape=escape)
                 return
 
             # fix up the initial roles
@@ -957,7 +959,7 @@
         """
         # ensure modification comes via POST
         if self.client.env['REQUEST_METHOD'] != 'POST':
-            raise roundup.exceptions.Reject(self._('Invalid request'))
+            raise Reject(self._('Invalid request'))
 
         # we need the username at a minimum
         if '__login_name' not in self.form:
--- a/roundup/cgi/client.py	Sat Oct 10 23:07:17 2015 +1100
+++ b/roundup/cgi/client.py	Sat Oct 10 23:35:51 2015 +1100
@@ -16,7 +16,7 @@
 from roundup import roundupdb, date, hyperdb, password
 from roundup.cgi import templating, cgitb, TranslationService
 from roundup.cgi.actions import *
-from roundup.exceptions import *
+from roundup.exceptions import LoginError, Reject, RejectRaw, Unauthorised
 from roundup.cgi.exceptions import *
 from roundup.cgi.form_parser import FormParser
 from roundup.mailer import Mailer, MessageSendError, encode_quopri
@@ -1274,9 +1274,9 @@
                 return getattr(self, action_klass)()
             else:
                 return action_klass(self).execute()
-
-        except (ValueError, Reject), err:
-            self.add_error_message(str(err))
+        except (ValueError, Reject) as err:
+            escape = not isinstance(err, RejectRaw)
+            self.add_error_message(str(err), escape=escape)
 
     def get_action_class(self, action_name):
         if (hasattr(self.instance, 'cgi_actions') and
--- a/roundup/exceptions.py	Sat Oct 10 23:07:17 2015 +1100
+++ b/roundup/exceptions.py	Sat Oct 10 23:35:51 2015 +1100
@@ -21,6 +21,15 @@
     """
     pass
 
+
+class RejectRaw(Reject):
+    """
+    Performs the same function as Reject, except HTML in the message is not
+    escaped when displayed to the user.
+    """
+    pass
+
+
 class UsageError(ValueError):
     pass
 

Roundup Issue Tracker: http://roundup-tracker.org/