• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / Products.StandardCacheManagers / 16399753970

17 Mar 2025 07:54AM UTC coverage: 50.611% (-2.0%) from 52.597%
16399753970

push

github

web-flow
Update Python version support. (#13)

* Drop support for Python 3.8.
* Add support for Python 3.13.

13 of 124 branches covered (10.48%)

Branch coverage included in aggregate %.

360 of 613 relevant lines covered (58.73%)

0.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.48
/src/Products/StandardCacheManagers/AcceleratedHTTPCacheManager.py
1
##############################################################################
2
#
3
# Copyright (c) 2002 Zope Foundation and Contributors.
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
7
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10
# FOR A PARTICULAR PURPOSE
11
#
12
##############################################################################
13
'''
14
Accelerated HTTP cache manager --
15
  Adds caching headers to the response so that downstream caches will
16
  cache according to a common policy.
17
'''
18

19
import logging
1✔
20
import socket
1✔
21
import time
1✔
22
from html import escape
1✔
23
from http.client import HTTPConnection
1✔
24
from operator import itemgetter
1✔
25
from urllib.parse import quote
1✔
26
from urllib.parse import urlparse
1✔
27

28
from AccessControl.class_init import InitializeClass
1✔
29
from AccessControl.Permissions import view_management_screens
1✔
30
from AccessControl.SecurityInfo import ClassSecurityInfo
1✔
31
from App.special_dtml import DTMLFile
1✔
32
from OFS.Cache import Cache
1✔
33
from OFS.Cache import CacheManager
1✔
34
from OFS.SimpleItem import SimpleItem
1✔
35
from zope.datetime import rfc1123_date
1✔
36

37

38
logger = logging.getLogger('Zope.AcceleratedHTTPCacheManager')
1✔
39

40

41
class AcceleratedHTTPCache(Cache):
1✔
42
    # Note the need to take thread safety into account.
43
    # Also note that objects of this class are not persistent,
44
    # nor do they use acquisition.
45

46
    connection_factory = HTTPConnection
1✔
47

48
    def __init__(self):
1✔
49
        self.hit_counts = {}
1✔
50

51
    def initSettings(self, kw):
1✔
52
        # Note that we lazily allow AcceleratedHTTPCacheManager
53
        # to verify the correctness of the internal settings.
54
        self.__dict__.update(kw)
1✔
55

56
    def ZCache_invalidate(self, ob):
1✔
57
        # Note that this only works for default views of objects at
58
        # their canonical path. If an object is viewed and cached at
59
        # any other path via acquisition or virtual hosting, that
60
        # cache entry cannot be purged because there is an infinite
61
        # number of such possible paths, and Squid does not support
62
        # any kind of fuzzy purging; we have to specify exactly the
63
        # URL to purge.  So we try to purge the known paths most
64
        # likely to turn up in practice: the physical path and the
65
        # current absolute_url_path.  Any of those can be
66
        # wrong in some circumstances, but it may be the best we can
67
        # do :-(
68
        # It would be nice if Squid's purge feature was better
69
        # documented.  (pot! kettle! black!)
70

71
        phys_path = ob.getPhysicalPath()
1✔
72
        if phys_path in self.hit_counts:
1!
73
            del self.hit_counts[phys_path]
×
74
        purge_paths = (ob.absolute_url_path(), quote('/'.join(phys_path)))
1✔
75
        # Don't purge the same path twice.
76
        if purge_paths[0] == purge_paths[1]:
1✔
77
            purge_paths = purge_paths[:1]
1✔
78
        results = []
1✔
79
        for url in self.notify_urls:
1✔
80
            if not url.strip():
1!
81
                continue
×
82
            # Send the PURGE request to each HTTP accelerator.
83
            if url[:7].lower() == 'http://':
1✔
84
                u = url
1✔
85
            else:
86
                u = 'http://' + url
1✔
87
            (scheme, host, path, params, query,
1✔
88
             fragment) = urlparse(u)
89
            if path.lower().startswith('/http://'):
1!
90
                path = path.lstrip('/')
×
91
            for ob_path in purge_paths:
1✔
92
                p = path.rstrip('/') + ob_path
1✔
93
                h = self.connection_factory(host)
1✔
94
                logger.debug(f'PURGING host {host}, path {p}')
1✔
95
                # An exception on one purge should not prevent the others.
96
                try:
1✔
97
                    h.request('PURGE', p)
1✔
98
                    # This better not hang. I wish httplib gave us
99
                    # control of timeouts.
100
                except socket.gaierror:
×
101
                    msg = ('socket.gaierror: maybe the server '
×
102
                           'at %s is down, or the cache manager '
103
                           'is misconfigured?')
104
                    logger.error(msg % url)
×
105
                    continue
×
106
                r = h.getresponse()
1✔
107
                status = f'{r.status} {r.reason}'
1✔
108
                results.append(status)
1✔
109
                logger.debug('purge response: %s' % status)
1✔
110
        return 'Server response(s): ' + ';'.join(results)
1✔
111

112
    def ZCache_get(self, ob, view_name, keywords, mtime_func, default):
1✔
113
        return default
×
114

115
    def ZCache_set(self, ob, data, view_name, keywords, mtime_func):
1✔
116
        # Note the blatant ignorance of view_name and keywords.
117
        # Standard HTTP accelerators are not able to make use of this
118
        # data.  mtime_func is also ignored because using "now" for
119
        # Last-Modified is as good as using any time in the past.
120
        REQUEST = ob.REQUEST
×
121
        RESPONSE = REQUEST.RESPONSE
×
122
        anon = 1
×
123
        u = REQUEST.get('AUTHENTICATED_USER', None)
×
124
        if u is not None:
×
125
            if u.getUserName() != 'Anonymous User':
×
126
                anon = 0
×
127
        phys_path = ob.getPhysicalPath()
×
128
        if phys_path in self.hit_counts:
×
129
            hits = self.hit_counts[phys_path]
×
130
        else:
131
            self.hit_counts[phys_path] = hits = [0, 0]
×
132
        if anon:
×
133
            hits[0] = hits[0] + 1
×
134
        else:
135
            hits[1] = hits[1] + 1
×
136

137
        if not anon and self.anonymous_only:
×
138
            return
×
139
        # Set HTTP Expires and Cache-Control headers
140
        seconds = self.interval
×
141
        expires = rfc1123_date(time.time() + seconds)
×
142
        RESPONSE.setHeader('Last-Modified', rfc1123_date(time.time()))
×
143
        RESPONSE.setHeader('Cache-Control', 'max-age=%d' % seconds)
×
144
        RESPONSE.setHeader('Expires', expires)
×
145

146

147
caches = {}
1✔
148
PRODUCT_DIR = __name__.split('.')[-2]
1✔
149

150

151
class AcceleratedHTTPCacheManager(CacheManager, SimpleItem):
1✔
152

153
    security = ClassSecurityInfo()
1✔
154
    security.setPermissionDefault('Change cache managers', ('Manager', ))
1✔
155

156
    manage_options = (
1✔
157
        {'label': 'Properties', 'action': 'manage_main'},
158
        {'label': 'Statistics', 'action': 'manage_stats'},
159
    ) + CacheManager.manage_options + SimpleItem.manage_options
160

161
    meta_type = 'Accelerated HTTP Cache Manager'
1✔
162
    zmi_icon = 'fas fa-forward'
1✔
163

164
    def __init__(self, ob_id):
1✔
165
        self.id = ob_id
1✔
166
        self.title = ''
1✔
167
        self._settings = {'anonymous_only': 1,
1✔
168
                          'interval': 3600,
169
                          'notify_urls': ()}
170
        self._resetCacheId()
1✔
171

172
    def getId(self):
1✔
173
        ' '
174
        return self.id
1✔
175

176
    @security.private
1✔
177
    def _remove_data(self):
1✔
178
        caches.pop(self.__cacheid, None)
1✔
179

180
    @security.private
1✔
181
    def _resetCacheId(self):
1✔
182
        self.__cacheid = f'{id(self)}_{time.time():f}'
1✔
183

184
    @security.private
1✔
185
    def ZCacheManager_getCache(self):
1✔
186
        cacheid = self.__cacheid
1✔
187
        try:
1✔
188
            return caches[cacheid]
1✔
189
        except KeyError:
1✔
190
            cache = AcceleratedHTTPCache()
1✔
191
            cache.initSettings(self._settings)
1✔
192
            caches[cacheid] = cache
1✔
193
            return cache
1✔
194

195
    @security.protected(view_management_screens)
1✔
196
    def getSettings(self):
1✔
197
        ' '
198
        return self._settings.copy()  # Don't let UI modify it.
1✔
199

200
    security.declareProtected(view_management_screens,  # NOQA: D001
1✔
201
                              'manage_main')
202
    manage_main = DTMLFile('dtml/propsAccel', globals())
1✔
203

204
    @security.protected('Change cache managers')
1✔
205
    def manage_editProps(self, title, settings=None, REQUEST=None):
1✔
206
        ' '
207
        if settings is None:
×
208
            settings = REQUEST
×
209
        self.title = str(title)
×
210
        self._settings = {
×
211
            'anonymous_only': settings.get('anonymous_only') and 1 or 0,
212
            'interval': int(settings['interval']),
213
            'notify_urls': tuple(settings['notify_urls'])}
214
        cache = self.ZCacheManager_getCache()
×
215
        cache.initSettings(self._settings)
×
216
        if REQUEST is not None:
×
217
            return self.manage_main(
×
218
                self, REQUEST, manage_tabs_message='Properties changed.')
219

220
    security.declareProtected(view_management_screens,  # NOQA: D001
1✔
221
                              'manage_stats')
222
    manage_stats = DTMLFile('dtml/statsAccel', globals())
1✔
223

224
    def _getSortInfo(self):
1✔
225
        """
226
        Returns the value of sort_by and sort_reverse.
227
        If not found, returns default values.
228
        """
229
        req = self.REQUEST
×
230
        sort_by = req.get('sort_by', 'anon')
×
231
        sort_reverse = int(req.get('sort_reverse', 1))
×
232
        return sort_by, sort_reverse
×
233

234
    @security.protected(view_management_screens)
1✔
235
    def getCacheReport(self):
1✔
236
        """
237
        Returns the list of objects in the cache, sorted according to
238
        the user's preferences.
239
        """
240
        sort_by, sort_reverse = self._getSortInfo()
×
241
        c = self.ZCacheManager_getCache()
×
242
        rval = []
×
243
        for path, (anon, auth) in c.hit_counts.items():
×
244
            rval.append({'path': '/'.join(path),
×
245
                         'anon': anon,
246
                         'auth': auth})
247
        if sort_by:
×
248
            rval.sort(key=itemgetter(sort_by), reverse=sort_reverse)
×
249
        return rval
×
250

251
    @security.protected(view_management_screens)
1✔
252
    def sort_link(self, name, id):
1✔
253
        """
254
        Utility for generating a sort link.
255
        """
256
        # XXX This ought to be in a library or something.
257
        sort_by, sort_reverse = self._getSortInfo()
×
258
        url = self.absolute_url() + '/manage_stats?sort_by=' + id
×
259
        newsr = 0
×
260
        if sort_by == id:
×
261
            newsr = not sort_reverse
×
262
        url = url + '&sort_reverse=' + (newsr and '1' or '0')
×
263
        return f'<a href="{escape(url, 1)}">{escape(name)}</a>'
×
264

265

266
InitializeClass(AcceleratedHTTPCacheManager)
1✔
267

268

269
manage_addAcceleratedHTTPCacheManagerForm = DTMLFile('dtml/addAccel',
1✔
270
                                                     globals())
271

272

273
def manage_addAcceleratedHTTPCacheManager(self, id, REQUEST=None):
1✔
274
    ' '
275
    self._setObject(id, AcceleratedHTTPCacheManager(id))
×
276
    if REQUEST is not None:
×
277
        return self.manage_main(self, REQUEST)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc