Skip to content

Commit 68849d9

Browse files
committed
Support setting cache-control headers for static files
Control how to cache static files. Can control by mime type or filename. Needs to use interfaces.py mechanism to configure. See customization.txt file in the interfaces.py section. Also added docs for using interfacs.py and a few examples.
1 parent 899c1fa commit 68849d9

File tree

4 files changed

+169
-4
lines changed

4 files changed

+169
-4
lines changed

CHANGES.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ Features:
3232
supposed to be used for as the CLI has full access to the files so a
3333
password check is not useful. An edge case is when the login has a :
3434
in it. In this case it may not work as expected. So don't do that.
35-
35+
- Implement Cache-Control headers for static files. Allows tracker
36+
admin to control caching for css, js and other static files. See
37+
customizing.html. The use is documented in the section describing
38+
how to use interfaces.py.
39+
3640
Fixed:
3741

3842
- issue2550996 - Give better error message when running with -c

doc/customizing.txt

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Customisation of Roundup can take one of six forms:
2222
1. `tracker configuration`_ changes
2323
2. database, or `tracker schema`_ changes
2424
3. "definition" class `database content`_ changes
25-
4. behavioural changes, through detectors_ and extensions_
25+
4. behavioural changes through detectors_, extensions_ and interfaces.py_
2626
5. `security / access controls`_
2727
6. change the `web interface`_
2828

@@ -1107,6 +1107,140 @@ flexible extension point mechanism.
11071107
Generic action can be added by inheriting from ``action.Action``
11081108
instead of ``cgi.action.Action``.
11091109

1110+
interfaces.py - hooking into the core of roundup
1111+
================================================
1112+
.. _interfaces.py:
1113+
1114+
There is a magic trick for hooking into the core of roundup. Using
1115+
this you can:
1116+
1117+
* modify class data structures
1118+
* monkey patch core code to add new functionality
1119+
* modify the email gateway
1120+
* add new rest endpoints
1121+
1122+
but with great power comes great responsibility.
1123+
1124+
Interfaces.py has been around since the earliest releases of roundup
1125+
and used to be the main way to get a lot of customization done. In
1126+
modern roundup, the extensions_ mechanism is used, but there are places
1127+
where interfaces.py is still useful.
1128+
1129+
Example: Changing Cache-Control headers
1130+
---------------------------------------
1131+
1132+
The Client class in cgi/client.py has a lookup table that is used to
1133+
set the Cache-Control headers for static files. The entries in this
1134+
table are set from interfaces.py using::
1135+
1136+
from roundup.cgi.client import Client
1137+
1138+
Client.Cache_Control['text/css'] = "public, max-age=3600"
1139+
Client.Cache_Control['application/javascript'] = "public, max-age=30"
1140+
Client.Cache_Control['rss.xml'] = "public, max-age=900"
1141+
Client.Cache_Control['local.js'] = "public, max-age=7200"
1142+
1143+
In this case static files delivered using @@file will have cache
1144+
headers set. These files are searched for along the `static_files`
1145+
path in the tracker's `config.ini`. In the example above:
1146+
1147+
* a css file (e.g. @@file/style.css) will be cached for an hour
1148+
* javascript files (e.g. @@file/libraries/jquery.js) will be cached
1149+
for 30 seconds
1150+
* a file named rss.xml will be cached for 5 minutes
1151+
* a file named local.js will be cached for 2 hours
1152+
1153+
Note that a file name match overrides the mime type settings.
1154+
1155+
Example: Implement password complexity checking
1156+
-----------------------------------------------
1157+
1158+
This example uses the zxcvbn_ module that you can place in the zxcvbn
1159+
subdirectory of your tracker's lib directory.
1160+
1161+
If you add this to the interfaces.py file in the root directory of
1162+
your tracker (same place as schema.py)::
1163+
1164+
import roundup.password as password
1165+
from roundup.exceptions import Reject
1166+
from zxcvbn import zxcvbn
1167+
1168+
# monkey patch the setPassword method with this method
1169+
# that checks password strength.
1170+
origPasswordFunc = password.Password.setPassword
1171+
def mpPasswordFunc(self, plaintext, scheme, config=None):
1172+
""" Replace the password set function with one that
1173+
verifies that the password is complex enough. It
1174+
has to be done at this point and not in an auditor
1175+
as the auditor only sees the encrypted password.
1176+
"""
1177+
results = zxcvbn(plaintext)
1178+
if results['score'] < 3:
1179+
l = []
1180+
map(l.extend, [[results['feedback']['warning']], results['feedback']['suggestions']])
1181+
errormsg = " ".join(l)
1182+
raise Reject ("Password is too easy to guess. " + errormsg)
1183+
return origPasswordFunc(self, plaintext, scheme, config=config)
1184+
1185+
password.Password.setPassword = mpPasswordFunc
1186+
1187+
it replaces the setPassword method in the Password class. The new
1188+
version validates that the password is sufficiently complex. Then it
1189+
passes off the setting of password to the original method.
1190+
1191+
Example: Enhance time intervals
1192+
-------------------------------
1193+
1194+
To make the user interface easier to use, you may want to support
1195+
other forms for intervals. For example you can support an interval
1196+
like 1.5 by interpreting it the same as 1:30 (1 hour 30 minutes).
1197+
Also you can allow a bare integer (e.g. 45) as a number of minutes.
1198+
1199+
To do this we intercept the from_raw method of the Interval class in
1200+
hyperdb.py with::
1201+
1202+
import roundup.hyperdb as hyperdb
1203+
origFrom_Raw = hyperdb.Interval.from_raw
1204+
1205+
def normalizeperiod(self, value, **kw):
1206+
''' Convert alternate time forms into standard interval format
1207+
1208+
[+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]
1209+
1210+
if value is float, it's hour and fractional hours
1211+
if value is integer, it's number of minutes
1212+
'''
1213+
if ":" not in value:
1214+
# Not a specified interval
1215+
# if int consider number of minutes
1216+
try:
1217+
isMinutes = int(value)
1218+
minutes = isMinutes%60
1219+
hours = (isMinutes - minutes) / 60
1220+
value = "%d:%d"%(hours,minutes)
1221+
except ValueError:
1222+
pass
1223+
# if float, consider it number of hours and fractional hours.
1224+
import math
1225+
try:
1226+
afterdecimal, beforedecimal = math.modf(float(value))
1227+
value = "%d:%d"%(beforedecimal,60*afterdecimal)
1228+
except ValueError:
1229+
pass
1230+
1231+
return origFrom_Raw(self, value, **kw)
1232+
1233+
hyperdb.Interval.from_raw = normalizeperiod
1234+
1235+
any call to convert an interval from raw form now has two simpler
1236+
(and more friendly) ways to specify common time intervals.
1237+
1238+
Other Examples
1239+
--------------
1240+
1241+
See the `rest interface documentation`_ for instructions on how to add
1242+
new rest endpoints using interfaces.py.
1243+
11101244
Database Content
11111245
================
11121246

