Package turbogears :: Package visit :: Module api

Source Code for Module turbogears.visit.api

  1  # -*- coding: UTF-8 -*- 
  2  """Base API of the TurboGears Visit Framework.""" 
  3   
  4  __all__ = [ 
  5      'BaseVisitManager', 
  6      'Visit', 
  7      'VisitFilter', 
  8      'create_extension_model', 
  9      'current', 
 10      'enable_visit_plugin', 
 11      'set_current', 
 12      'start_extension', 
 13      'shutdown_extension', 
 14  ] 
 15   
 16   
 17  import logging 
 18  try: 
 19      from hashlib import sha1 
 20  except ImportError: 
 21      from sha import new as sha1 
 22  import threading 
 23  import time 
 24  from Cookie import Morsel 
 25  from random import random 
 26  from datetime import timedelta, datetime 
 27   
 28  import cherrypy 
 29  import pkg_resources 
 30   
 31  from cherrypy.filters.basefilter import BaseFilter 
 32  from turbogears import config 
 33  from turbogears.util import load_class 
 34   
 35  log = logging.getLogger("turbogears.visit") 
 36   
 37  # Global VisitManager 
 38  _manager = None 
 39   
 40  # Global list of plugins for the Visit Tracking framework 
 41  _plugins = list() 
