Package turbogears :: Package visit :: Module api

Source Code for Module turbogears.visit.api

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