changeset 8262:2a7c3eeaf167

feat: add templating utils method dynamically; method to set http code Added new utils.set_http_response(integer) to set the HTML response code from a template. Useful for error handling inside template. Also noted that a real TemplatingUtils (like set_http_response) method gets the TemplatingUtils object instance, but there is no way to do this with registerUtil() from an extension file. Added new instance.registerUtilMethod() method to register a function in an extension as a method passing the client instance in as the first parameter (aka self).
author John Rouillard <rouilj@ieee.org>
date Tue, 07 Jan 2025 20:22:33 -0500
parents 28c5030757d3
children 78b13272d41b
files CHANGES.txt doc/admin_guide.txt doc/reference.txt doc/upgrading.txt roundup/cgi/templating.py roundup/instance.py
diffstat 6 files changed, 142 insertions(+), 26 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Sun Jan 05 00:59:12 2025 -0500
+++ b/CHANGES.txt	Tue Jan 07 20:22:33 2025 -0500
@@ -95,7 +95,13 @@
 - issue2551116 - Replace xmlrpclib (xmlrpc.client) with defusedxml.
   Added support for defusedxml to better secure the xmlrpc
   endpoint. (John Rouillard)
-
+- Added new instance.registerUtilMethod() method to make using complex
+  templating easier as it provides a default Client instance to the
+  templating method. (John Rouillard)
+- Added new templating utils.set_http_response(integer) method to
+  allow reporting an error to the user from a template. (John
+  Rouillard)
+	
 2024-07-13 2.4.0
 
 Fixed:
--- a/doc/admin_guide.txt	Sun Jan 05 00:59:12 2025 -0500
+++ b/doc/admin_guide.txt	Tue Jan 07 20:22:33 2025 -0500
@@ -443,6 +443,8 @@
 More secure CSPs can also be created. However because of the ability
 to customise the web interface, it is difficult to provide guidance.
 
+.. _dynamic_csp:
+
 Dynamic CSP
 -----------
 
@@ -469,7 +471,7 @@
     }
 
 
-    def AddHtmlHeaders(client, header_dict=None):
+    def AddHtmlHeaders(self, header_dict=None):
 	''' Generate https headers from dict use default security headers
 
 	    Setting the header with a value of None will not inject the
@@ -479,34 +481,35 @@
 	    nonce. Use to set a nonce for inline scripts.
 	'''
 	try:
-	    if client.client_nonce is None:
+	    if self.client.client_nonce is None:
 		# logger.warning("client_nonce is None")
-		client.client_nonce = client.session_api._gen_sid()
+		self.client.client_nonce = self.client.session_api._gen_sid()
 	except AttributeError:
-	    # client.client_nonce doesn't exist, create it
+	    # self.client.client_nonce doesn't exist, create it
 	    # logger.warning("client_nonce does not exist, creating")
-	    client.client_nonce = client.session_api._gen_sid()
+	    self.client.client_nonce = client.session_api._gen_sid()
 
 	headers = default_security_headers.copy()
 	if isinstance(header_dict, dict):
             headers.update(header_dict)
 
-	client_headers = client.additional_headers
+	client_headers = self.client.additional_headers
 
 	for header, value in list(headers.items()):
 	    if value is None:
 		continue
 	    client_headers[header] = value.format(
-                nonce=client.client_nonce)
+                nonce=self.client.client_nonce)
 
     def init(instance):
-        instance.registerUtil('AddHtmlHeaders', AddHtmlHeaders)
+        # Note the use of the new (in version 2.5) registerUtilMethod
+        instance.registerUtilMethod('AddHtmlHeaders', AddHtmlHeaders)
 
 
 Adding the following to ``page.html`` right after the opening
 ``<html....`>`` tag::
 
-   <tal:code tal:content="python:utils.AddHtmlHeaders(request.client)" />
+   <tal:code tal:content="python:utils.AddHtmlHeaders()" />
 
 will invoke ``AddHtmlHeaders()`` to add the CSP header with the nonce.
 
@@ -523,6 +526,43 @@
 
 for each script, object or style tag.
 
+If you are using a version of Roundup before version 2.5, you need to
+replace ``instance.registerUtilMethod`` with
+``instance.registerUtil``. For example::
+
+    def init(instance):
+        # Note the use of the new (in version 2.5) registerUtilMethod
+        instance.registerUtil('AddHtmlHeaders', AddHtmlHeaders)
+
+The AddHtmlHeaders function needs to be changed so that ``self.client``
+is replaced by ``client``::
+
+    # replace self parameter with client
+    def AddHtmlHeaders(client, header_dict=None):
+	''' Generate https headers from dict use default security headers
+
+	    Setting the header with a value of None will not inject the
+	    header and can override the default set.
+
+	    Header values will be formatted with a dictionary including a
+	    nonce. Use to set a nonce for inline scripts.
+	'''
+
+	### Then change all references to self.client to client
+
+	try:
+	    if client.client_nonce is None:  # note self.client -> client
+		# logger.warning("client_nonce is None")
+		client.client_nonce = self.client.session_api._gen_sid()
+
+	...
+
+Lastly the client must be passed explicitly when calling
+AddHtmlHeaders. The call looks like::
+
+   <tal:code tal:content="python:utils.AddHtmlHeaders(request.client)" />
+
+
 Remediating ``unsafe-inline``
 -----------------------------
 .. _remediating unsafe-inline:
--- a/doc/reference.txt	Sun Jan 05 00:59:12 2025 -0500
+++ b/doc/reference.txt	Tue Jan 07 20:22:33 2025 -0500
@@ -1148,6 +1148,11 @@
   (see `adding a time log to your issues
   <customizing.html#adding-a-time-log-to-your-issues-4>`_ for an example)
 