@@ -5474,3 +5608,5 @@ rather than requiring a web server restart.
54745608

54755609
.. _`design documentation`: design.html
54765610
.. _`developer's guide`: developers.html
5611+
.. _`rest interface documentation`: rest.html#programming-the-rest-api
5612+
.. _`zxcvbn`: https://github.com/dwolfhub/zxcvbn-python

roundup/cgi/client.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ class Client:
341341
errno.ETIMEDOUT,
342342
)
343343

344+
Cache_Control = {}
345+
344346
def __init__(self, instance, request, env, form=None, translator=None):
345347
# re-seed the random number generator. Is this is an instance of
346348
# random.SystemRandom it has no effect.
@@ -1711,6 +1713,17 @@ def serve_static_file(self, file):
17111713
else:
17121714
mime_type = 'text/plain'
17131715

1716+
# get filename: given a/b/c.js extract c.js
1717+
fn=file.rpartition("/")[2]
1718+
if fn in self.Cache_Control:
1719+
# if filename matches, don't use cache control
1720+
# for mime type.
1721+
self.additional_headers['Cache-Control'] = \
1722+
self.Cache_Control[fn]
1723+
elif mime_type in self.Cache_Control:
1724+
self.additional_headers['Cache-Control'] = \
1725+
self.Cache_Control[mime_type]
1726+
17141727
self._serve_file(lmt, mime_type, '', filename)
17151728

17161729
def _serve_file(self, lmt, mime_type, content=None, filename=None):

test/test_cgi.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,15 +1732,27 @@ def my_serve_file(a, b, c, d):
17321732
self.assertEqual(output[0][1], "text/css")
17331733
self.assertEqual(output[0][3], "_test_cgi_form/detectors/css/README.css")
17341734
del output[0] # reset output buffer
1735-
1736-
1735+
1736+
cl.Cache_Control['text/css'] = 'public, max-age=3600'
17371737
# use subdir in static files path
17381738
cl.instance.config['STATIC_FILES'] = 'detectors html/css'
17391739
os.mkdir('_test_cgi_form/html/css')
17401740
f = open('_test_cgi_form/html/css/README1.css', 'a').close()
17411741
cl.serve_static_file("README1.css")
17421742
self.assertEqual(output[0][1], "text/css")
17431743
self.assertEqual(output[0][3], "_test_cgi_form/html/css/README1.css")
1744+
self.assertTrue( "Cache-Control" in cl.additional_headers )
1745+
self.assertEqual( cl.additional_headers,
1746+
{'Cache-Control': 'public, max-age=3600'} )
1747+
del output[0] # reset output buffer
1748+
1749+
cl.Cache_Control['README1.css'] = 'public, max-age=60'
1750+
cl.serve_static_file("README1.css")
1751+
self.assertEqual(output[0][1], "text/css")
1752+
self.assertEqual(output[0][3], "_test_cgi_form/html/css/README1.css")
1753+
self.assertTrue( "Cache-Control" in cl.additional_headers )
1754+
self.assertEqual( cl.additional_headers,
1755+
{'Cache-Control': 'public, max-age=60'} )
17441756
del output[0] # reset output buffer
17451757

17461758

0 commit comments

Comments
 (0)