[notification] avoid leaking cnxsets (closes #3243810)
When sending notifications, we get each recipient as either an email
address or a CWUser. In the latter case, we create a temporary session
for that user and use it to send the mail. However, if we later decided
to not send the mail after all, we'd leak the session and its cnxset.
Add a try block inside the loop to make sure the temporary sessions are
closed properly.
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""HTTP cache managers"""
__docformat__ = "restructuredtext en"
from time import mktime
from datetime import datetime
# time delta usable to convert localized time to GMT time
# XXX this become erroneous after a DST transition!!!
GMTOFFSET = - (datetime.now() - datetime.utcnow())
class NoHTTPCacheManager(object):
"""default cache manager: set no-cache cache control policy"""
def __init__(self, view):
self.view = view
self.req = view._cw
self.cw_rset = view.cw_rset
def set_headers(self):
self.req.set_header('Cache-control', 'no-cache')
class MaxAgeHTTPCacheManager(NoHTTPCacheManager):
"""max-age cache manager: set max-age cache control policy, with max-age
specified with the `cache_max_age` attribute of the view
"""
def set_headers(self):
self.req.set_header('Cache-control',
'max-age=%s' % self.view.cache_max_age)
class EtagHTTPCacheManager(NoHTTPCacheManager):
"""etag based cache manager for startup views
* etag is generated using the view name and the user's groups
* set policy to 'must-revalidate' and expires to the current time to force
revalidation on each request
"""
def etag(self):
if not self.req.cnx: # session without established connection to the repo
return self.view.__regid__
return self.view.__regid__ + '/' + ','.join(sorted(self.req.user.groups))
def max_age(self):
# 0 to actually force revalidation
return 0
def last_modified(self):
"""return view's last modified GMT time"""
return self.view.last_modified()
def set_headers(self):
req = self.req
try:
req.set_header('Etag', '"%s"' % self.etag())
except NoEtag:
self.req.set_header('Cache-control', 'no-cache')
return
req.set_header('Cache-control',
'must-revalidate;max-age=%s' % self.max_age())
mdate = self.last_modified()
# use a timestamp, not a formatted raw header, and let
# the front-end correctly generate it
# ("%a, %d %b %Y %H:%M:%S GMT" return localized date that
# twisted don't parse correctly)
req.set_header('Last-modified', mktime(mdate.timetuple()), raw=False)
class EntityHTTPCacheManager(EtagHTTPCacheManager):
"""etag based cache manager for view displaying a single entity
* etag is generated using entity's eid, the view name and the user's groups
* get last modified time from the entity definition (this may not be the
entity's modification time since a view may include some related entities
with a modification time to consider) using the `last_modified` method
"""
def etag(self):
if self.cw_rset is None or len(self.cw_rset) == 0: # entity startup view for instance
return super(EntityHTTPCacheManager, self).etag()
if len(self.cw_rset) > 1:
raise NoEtag()
etag = super(EntityHTTPCacheManager, self).etag()
eid = self.cw_rset[0][0]
if self.req.user.owns(eid):
etag += ',owners'
return str(eid) + '/' + etag
class NoEtag(Exception):
"""an etag can't be generated"""
__all__ = ('GMTOFFSET',
'NoHTTPCacheManager', 'MaxAgeHTTPCacheManager',
'EtagHTTPCacheManager', 'EntityHTTPCacheManager')
# monkey patching, so view doesn't depends on this module and we have all
# http cache related logic here
from cubicweb import view as viewmod
def set_http_cache_headers(self):
self.http_cache_manager(self).set_headers()
viewmod.View.set_http_cache_headers = set_http_cache_headers
def last_modified(self):
"""return the date/time where this view should be considered as
modified. Take care of possible related objects modifications.
/!\ must return GMT time /!\
"""
# XXX check view module's file modification time in dev mod ?
ctime = datetime.utcnow()
if self.cache_max_age:
mtime = self._cw.header_if_modified_since()
if mtime:
tdelta = (ctime - mtime)
if tdelta.days * 24*60*60 + tdelta.seconds <= self.cache_max_age:
return mtime
# mtime = ctime will force page rerendering
return ctime
viewmod.View.last_modified = last_modified
# configure default caching
viewmod.View.http_cache_manager = NoHTTPCacheManager
# max-age=0 to actually force revalidation when needed
viewmod.View.cache_max_age = 0
viewmod.StartupView.http_cache_manager = MaxAgeHTTPCacheManager
viewmod.StartupView.cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
### HTTP Cache validator ############################################
def get_validators(headers_in):
"""return a list of http condition validator relevant to this request
"""
result = []
for header, func in VALIDATORS:
value = headers_in.getHeader(header)
if value is not None:
result.append((func, value))
return result
def if_modified_since(ref_date, headers_out):
last_modified = headers_out.getHeader('last-modified')
if last_modified is None:
return True
return ref_date < last_modified
def if_none_match(tags, headers_out):
etag = headers_out.getHeader('etag')
if etag is None:
return True
return not ((etag in tags) or ('*' in tags))
VALIDATORS = [
('if-modified-since', if_modified_since),
#('if-unmodified-since', if_unmodified_since),
('if-none-match', if_none_match),
#('if-modified-since', if_modified_since),
]