42 43 # Accessor functions for getting and setting the current visit information. 44 -def current():
45 """Retrieve the current visit record from the cherrypy request.""" 46 return getattr(cherrypy.request, "tg_visit", None)
47
48 -def set_current(visit):
49 """Set the current visit record on the cherrypy request being processed.""" 50 cherrypy.request.tg_visit = visit
51
52 -def _create_visit_manager(timeout):
53 """Create a VisitManager based on the plugin specified in the config file.""" 54 plugin_name = config.get("visit.manager", "sqlalchemy") 55 plugins = pkg_resources.iter_entry_points( 56 "turbogears.visit.manager", plugin_name) 57 log.debug("Loading visit manager from plugin: %s", plugin_name) 58 provider_class = None 59 for entrypoint in plugins: 60 try: 61 provider_class = entrypoint.load() 62 break 63 except ImportError, e: 64 log.error("Error loading visit plugin '%s': %s", entrypoint, e) 65 66 if not provider_class and '.' in plugin_name: 67 try: 68 provider_class = load_class(plugin_name) 69 except ImportError, e: 70 log.error("Error loading visit class '%s': %s", plugin_name, e) 71 if not provider_class: 72 raise RuntimeError("VisitManager plugin missing: %s" % plugin_name) 73 return provider_class(timeout)
74
75 76 # Interface for the TurboGears extension 77 78 -def start_extension():
79 global _manager 80 81 # Bail out if the application hasn't enabled this extension 82 if not config.get("visit.on", False): 83 return 84 85 # Bail out if this extension is already running 86 if _manager: 87 log.warning("Visit manager already running.") 88 return 89 90 # How long may the visit be idle before a new visit ID is assigned? 91 # The default is 20 minutes. 92 timeout = timedelta(minutes=config.get("visit.timeout", 20)) 93 log.info("Visit Tracking starting (timeout = %i sec).", timeout.seconds) 94 # Create the thread that manages updating the visits 95 _manager = _create_visit_manager(timeout) 96 97 visit_filter = VisitFilter() 98 # Install Filter into the root filter chain 99 if not hasattr(cherrypy.root, "_cp_filters"): 100 cherrypy.root._cp_filters = list() 101 if not visit_filter in cherrypy.root._cp_filters: 102 cherrypy.root._cp_filters.append(visit_filter)
103
104 -def shutdown_extension():
105 # Bail out if this extension is not running. 106 global _manager 107 if not _manager: 108 return 109 log.info("Visit Tracking shutting down.") 110 _manager.shutdown() 111 _manager = None
112
113 -def create_extension_model():
114 """Create the data model of the VisitManager if one exists.""" 115 if _manager: 116 _manager.create_model()
117
118 -def enable_visit_plugin(plugin):
119 """Register a visit tracking plugin. 120 121 These plugins will be called for each request. 122 123 """ 124 _plugins.append(plugin)
125
126 127 -class Visit(object):
128 """Basic container for visit related data.""" 129
130 - def __init__(self, key, is_new):
131 self.key = key 132 self.is_new = is_new
133
134 135 -class VisitFilter(BaseFilter):
136 """A filter that automatically tracks visitors.""" 137
138 - def __init__(self):
139 get = config.get 140 # Where to look for the session key in the request and in which order 141 self.source = [s.strip().lower() for s in 142 get("visit.source", "cookie").split(',')] 143 if set(self.source).difference(('cookie', 'form')): 144 log.error("Unsupported visit.source in configuration.") 145 # Get the name to use for the identity cookie. 146 self.cookie_name = get("visit.cookie.name", "tg-visit") 147 if Morsel().isReservedKey(self.cookie_name): 148 log.error("Reserved name chosen as visit.cookie.name.") 149 # and the name of the request param. MUST NOT contain dashes or dots, 150 # otherwise the NestedVariablesFilter will choke on it. 151 self.visit_key_param = get("visit.form.name", "tg_visit") 152 # TODO: The path should probably default to whatever 153 # the root is masquerading as in the event of a 154 # virtual path filter. 155 self.cookie_path = get("visit.cookie.path", "/") 156 # The secure bit should be set for HTTPS only sites 157 self.cookie_secure = get("visit.cookie.secure", False) 158 # By default, I don't specify the cookie domain. 159 self.cookie_domain = get("visit.cookie.domain", None) 160 if self.cookie_domain == "localhost": 161 log.error("Invalid value 'localhost' for visit.cookie.domain." 162 " Try None instead.") 163 # Use max age only if the cookie shall explicitly be permanent 164 self.cookie_max_age = get("visit.cookie.permanent", 165 False) and int(get("visit.timeout", "20")) * 60 or None 166 # Use httponly to specify that the cookie shall only be transfered 167 # in HTTP requests, and shall not be accessible through JavaScript. 168 # This is intended to mitigate some forms of cross-site scripting. 169 self.cookie_httponly = get("visit.cookie.httponly", False) 170 if self.cookie_httponly and not Morsel().isReservedKey('httponly'): 171 # Python versions < 2.6 do not support the httponly key 172 log.error("The visit.cookie.httponly setting" 173 " is not supported by this Python version.") 174 self.cookie_httponly = False 175 log.info("Visit filter initialized")
176
177 - def before_main(self):
178 """Check whether submitted request belongs to an existing visit.""" 179 if not config.get("visit.on", True): 180 set_current(None) 181 return 182 visit = current() 183 if not visit: 184 visit_key = None 185 for source in self.source: 186 if source == 'cookie': 187 visit_key = cherrypy.request.simple_cookie.get( 188 self.cookie_name) 189 if visit_key: 190 visit_key = visit_key.value 191 log.debug("Retrieved visit key '%s' from cookie '%s'.", 192 visit_key, self.cookie_name) 193 elif source == 'form': 194 visit_key = cherrypy.request.params.pop( 195 self.visit_key_param, None) 196 log.debug( 197 "Retrieved visit key '%s' from request param '%s'.", 198 visit_key, self.visit_key_param) 199 if visit_key: 200 visit = _manager.visit_for_key(visit_key) 201 break 202 if visit: 203 log.debug("Using visit from request with key: %s", visit_key) 204 else: 205 visit_key = self._generate_key() 206 visit = _manager.new_visit_with_key(visit_key) 207 log.debug("Created new visit with key: %s", visit_key) 208 self.send_cookie(visit_key) 209 set_current(visit) 210 # Inform all the plugins that a request has been made for the current 211 # visit. This gives plugins the opportunity to track click-path or 212 # retrieve the visitor's identity. 213 try: 214 for plugin in _plugins: 215 plugin.record_request(visit) 216 except cherrypy.InternalRedirect, e: 217 # Can't allow an InternalRedirect here because CherryPy is dumb, 218 # instead change cherrypy.request.object_path to the url desired. 219 cherrypy.request.object_path = e.path
220 221 @staticmethod
222 - def _generate_key():
223 """Return a (pseudo)random hash based on seed.""" 224 # Adding remoteHost and remotePort doesn't make this any more secure, 225 # but it makes people feel secure... It's not like I check to make 226 # certain you're actually making requests from that host and port. So 227 # it's basically more noise. 228 key_string = '%s%s%s%s' % (random(), datetime.now(), 229 cherrypy.request.remote_host, cherrypy.request.remote_port) 230 return sha1(key_string).hexdigest()
231 241
264
265 266 -class BaseVisitManager(threading.Thread):
267
268 - def __init__(self, timeout):
269 super(BaseVisitManager, self).__init__(name="VisitManager") 270 self.timeout = timeout 271 self.queue = dict() 272 self.lock = threading.Lock() 273 self._shutdown = threading.Event() 274 self.interval = config.get('visit.interval', 30) # seconds 275 # We must create the visit model before the manager thread is started. 276 self.create_model() 277 self.setDaemon(True) 278 self.start()
279
280 - def create_model(self):
281 pass
282
283 - def new_visit_with_key(self, visit_key):
284 """Return a new Visit object with the given key.""" 285 raise NotImplementedError
286
287 - def visit_for_key(self, visit_key):
288 """Return the visit for this key. 289 290 Return None if the visit doesn't exist or has expired. 291 292 """ 293 raise NotImplementedError
294
295 - def update_queued_visits(self, queue):
296 """Extend the expiration of the queued visits.""" 297 raise NotImplementedError
298
299 - def update_visit(self, visit_key, expiry):
300 try: 301 self.lock.acquire() 302 self.queue[visit_key] = expiry 303 finally: 304 self.lock.release()
305
306 - def shutdown(self, timeout=None):
307 try: 308 self.lock.acquire() 309 self._shutdown.set() 310 self.join(timeout) 311 finally: 312 self.lock.release() 313 if self.isAlive(): 314 log.error("Visit Manager thread failed to shutdown.")
315
316 - def run(self):
317 while not self._shutdown.isSet(): 318 self.lock.acquire() 319 if self._shutdown.isSet(): 320 self.lock.release() 321 continue 322 queue = None 323 try: 324 # make a copy of the queue and empty the original 325 if self.queue: 326 queue = self.queue.copy() 327 self.queue.clear() 328 if queue is not None: 329 self.update_queued_visits(queue) 330 finally: 331 self.lock.release() 332 self._shutdown.wait(self.interval)
333