diff roundup/cgi/client.py @ 6974:178c80c77ca4

flake8 whitespace fixes plus X == True -> X is True
author John Rouillard <rouilj@ieee.org>
date Wed, 14 Sep 2022 15:08:54 -0400
parents d9c9f5b81d4d
children fe4a6ba98bfe
line wrap: on
line diff
--- a/roundup/cgi/client.py	Wed Sep 14 15:06:44 2022 -0400
+++ b/roundup/cgi/client.py	Wed Sep 14 15:08:54 2022 -0400
@@ -2,55 +2,63 @@
 """
 __docformat__ = 'restructuredtext'
 
+import base64
+import binascii
+import cgi
+import codecs
+import email.utils
+import errno
+import hashlib
 import logging
-logger = logging.getLogger('roundup')
+import mimetypes
+import os
+import quopri
+import re
+import socket
+import stat
+import sys
+import time
 
-import base64, binascii, cgi, codecs, mimetypes, os
-import quopri, re, stat, sys, time
-import socket, errno, hashlib
-import email.utils
+from email.mime.base import MIMEBase
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
 from traceback import format_exc
-
-import roundup.anypy.random_ as random_
-if not random_.is_weak:
-    logger.debug("Importing good random generator")
-else:
-    logger.warning("**SystemRandom not available. Using poor random generator")
-
 try:
     from OpenSSL.SSL import SysCallError
 except ImportError:
     class SysCallError(Exception):
         pass
 
-from roundup.anypy.html import html_escape
+import roundup.anypy.email_
+import roundup.anypy.random_ as random_   # quality of random checked below
+
+from roundup import date, hyperdb, password, rest, roundupdb, xmlrpc
 
-from roundup import roundupdb, date, hyperdb, password
-from roundup.cgi import templating, cgitb, TranslationService
-from roundup.cgi import actions
+from roundup.anypy import http_, urllib_, xmlrpc_
+from roundup.anypy.cookie_ import  BaseCookie, CookieError, get_cookie_date, \
+    SimpleCookie
+from roundup.anypy.html import html_escape
+from roundup.anypy.strings import s2b, b2s, bs2b, uchr, is_us
+
+from roundup.cgi import accept_language, actions, cgitb, templating, \
+    TranslationService
+from roundup.cgi.exceptions import (
+    DetectorError, FormError, IndexerQueryError, NotFound, NotModified,
+    Redirect, SendFile, SendStaticFile, SeriousError)
+from roundup.cgi.form_parser import FormParser
+
 from roundup.exceptions import LoginError, Reject, RejectRaw, \
                                Unauthorised, UsageError
-from roundup.cgi.exceptions import (
-    FormError, IndexerQueryError, NotFound, NotModified, Redirect,
-    SendFile, SendStaticFile, DetectorError, SeriousError)
-from roundup.cgi.form_parser import FormParser
+
 from roundup.mailer import Mailer, MessageSendError
-from roundup.cgi import accept_language
-from roundup import xmlrpc
-from roundup import rest
+
+logger = logging.getLogger('roundup')
 
-from roundup.anypy.cookie_ import CookieError, BaseCookie, SimpleCookie, \
-    get_cookie_date
-from roundup.anypy import http_
-from roundup.anypy import urllib_
-from roundup.anypy import xmlrpc_
+if not random_.is_weak:
+    logger.debug("Importing good random generator")
+else:
+    logger.warning("**SystemRandom not available. Using poor random generator")
 
-from email.mime.base import MIMEBase
-from email.mime.text import MIMEText
-from email.mime.multipart import MIMEMultipart
-import roundup.anypy.email_
-
-from roundup.anypy.strings import s2b, b2s, bs2b, uchr, is_us
 
 def initialiseSecurity(security):
     '''Create some Permissions and Roles on the security object
@@ -58,11 +66,13 @@
     This function is directly invoked by security.Security.__init__()
     as a part of the Security object instantiation.
     '''
-    p = security.addPermission(name="Web Access",
+    p = security.addPermission(
+        name="Web Access",
         description="User may access the web interface")
     security.addPermissionToRole('Admin', p)
 
-    p = security.addPermission(name="Rest Access",
+    p = security.addPermission(
+        name="Rest Access",
         description="User may access the rest interface")
     security.addPermissionToRole('Admin', p)
 
@@ -72,17 +82,20 @@
 
     # doing Role stuff through the web - make sure Admin can
     # TODO: deprecate this and use a property-based control
-    p = security.addPermission(name="Web Roles",
+    p = security.addPermission(
+        name="Web Roles",
         description="User may manipulate user Roles through the web")
     security.addPermissionToRole('Admin', p)
 
+
 def add_message(msg_list, msg, escape=True):
     if escape:
         msg = html_escape(msg, quote=False).replace('\n', '<br />\n')
     else:
         msg = msg.replace('\n', '<br />\n')
-    msg_list.append (msg)
-    return msg_list # for unittests
+    msg_list.append(msg)
+    return msg_list  # for unittests
+
 
 default_err_msg = ''"""<html><head><title>An error has occurred</title></head>
 <body><h1>An error has occurred</h1>
@@ -90,10 +103,11 @@
 The tracker maintainers have been notified of the problem.</p>
 </body></html>"""
 
+
 def seed_pseudorandom():
     '''A function to seed the default pseudorandom random number generator
        which is used to (at minimum):
-          * generate part of email message-id 
+          * generate part of email message-id
           * generate OTK for password reset
           * generate the temp recovery password
 
@@ -104,6 +118,7 @@
     import random
     random.seed()
 
+
 class LiberalCookie(SimpleCookie):
     """ Python's SimpleCookie throws an exception if the cookie uses invalid
         syntax.  Other applications on the same server may have done precisely
