Home | Trees | Indices | Help |
|
---|
|
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)4043 """Set the current visit record on the CherryPy request being processed.""" 44 cherrypy.request.tg_visit = visit4548 """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)6873 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)8992 # Bail out if this extension is not running. 93 global _manager 94 if not _manager: 95 return 96 _manager.shutdown() 97 _manager = None98101 """Create the data model of the VisitManager if one exists.""" 102 if _manager: 103 _manager.create_model()104107 """Register a visit tracking plugin. 108 109 These plugins will be called for each request. 110 111 """ 112 _plugins.append(plugin)113116 """Unregister a visit tracking plugin.""" 117 _plugins[:] = [p for p in _plugins if p is not plugin]118 126129 """A tool that automatically tracks visitors.""" 130 133258135 """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.path214 215 @staticmethod217 """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 235261338263 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 277 281283 """Return the visit for this key. 284 285 Return None if the visit doesn't exist or has expired. 286 287 """ 288 raise NotImplementedError289 293295 try: 296 self.lock.acquire() 297 self.queue[visit_key] = expiry 298 finally: 299 self.lock.release()300302 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.")313315 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)
Home | Trees | Indices | Help |
|
---|
Generated by Epydoc 3.0.1 on Fri Jul 19 17:20:03 2019 | http://epydoc.sourceforge.net |