+* ``instance.registerUtilMethod`` is also used for adding `templating
+  utilities`_, and provides a client instance by default to the
+  function. This makes more complex templating actions easier to
+  use. (see :ref:`dynamic_csp` for an example)
+
 * ``instance.registerAction`` is used to add more actions to the
   instance and to web interface. See `Defining new web actions`_
   for details.  Generic action can be added by inheriting from
@@ -3465,22 +3470,23 @@
 .. table::
   :class: valign-top
 
-  =============== ========================================================
-  Method          Description
-  =============== ========================================================
-  Batch           return a batch object using the supplied list
-  anti_csrf_nonce returns the random nonce generated for this session
-  expandfile      load a file into a template and expand
-                  '%(tokenname)s' in the file using
-                  values from the supplied dictionary.
-  html_quote      quote some text as safe in HTML (ie. <, >, ...)
-  html_calendar   renders an HTML calendar used by the
-		  ``_generic.calendar.html`` template (itself invoked by
-		  the popupCalendar DateHTMLProperty method
-  readfile        read JavaScript or other content in an external
-                  file into the template.
-  url_quote       quote some text as safe for a URL (ie. space, %, ...)
-  =============== ========================================================
+  ================= ========================================================
+  Method            Description
+  ================= ========================================================
+  Batch             return a batch object using the supplied list
+  anti_csrf_nonce   returns the random nonce generated for this session
+  expandfile        load a file into a template and expand
+                    '%(tokenname)s' in the file using
+                    values from the supplied dictionary.
+  html_quote        quote some text as safe in HTML (ie. <, >, ...)
+  html_calendar     renders an HTML calendar used by the
+		    ``_generic.calendar.html`` template (itself invoked by
+		    the popupCalendar DateHTMLProperty method
+  readfile          read JavaScript or other content in an external
+                    file into the template.
+  set_http_response sets the HTTP response code for the request.
+  url_quote         quote some text as safe for a URL (ie. space, %, ...)
+  ================= ========================================================
 
 Additional info can be obtained by starting ``python`` with the
 ``roundup`` subdirectory on your PYTHONPATH and using the Python help
--- a/doc/upgrading.txt	Sun Jan 05 00:59:12 2025 -0500
+++ b/doc/upgrading.txt	Tue Jan 07 20:22:33 2025 -0500
@@ -253,6 +253,28 @@
 to the data format (mime type, API version) will return 406 errors,
 where some previously resulted in 400 errors.
 
+New method for registering templating utils (info)
+--------------------------------------------------
+
+If you are building a template utility function that needs access
+to:
+
+  * the database
+  * the client instance
+  * the form the user submitted
+
+you had to pass these objects from the template using the ``db``,
+``request.client`` or ``request.form``  arguments.
+
+A new method for registering a template utility has been
+added. If you use the ``instance`` object's
+``registerUtilMethod()`` to register a utility function, you do
+not need to pass these arguments. The function is called as a
+method and the first argument is a ``client`` instance from which
+the database (client.db), form (client.form).
+
+You can find an example in :ref:`dynamic_csp`.
+
 .. index:: Upgrading; 2.3.0 to 2.4.0
 
 Migrating from 2.3.0 to 2.4.0
--- a/roundup/cgi/templating.py	Sun Jan 05 00:59:12 2025 -0500
+++ b/roundup/cgi/templating.py	Tue Jan 07 20:22:33 2025 -0500
@@ -3799,6 +3799,18 @@
                 {'fullpath': fullpath, 'issue': e.args[0]})
             return ""
 
+    def set_http_response(self, code):
+        '''Set the HTTP response code to the integer `code`.
+            Example::
+
+              <tal:x
+               tal:replace="python:utils.set_response(404);"
+              />
+
+
+            will make the template return code 404 (not found).
+            '''
+        self.client.response_code = code
 
 class MissingValue(object):
     def __init__(self, description, **kwargs):
--- a/roundup/instance.py	Sun Jan 05 00:59:12 2025 -0500
+++ b/roundup/instance.py	Tue Jan 07 20:22:33 2025 -0500
@@ -247,8 +247,38 @@
             self.cgi_actions[name] = action
 
     def registerUtil(self, name, function):
+        """Register a function that can be called using:
+           `utils.<name>(...)`.
+
+           The function is defined as:
+
+               def function(...):
+
+           If you need access to the client, database, form or other
+           item, you have to pass it explicitly::
+
+               utils.name(request.client, ...)
+
+           If you need client access, consider using registerUtilMethod()
+           instead.
+
+        """
         self.templating_utils[name] = function
 
+    def registerUtilMethod(self, name, function):
+        """Register a method that can be called using:
+           `utils.<name>(...)`.
+
+           Unlike registerUtil, the method is defined as:
+
+               def function(self, ...):
+
+           `self` is a TemplatingUtils object. You can use self.client
+           to access the client object for your request.
+        """
+        setattr(self.TemplatingUtils,
+                name, 
+                function)
 
 class TrackerError(RoundupException):
     pass

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