@@ -164,7 +179,7 @@
 
     def __init__(self, client):
         self._data = {}
-        self._sid  = None
+        self._sid = None
 
         self.client = client
         self.session_db = client.db.getSessionManager()
@@ -235,6 +250,7 @@
         if set_cookie:
             self.client.add_cookie(self.cookie_name, self._sid, expire=expire)
 
+
 # import from object as well so it's a new style object and I can use super()
 class BinaryFieldStorage(cgi.FieldStorage, object):
     '''This class works around the bug https://bugs.python.org/issue27777.
@@ -251,6 +267,7 @@
             return tempfile.TemporaryFile("wb+")
         return super(BinaryFieldStorage, self).make_file()
 
+
 class Client:
     """Instantiate to handle one CGI request.
 
@@ -341,9 +358,9 @@
     #     takes precedence over mime type.
     # Key can be mime type - all files of that mimetype will get the value
     Cache_Control = {
-        'application/javascript': "public, max-age=1209600", # 2 weeks
-        'text/javascript': "public, max-age=1209600",        # 2 weeks
-        'text/css':               "public, max-age=4838400", # 8 weeks/2 months
+        'application/javascript': "public, max-age=1209600",  # 2 weeks
+        'text/javascript': "public, max-age=1209600",         # 2 weeks
+        'text/css':               "public, max-age=4838400",  # 8 weeks/2 mnths
     }
 
     # list of valid http compression (Content-Encoding) algorithms
@@ -373,7 +390,7 @@
     # which uses this without a content-type so that's an issue.
     # Also for text based data, might have charset too so need to parse
     # content-type.
-    precompressed_mime_types = [ "image/png", "image/jpeg" ]
+    precompressed_mime_types = ["image/png", "image/jpeg"]
 
     def __init__(self, instance, request, env, form=None, translator=None):
         # re-seed the random number generator. Is this is an instance of
@@ -388,14 +405,14 @@
         self.instance = instance
         self.request = request
         self.env = env
-        if translator is not None :
+        if translator is not None:
             self.setTranslator(translator)
             # XXX we should set self.language to "translator"'s language,
             # but how to get it ?
             self.language = ""
-        else :
+        else:
             self.setTranslator(TranslationService.NullTranslationService())
-            self.language = "" # as is the default from determine_language
+            self.language = ""  # as is the default from determine_language
 
         self.mailer = Mailer(instance.config)
         # If True the form contents wins over the database contents when
@@ -410,7 +427,7 @@
         self.base = self.instance.config.TRACKER_WEB
 
         # should cookies be secure?
-        self.secure = self.base.startswith ('https')
+        self.secure = self.base.startswith('https')
 
         # check the tracker_web setting
         if not self.base.endswith('/'):
@@ -439,7 +456,7 @@
                 if 'CONTENT_LENGTH' not in self.env:
                     self.env['CONTENT_LENGTH'] = 0
                     logger.debug("Setting CONTENT_LENGTH to 0 for method: %s",
-                                self.env['REQUEST_METHOD'])
+                                 self.env['REQUEST_METHOD'])
 
             # cgi.FieldStorage must save all data as
             # binary/bytes. Subclass BinaryFieldStorage does this.
@@ -517,7 +534,7 @@
             del(os.environ['HTTP_PROXY'])
 
         xmlrpc_enabled = self.instance.config.WEB_ENABLE_XMLRPC
-        rest_enabled   = self.instance.config.WEB_ENABLE_REST
+        rest_enabled = self.instance.config.WEB_ENABLE_REST
         try:
             if xmlrpc_enabled and self.path == 'xmlrpc':
                 self.handle_xmlrpc()
@@ -530,10 +547,10 @@
             if hasattr(self, 'db'):
                 self.db.close()
 
-
     def handle_xmlrpc(self):
         if self.env.get('CONTENT_TYPE') != 'text/xml':
-            self.write(b"This is the endpoint of Roundup <a href='" +
+            self.write(
+                b"This is the endpoint of Roundup <a href='" +
                 b"https://www.roundup-tracker.org/docs/xmlrpc.html'>" +
                 b"XML-RPC interface</a>.")
             return
@@ -549,7 +566,7 @@
         # Set the charset and language, since other parts of
         # Roundup may depend upon that.
         self.determine_charset()
-        if self.instance.config["WEB_TRANSLATE_XMLRPC"] :
+        if self.instance.config["WEB_TRANSLATE_XMLRPC"]:
             self.determine_language()
         # Open the database as the correct user.
         try:
@@ -573,29 +590,29 @@
             self.setHeader("Content-Length", str(len(output)))
             self.write(s2b(output))
             return
-        
+
         self.check_anonymous_access()
 
         try:
-            # coverting from function returning true/false to 
+            # coverting from function returning true/false to
             # raising exceptions
             # Call csrf with xmlrpc checks enabled.
             # It will return True if everything is ok,
             # raises exception on check failure.
-            csrf_ok =  self.handle_csrf(api=True)
+            csrf_ok = self.handle_csrf(api=True)
         except (Unauthorised, UsageError) as msg:
             # report exception back to server
             exc_type, exc_value, exc_tb = sys.exc_info()
             output = xmlrpc_.client.dumps(
                 xmlrpc_.client.Fault(1, "%s:%s" % (exc_type, exc_value)),
                 allow_none=True)
-            csrf_ok = False # we had an error, failed check
+            csrf_ok = False  # we had an error, failed check
 
-        if csrf_ok == True:
+        if csrf_ok is True:
             handler = xmlrpc.RoundupDispatcher(self.db,
-                                           self.instance.actions,
-                                           self.translator,
-                                           allow_none=True)
+                                               self.instance.actions,
+                                               self.translator,
+                                               allow_none=True)
             output = handler.dispatch(input)
 
         self.setHeader("Content-Type", "text/xml")
@@ -605,7 +622,7 @@
     def handle_rest(self):
         # Set the charset and language
         self.determine_charset()
-        if self.instance.config["WEB_TRANSLATE_REST"] :
+        if self.instance.config["WEB_TRANSLATE_REST"]:
             self.determine_language()
         # Open the database as the correct user.
         # TODO: add everything to RestfulDispatcher
@@ -615,17 +632,17 @@
             self.db.i18n = self.translator
         except LoginError as err:
             self.response_code = http_.client.UNAUTHORIZED
-            output = s2b("Invalid Login - %s"%str(err))
+            output = s2b("Invalid Login - %s" % str(err))
             self.setHeader("Content-Length", str(len(output)))
             self.setHeader("Content-Type", "text/plain")
             self.write(output)
             return
 
         # allow preflight request even if unauthenticated
-        if ( self.env['REQUEST_METHOD'] == "OPTIONS"
-             and self.request.headers.get ("Access-Control-Request-Headers")
-             and self.request.headers.get ("Access-Control-Request-Method")
-             and self.request.headers.get ("Origin")
+        if (self.env['REQUEST_METHOD'] == "OPTIONS"
+            and self.request.headers.get("Access-Control-Request-Headers")
+            and self.request.headers.get("Access-Control-Request-Method")
+            and self.request.headers.get("Origin")
         ):
             # verify Origin is allowed
             if not self.is_origin_header_ok(api=True):
@@ -635,12 +652,12 @@
                 self.response_code = 400
                 msg = self._("Client is not allowed to use Rest Interface.")
                 output = s2b(
-                    '{ "error": { "status": 400, "msg": "%s" } }'%msg )
+                    '{ "error": { "status": 400, "msg": "%s" } }' % msg)
                 self.setHeader("Content-Length", str(len(output)))
                 self.setHeader("Content-Type", "application/json")
                 self.write(output)
                 return
-            
+
             # Call rest library to handle the pre-flight request
             handler = rest.RestfulInstance(self, self.db)
             output = handler.dispatch(self.env['REQUEST_METHOD'],
@@ -653,7 +670,7 @@
             self.setHeader("Content-Type", "application/json")
             self.write(output)
             return
-        
+
         self.check_anonymous_access()
 
         try:
@@ -664,19 +681,20 @@
         except (Unauthorised, UsageError) as msg:
             # FIXME should return what the client requests
             # via accept header.
-            output = s2b('{ "error": { "status": 400, "msg": "%s"}}'% str(msg))
+            output = s2b('{ "error": { "status": 400, "msg": "%s"}}' % 
+                         str(msg))
             self.response_code = 400
             self.setHeader("Content-Length", str(len(output)))
             self.setHeader("Content-Type", "application/json")
             self.write(output)
-            csrf_ok = False # we had an error, failed check
+            csrf_ok = False  # we had an error, failed check
             return
 
         # With the return above the if will never be false,
         # Keeping the if so we can remove return to pass
         # output though  and format output according to accept
         # header.
-        if csrf_ok == True:
+        if csrf_ok is True:
             # Call rest library to handle the request
             handler = rest.RestfulInstance(self, self.db)
             output = handler.dispatch(self.env['REQUEST_METHOD'],
@@ -684,7 +702,7 @@
 
         # type header set by rest handler
         # self.setHeader("Content-Type", "text/xml")
-        if self.response_code == 204: # no body with 204
+        if self.response_code == 204:  # no body with 204
             self.write("")
         else:
             self.setHeader("Content-Length", str(len(output)))
@@ -754,7 +772,7 @@
                 # check for a valid csrf token identifying the right user
                 csrf_ok = True
                 try:
-                    # coverting from function returning true/false to 
+                    # coverting from function returning true/false to
                     # raising exceptions
                     csrf_ok = self.handle_csrf()
                 except (UsageError, Unauthorised) as msg:
@@ -788,9 +806,9 @@
                 # <rj> always expire pages, as IE just doesn't seem to do the
                 # right thing here :(
                 date = time.time() - 1
-                #if self._error_message or self._ok_message:
+                # if self._error_message or self._ok_message:
                 #    date = time.time() - 1
-                #else:
+                # else:
                 #    date = time.time() + 5
                 self.additional_headers['Expires'] = \
                     email.utils.formatdate(date, usegmt=True)
@@ -825,7 +843,7 @@
             if url:
                 self.additional_headers['Location'] = str(url)
                 self.response_code = 302
-            self.write_html('Redirecting to <a href="%s">%s</a>'%(url, url))
+            self.write_html('Redirecting to <a href="%s">%s</a>' % (url, url))
         except LoginError as message:
             # The user tried to log in, but did not provide a valid
             # username and password.  If we support HTTP
@@ -862,7 +880,7 @@
                    <p class="error-message">%s</p>
                 </body></html>
                 """
-                self.write_html(error_page%str(e))
+                self.write_html(error_page % str(e))
             else:
                 self.response_code = 404
                 self.template = '404'
@@ -965,8 +983,9 @@
             try:
                 codecs.lookup(charset)
             except LookupError:
-                self.add_error_message(self._('Unrecognized charset: %r')
-                    % charset)
+                self.add_error_message(self._('Unrecognized charset: %r') %
+                                              charset)
+
                 charset_parameter = 0
             else:
                 self.charset = charset.lower()
@@ -985,6 +1004,7 @@
             decoder = codecs.getdecoder(self.charset)
             encoder = codecs.getencoder(self.STORAGE_CHARSET)
             re_charref = re.compile('&#([0-9]+|x[0-9a-f]+);', re.IGNORECASE)
+
             def _decode_charref(matchobj):
                 num = matchobj.group(1)
                 if num[0].lower() == 'x':
@@ -1026,7 +1046,7 @@
             else:
                 language = ""
 
-        if not language :
+        if not language:
             # default to tracker language
             language = self.instance.config["TRACKER_LANGUAGE"]
 
@@ -1043,7 +1063,7 @@
         ''' authenticate the bearer token. Refactored from determine_user()
             to alow it to be overridden if needed.
         '''
-        try: # will jwt import?
+        try:  # will jwt import?
             import jwt
         except ImportError:
             # no support for jwt, this is fine.
@@ -1056,7 +1076,7 @@
             self.setHeader("WWW-Authenticate", "Basic")
             raise LoginError('Support for jwt disabled by admin.')
 
-        try: # handle jwt exceptions
+        try:  # handle jwt exceptions
             token = jwt.decode(challenge, secret,
                                algorithms=['HS256'],
                                audience=self.db.config.TRACKER_WEB,
@@ -1067,7 +1087,7 @@
             raise LoginError(str(err))
 
         return(token)
-    
+
     def determine_user(self):
         """Determine who the user is"""
         self.opendb('admin')
@@ -1076,7 +1096,7 @@
         # so we define a new function to encpsulate and return the jwt roles
         # and not take the roles from the database.
         override_get_roles = None
-        
+
         # get session data from db
         # XXX: rename
         self.session_api = Session(self)
@@ -1100,8 +1120,8 @@
                 # we have external auth (e.g. by Apache)
                 user = self.env[remote_user_header]
                 if cfg.WEB_HTTP_AUTH_CONVERT_REALM_TO_LOWERCASE and '@' in user:
-                    u, d = user.split ('@', 1)
-                    user = '@'.join ((u, d.lower()))
+                    u, d = user.split('@', 1)
+                    user = '@'.join((u, d.lower()))
             elif self.env.get('HTTP_AUTHORIZATION', ''):
                 # try handling Basic Auth ourselves
                 auth = self.env['HTTP_AUTHORIZATION']
@@ -1137,7 +1157,7 @@
                     # try to seed with something harder to guess than
                     # just the time. If random is SystemRandom,
                     # this is a no-op.
-                    random_.seed("%s%s"%(password,time.time())) 
+                    random_.seed("%s%s" % (password, time.time()))
                 elif scheme.lower() == 'bearer':
                     token = self.authenticate_bearer_token(challenge)
 
@@ -1154,14 +1174,14 @@
                         raise LoginError("Token subject is invalid.")
 
                     # validate roles
-                    all_rolenames = [ role[0] for role in self.db.security.role.items() ]
+                    all_rolenames = [role[0] for role in self.db.security.role.items()]
                     for r in token['roles']:
                         if r.lower() not in all_rolenames:
                             raise LoginError("Token roles are invalid.")
-                            
+
                     # will be used later to override the get_roles method
-                    override_get_roles = \
-                            lambda self: iter_roles(','.join(token['roles']))
+                    override_get_roles = lambda self: iter_roles(
+                        ','.join(token['roles']))
 
         # if user was not set by http authorization, try session lookup
         if not user:
@@ -1229,13 +1249,13 @@
         if self.user == 'anonymous':
             if not self.db.security.hasPermission('Web Access', self.userid):
                 raise Unauthorised(self._("Anonymous users are not "
-                    "allowed to use the web interface"))
+                                          "allowed to use the web interface"))
 
     def is_origin_header_ok(self, api=False):
         origin = self.env['HTTP_ORIGIN']
         # note base https://host/... ends host with with a /,
         # so add it to origin.
-        foundat = self.base.find(origin +'/')
+        foundat = self.base.find(origin + '/')
         if foundat == 0:
             return True
 
@@ -1246,7 +1266,7 @@
         # find a match for other possible origins
         # Original spec says origin is case sensitive match.
         # Living spec doesn't address Origin value's case or
-        # how to compare it. So implement case sensitive.... 
+        # how to compare it. So implement case sensitive....
         if allowed_origins:
             if allowed_origins[0] == '*' or origin in allowed_origins:
                 return True
@@ -1259,14 +1279,14 @@
         referer_comp = urllib_.urlparse(referer)
 
         # self.base always has trailing /, so add trailing / to referer_origin
-        referer_origin = "%s://%s/"%(referer_comp[0], referer_comp[1])
+        referer_origin = "%s://%s/" % (referer_comp[0], referer_comp[1])
         foundat = self.base.find(referer_origin)
         if foundat == 0:
             return True
 
         if not api:
             return False
-        
+
         allowed_origins = self.db.config['WEB_ALLOWED_API_ORIGINS']
         if allowed_origins[0] == '*':
             return True
@@ -1332,7 +1352,7 @@
         # Create the otks handle here as we need it almost immediately.
         # If this is perf issue, set to None here and check below
         # once all header checks have passed if it needs to be opened.
-        otks=self.db.getOTKManager()
+        otks = self.db.getOTKManager()
 
         # Assume: never allow changes via GET
         if self.env['REQUEST_METHOD'] not in ['POST', 'PUT', 'DELETE']:
@@ -1352,7 +1372,7 @@
                     referer = self.env['HTTP_REFERER']
                 else:
                     referer = self._("Referer header not available.")
-                key=self.form['@csrf'].value
+                key = self.form['@csrf'].value
                 if otks.exists(key):
                     logger.error(
                         self._("csrf key used with wrong method from: %s"),
@@ -1360,12 +1380,12 @@
                     otks.destroy(key)
                     otks.commit()
             # do return here. Keys have been obsoleted.
-            # we didn't do a expire cycle of session keys, 
+            # we didn't do a expire cycle of session keys,
             # but that's ok.
             return True
 
-        config=self.instance.config
-        current_user=self.db.getuid()
+        config = self.instance.config
+        current_user = self.db.getuid()
 
         # List HTTP headers we check. Note that the xmlrpc header is
         # missing. Its enforcement is different (yes/required are the
@@ -1381,40 +1401,43 @@
 
         # If required headers are missing, raise an error
         for header in header_names:
-            if (config["WEB_CSRF_ENFORCE_HEADER_%s"%header] == 'required'
+            if (config["WEB_CSRF_ENFORCE_HEADER_%s" % header] == 'required'
                     and "HTTP_%s" % header.replace('-', '_') not in self.env):
                 logger.error(self._("csrf header %s required but missing for user%s."), header, current_user)
-                raise Unauthorised(self._("Missing header: %s")%header)
-                
+                raise Unauthorised(self._("Missing header: %s") % header)
+
         # self.base always matches: ^https?://hostname
-        enforce=config['WEB_CSRF_ENFORCE_HEADER_REFERER']
+        enforce = config['WEB_CSRF_ENFORCE_HEADER_REFERER']
         if 'HTTP_REFERER' in self.env and enforce != "no":
             if not self.is_referer_header_ok(api=api):
                 referer = self.env['HTTP_REFERER']
                 if enforce in ('required', 'yes'):
                     logger.error(self._("csrf Referer header check failed for user%s. Value=%s"), current_user, referer)
-                    raise Unauthorised(self._("Invalid Referer: %s")%(referer))
+                    raise Unauthorised(self._("Invalid Referer: %s") % (
+                        referer))
                 elif enforce == 'logfailure':
-                    logger.warning(self._("csrf Referer header check failed for user%s. Value=%s"), current_user, referer)
+                    logger.warning(self._(
+                        "csrf Referer header check failed for user%s. Value=%s"),
+                                   current_user, referer)
             else:
                 header_pass += 1
 
         # if you change these make sure to consider what
         # happens if header variable exists but is empty.
         # self.base.find("") returns 0 for example not -1
-        enforce=config['WEB_CSRF_ENFORCE_HEADER_ORIGIN']
+        enforce = config['WEB_CSRF_ENFORCE_HEADER_ORIGIN']
         if 'HTTP_ORIGIN' in self.env and enforce != "no":
             if not self.is_origin_header_ok(api=api):
                 origin = self.env['HTTP_ORIGIN']
                 if enforce in ('required', 'yes'):
                     logger.error(self._("csrf Origin header check failed for user%s. Value=%s"), current_user, origin)
-                    raise Unauthorised(self._("Invalid Origin %s"%origin))
+                    raise Unauthorised(self._("Invalid Origin %s" % origin))
                 elif enforce == 'logfailure':
                     logger.warning(self._("csrf Origin header check failed for user%s. Value=%s"), current_user, origin)
             else:
                 header_pass += 1
-                
-        enforce=config['WEB_CSRF_ENFORCE_HEADER_X-FORWARDED-HOST']
+
+        enforce = config['WEB_CSRF_ENFORCE_HEADER_X-FORWARDED-HOST']
         if 'HTTP_X_FORWARDED_HOST' in self.env:
             if enforce != "no":
                 host = self.env['HTTP_X_FORWARDED_HOST']
@@ -1422,10 +1445,15 @@
                 # 4 means self.base has http:/ prefix, 5 means https:/ prefix
                 if foundat not in [4, 5]:
                     if enforce in ('required', 'yes'):
-                        logger.error(self._("csrf X-FORWARDED-HOST header check failed for user%s. Value=%s"), current_user, host)
-                        raise Unauthorised(self._("Invalid X-FORWARDED-HOST %s")%host)
+                        logger.error(self._(
+                            "csrf X-FORWARDED-HOST header check failed for user%s. Value=%s"),
+                                     current_user, host)
+                        raise Unauthorised(self._(
+                            "Invalid X-FORWARDED-HOST %s") % host)
                     elif enforce == 'logfailure':
-                        logger.warning(self._("csrf X-FORWARDED-HOST header check failed for user%s. Value=%s"), current_user, host)
+                        logger.warning(self._(
+                            "csrf X-FORWARDED-HOST header check failed for user%s. Value=%s"),
+                                       current_user, host)
                 else:
                     header_pass += 1
         else:
@@ -1435,7 +1463,7 @@
             # that only. The proxy setting X-F-H has probably set
             # the host header to a local hostname that is
             # internal name of system not name supplied by user.
-            enforce=config['WEB_CSRF_ENFORCE_HEADER_HOST']
+            enforce = config['WEB_CSRF_ENFORCE_HEADER_HOST']
             if 'HTTP_HOST' in self.env and enforce != "no":
                 host = self.env['HTTP_HOST']
                 foundat = self.base.find('://' + host + '/')
@@ -1443,18 +1471,18 @@
                 if foundat not in [4, 5]:
                     if enforce in ('required', 'yes'):
                         logger.error(self._("csrf HOST header check failed for user%s. Value=%s"), current_user, host)
-                        raise Unauthorised(self._("Invalid HOST %s")%host)
+                        raise Unauthorised(self._("Invalid HOST %s") % host)
                     elif enforce == 'logfailure':
                         logger.warning(self._("csrf HOST header check failed for user%s. Value=%s"), current_user, host)
                 else:
                     header_pass += 1
 
-        enforce=config['WEB_CSRF_HEADER_MIN_COUNT']
+        enforce = config['WEB_CSRF_HEADER_MIN_COUNT']
         if header_pass < enforce:
             logger.error(self._("Csrf: unable to verify sufficient headers"))
             raise UsageError(self._("Unable to verify sufficient headers"))
 
-        enforce=config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH']
+        enforce = config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH']
         if api:
             if enforce in ['required', 'yes']:
                 # if we get here we have usually passed at least one
@@ -1465,7 +1493,9 @@
                 #
                 # see: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Protecting_REST_Services:_Use_of_Custom_Request_Headers
                 if 'HTTP_X_REQUESTED_WITH' not in self.env:
-                    logger.error(self._("csrf X-REQUESTED-WITH xmlrpc required header check failed for user%s."), current_user)
+                    logger.error(self._(
+                        "csrf X-REQUESTED-WITH xmlrpc required header check failed for user%s."),
+                                 current_user)
                     raise UsageError(self._("Required Header Missing"))
 
         # Expire old csrf tokens now so we don't use them.  These will
@@ -1484,12 +1514,12 @@
             return True
 
         # process @csrf tokens past this point.
-        key=None
+        key = None
         nonce_user = None
         nonce_session = None
 
         if '@csrf' in self.form:
-            key=self.form['@csrf'].value
+            key = self.form['@csrf'].value
 
             nonce_user = otks.get(key, 'uid', default=None)
             nonce_session = otks.get(key, 'sid', default=None)
@@ -1500,14 +1530,15 @@
         # commit the deletion/expiration of all keys
         otks.commit()
 
-        enforce=config['WEB_CSRF_ENFORCE_TOKEN']
-        if key is None: # we do not have an @csrf token
+        enforce = config['WEB_CSRF_ENFORCE_TOKEN']
+        if key is None:  # we do not have an @csrf token
             if enforce == 'required':
                 logger.error(self._("Required csrf field missing for user%s"), current_user)
                 raise UsageError(self._("We can't validate your session (csrf failure). Re-enter any unsaved data and try again."))
             elif enforce == 'logfailure':
-                    # FIXME include url
-                    logger.warning(self._("csrf field not supplied by user%s"), current_user)
+                # FIXME include url
+                logger.warning(self._("csrf field not supplied by user%s"),
+                               current_user)
             else:
                 # enforce is either yes or no. Both permit change if token is
                 # missing
@@ -1568,8 +1599,8 @@
                     current_session, nonce_session, current_user, key)
                 raise UsageError(self._("We can't validate your session (csrf failure). Re-enter any unsaved data and try again."))
             elif enforce == 'logfailure':
-                    logger.warning(
-                        self._("logged only: Csrf mismatch user: current session %s != stored session %s, current user/stored user is: %s for key %s."),
+                logger.warning(
+                    self._("logged only: Csrf mismatch user: current session %s != stored session %s, current user/stored user is: %s for key %s."),
                     current_session, nonce_session, current_user, key)
         # we are done and the change can occur.
         return True
@@ -1602,7 +1633,6 @@
                 # we can no longer use it.
                 self.session_api = Session(self)
 
-
     def determine_context(self, dre=re.compile(r'([^\d]+)0*(\d+)')):
         """Determine the context of this page from the URL:
 
@@ -1691,13 +1721,13 @@
             try:
                 klass = self.db.getclass(self.classname)
             except KeyError:
-                raise NotFound('%s/%s'%(self.classname, self.nodeid))
+                raise NotFound('%s/%s' % (self.classname, self.nodeid))
             if int(self.nodeid) > 2**31:
                 # Postgres will complain with a ProgrammingError
                 # if we try to pass in numbers that are too large
-                raise NotFound('%s/%s'%(self.classname, self.nodeid))
+                raise NotFound('%s/%s' % (self.classname, self.nodeid))
             if not klass.hasnode(self.nodeid):
-                raise NotFound('%s/%s'%(self.classname, self.nodeid))
+                raise NotFound('%s/%s' % (self.classname, self.nodeid))
             # with a designator, we default to item view
             self.template = 'item'
         else:
@@ -1740,10 +1770,9 @@
 
         # make sure we have permission
         if not self.db.security.hasPermission('View', self.userid,
-                classname, 'content', nodeid):
+                                              classname, 'content', nodeid):
             raise Unauthorised(self._("You are not allowed to view "
-                "this file."))
-
+                                      "this file."))
 
         # --- mime-type security
         # mime type detection is performed in cgi.form_parser
@@ -1751,10 +1780,10 @@
         # everything not here is served as 'application/octet-stream'
         whitelist = [
             'text/plain',
-            'text/x-csrc',   # .c
-            'text/x-chdr',   # .h
-            'text/x-patch',  # .patch and .diff
-            'text/x-python', # .py
+            'text/x-csrc',    # .c
+            'text/x-chdr',    # .h
+            'text/x-patch',   # .patch and .diff
+            'text/x-python',  # .py
             'text/xml',
             'text/csv',
             'text/css',
@@ -1784,7 +1813,6 @@
 
         # --/ mime-type security
 
-
         # If this object is a file (i.e., an instance of FileClass),
         # see if we can find it in the filesystem.  If so, we may be
         # able to use the more-efficient request.sendfile method of
@@ -1820,7 +1848,7 @@
             if is_us(prefix):
                 # prefix can be a string or list depending on
                 # option. Make it a list to iterate over.
-                prefix = [ prefix ]
+                prefix = [prefix]
 
             for p in prefix:
                 # if last element of STATIC_FILES ends with '/-',
@@ -1834,7 +1862,7 @@
                 p = os.path.normpath(p)
                 filename = os.path.normpath(os.path.join(p, file))
                 if os.path.isfile(filename) and filename.startswith(p):
-                    break # inner loop over list of directories
+                    break  # inner loop over list of directories
                 else:
                     # reset filename to None as sentinel for use below.
                     filename = None
@@ -1843,7 +1871,7 @@
             if filename:
                 break
 
-        if filename is None: # we didn't find a filename
+        if filename is None:  # we didn't find a filename
             raise NotFound(file)
 
         # last-modified time
@@ -1859,7 +1887,7 @@
                 mime_type = 'text/plain'
 
         # get filename: given a/b/c.js extract c.js
-        fn=file.rpartition("/")[2]
+        fn = file.rpartition("/")[2]
         if fn in self.Cache_Control:
             # if filename matches, don't use cache control
             # for mime type.
@@ -1868,7 +1896,7 @@
         elif mime_type in self.Cache_Control:
             self.additional_headers['Cache-Control'] = \
                             self.Cache_Control[mime_type]
-            
+
         self._serve_file(lmt, mime_type, '', filename)
 
     def _serve_file(self, lmt, mime_type, content=None, filename=None):
@@ -1956,10 +1984,10 @@
         # determine if view is oktmpl|errortmpl. If so assign the
         # right one to the view parameter. If we don't have alternate
         # templates, just leave view alone.
-        if (view and view.find('|') != -1 ):
+        if (view and view.find('|') != -1):
             # we have alternate templates, parse them apart.
             (oktmpl, errortmpl) = view.split("|", 2)
-            if self._error_message: 
+            if self._error_message:
                 # we have an error, use errortmpl
                 view = errortmpl
             else:
@@ -1972,7 +2000,7 @@
         if name is None:
             name = 'home'
 
-        tplname = name     
+        tplname = name
         if view:
             # Support subdirectories for templates. Value is path/to/VIEW
             # or just VIEW if the template is in the html directory of
@@ -1983,7 +2011,7 @@
                 tplname = '%s.%s' % (name, view)
             else:
                 # try path/class.view
-                tplname = '%s/%s.%s'%(
+                tplname = '%s/%s.%s' % (
                     view[:slash_loc], name, view[slash_loc+1:])
 
         if loader.check(tplname):
@@ -2001,9 +2029,10 @@
         if loader.check(generic):
             return generic
 
-        raise templating.NoTemplate('No template file exists for templating '
-            '"%s" with template "%s" (neither "%s" nor "%s")' % (name, view,
-            tplname, generic))
+        raise templating.NoTemplate(
+            'No template file exists for templating '
+            '"%s" with template "%s" (neither "%s" nor "%s")' % (
+                name, view, tplname, generic))
 
     def renderContext(self):
         """ Return a PageTemplate for the named page
@@ -2030,21 +2059,22 @@
                 else:
                     timings = {'starttag': '<p>', 'endtag': '</p>'}
                 timings['seconds'] = time.time()-self.start
-                s = self._('%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n'
-                    ) % timings
+                s = self._(
+                    '%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n'
+                ) % timings
                 if hasattr(self.db, 'stats'):
                     timings.update(self.db.stats)
                     s += self._("%(starttag)sCache hits: %(cache_hits)d,"
-                        " misses %(cache_misses)d."
-                        " Loading items: %(get_items)f secs."
-                        " Filtering: %(filtering)f secs."
-                        "%(endtag)s\n") % timings
+                                " misses %(cache_misses)d."
+                                " Loading items: %(get_items)f secs."
+                                " Filtering: %(filtering)f secs."
+                                "%(endtag)s\n") % timings
                 s += '</body>'
                 result = result.replace('</body>', s)
             return result
         except templating.NoTemplate as message:
-            self.response_code = 400 
-            return '<strong>%s</strong>'%html_escape(str(message))
+            self.response_code = 400
+            return '<strong>%s</strong>' % html_escape(str(message))
         except templating.Unauthorised as message:
             raise Unauthorised(html_escape(str(message)))
         except:
@@ -2080,9 +2110,9 @@
         trial_templates = []
         if use_template:
             if response_code == 400:
-                trial_templates = [ "400" ]
+                trial_templates = ["400"]
             else:
-                trial_templates = [ str(response_code), "400" ]
+                trial_templates = [str(response_code), "400"]
 
         tplname = None
         for rcode in trial_templates:
@@ -2126,6 +2156,7 @@
         ('export_csv',  actions.ExportCSVAction),
         ('export_csv_id',  actions.ExportCSVWithIdAction),
     )
+
     def handle_action(self):
         """ Determine whether there should be an Action called.
 
@@ -2179,7 +2210,8 @@
                 if name == action_name:
                     break
             else:
-                raise ValueError('No such action "%s"'%html_escape(action_name))
+                raise ValueError('No such action "%s"' % 
+                                 html_escape(action_name))
         return action_klass
 
     def _socket_op(self, call, *args, **kwargs):
@@ -2193,7 +2225,7 @@
         try:
             call(*args, **kwargs)
         except socket.error as err:
-            err_errno = getattr (err, 'errno', None)
+            err_errno = getattr(err, 'errno', None)
             if err_errno is None:
                 try:
                     err_errno = err[0]
@@ -2245,7 +2277,7 @@
            if Vary exists.'''
 
         if ('Vary' in self.additional_headers):
-            self.additional_headers['Vary'] += ", %s"%header
+            self.additional_headers['Vary'] += ", %s" % header
         else:
             self.additional_headers['Vary'] = header
 
@@ -2266,15 +2298,14 @@
 
         # abort if file-type already compressed
         if ('Content-Type' in self.additional_headers) and \
-           (self.additional_headers['Content-Type'] in \
-               self.precompressed_mime_types):
+           (self.additional_headers['Content-Type'] in
+                self.precompressed_mime_types):
             return byte_content
 
         encoder = None
         # return same content if unable to compress
         new_content = byte_content
 
- 
         encoder = self.determine_content_encoding()
 
         if encoder == 'zstd':
@@ -2316,8 +2347,9 @@
                 pass  # etag not set for non-rest endpoints
             else:
                 etag_end = current_etag.rindex('"')
-                self.additional_headers['ETag'] = ( current_etag[:etag_end] +
-                                    '-' + encoder + current_etag[etag_end:])
+                self.additional_headers['ETag'] = (
+                    current_etag[:etag_end] +
+                    '-' + encoder + current_etag[etag_end:])
 
         return new_content
 
@@ -2442,7 +2474,7 @@
             # We only support strong entity tags.
             if_range = self.http_strip(if_range)
             if (not if_range.startswith('"')
-                or not if_range.endswith('"')):
+                    or not if_range.endswith('"')):
                 return None
             # If the condition doesn't match the entity tag, then we
             # must send the client the entire file.
@@ -2485,7 +2517,7 @@
         # We do not handle suffix ranges.
         if not first:
             return None
-       # Convert the first and last positions to integers.
+        # Convert the first and last positions to integers.
         try:
             first = int(first)
             if last:
@@ -2558,17 +2590,19 @@
                 # 2nd best or worse.
                 for encoder in encoding_list:
                     try:
-                        trial_filename = '%s.%s'%(filename,encoder)
+                        trial_filename = '%s.%s' % (filename, encoder)
                         trial_stat_info = os.stat(trial_filename)
                         if stat_info[stat.ST_MTIME] > \
                            trial_stat_info[stat.ST_MTIME]:
                             # compressed file is obsolete
                             # don't use it
-                            logger.warning(self._("Cache failure: "
-                                    "compressed file %(compressed)s is "
-                                    "older than its source file "
-                                    "%(filename)s"%{'filename': filename,
-                                             'compressed': trial_filename}))
+                            logger.warning(self._(
+                                "Cache failure: "
+                                "compressed file %(compressed)s is "
+                                "older than its source file "
+                                "%(filename)s" % {
+                                    'filename': filename,
+                                    'compressed': trial_filename}))
 
                             continue
                         filename = trial_filename
@@ -2581,14 +2615,14 @@
                     except EnvironmentError as e:
                         if e.errno != errno.ENOENT:
                             raise
-                    
+
         # If the headers have not already been finalized,
         if not self.headers_done:
             # RFC 2616 14.19: ETag
             #
             # Compute the entity tag, in a format similar to that
             # used by Apache.
-            # 
+            #
             # Tag does *not* change with Content-Encoding.
             # Header 'Vary: Accept-Encoding' is returned with response.
             # RFC2616 section 13.32 discusses etag and references
@@ -2618,7 +2652,8 @@
         # If the client doesn't actually want the body, or if we are
         # indicating an invalid range.
         if (self.env['REQUEST_METHOD'] == 'HEAD'
-            or self.response_code == http_.client.REQUESTED_RANGE_NOT_SATISFIABLE):
+                or self.response_code ==
+                http_.client.REQUESTED_RANGE_NOT_SATISFIABLE):
             self.setHeader("Content-Length", "0")
             self.header()
             return
@@ -2652,7 +2687,7 @@
         """Put up the appropriate header.
         """
         if headers is None:
-            headers = {'Content-Type':'text/html; charset=utf-8'}
+            headers = {'Content-Type': 'text/html; charset=utf-8'}
         if response is None:
             response = self.response_code
 
@@ -2662,21 +2697,21 @@
         if headers.get('Content-Type', 'text/html') == 'text/html':
             headers['Content-Type'] = 'text/html; charset=utf-8'
 
-        if response in [ 204, 304]: # has no body so no content-type
+        if response in [204, 304]:  # has no body so no content-type
             del(headers['Content-Type'])
 
         headers = list(headers.items())
 
         for ((path, name), (value, expire)) in self._cookies.items():
-            cookie = "%s=%s; Path=%s;"%(name, value, path)
+            cookie = "%s=%s; Path=%s;" % (name, value, path)
             if expire is not None:
-                cookie += " expires=%s;"%get_cookie_date(expire)
+                cookie += " expires=%s;" % get_cookie_date(expire)
             # mark as secure if https, see issue2550689
             if self.secure:
                 cookie += " secure;"
             ssc = self.db.config['WEB_SAMESITE_COOKIE_SETTING']
             if ssc != "None":
-                cookie += " SameSite=%s;"%ssc
+                cookie += " SameSite=%s;" % ssc
             # prevent theft of session cookie, see issue2550689
             cookie += " HttpOnly;"
             headers.append(('Set-Cookie', cookie))

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