Package turbogears :: Package visit :: Module api

Source Code for Module turbogears.visit.api

  1  import logging 
  2  import sha 
  3  import threading 
  4  import time 
  5   
  6  try: 
  7      set() 
  8  except NameError: 
  9      from sets import Set as set 
 10   
 11  from random import random 
 12  from datetime import timedelta, datetime 
 13   
 14  import cherrypy 
 15  import pkg_resources 
 16   
 17  from cherrypy.filters.basefilter import BaseFilter 
 18  from turbogears import config 
 19   
 20  log = logging.getLogger("turbogears.visit") 
 21   
 22  # Global VisitManager 
 23  _manager = None 
 24   
 25  # Global list of plugins for the Visit Tracking framework 
 26  _plugins = list() 
 27   
 28  # Accessor functions for getting and setting the current visit information. 
29 -def current():
30 """Retrieve the current visit record from the cherrypy request.""" 31 return getattr(cherrypy.request, "tg_visit", None)
32
33 -def set_current(visit):
34 """Set the current visit record on the cherrypy request being processed.""" 35 cherrypy.request.tg_visit = visit
36
37 -def _create_visit_manager(timeout):
38 """Create a VisitManager based on the plugin specified in the config file.""" 39 plugin_name = config.get("visit.manager", "sqlobject") 40 plugins = pkg_resources.iter_entry_points( 41 "turbogears.visit.manager", plugin_name) 42 log.debug("Loading visit manager from plugin: %s", plugin_name) 43 for entrypoint in plugins: 44 plugin = entrypoint.load() 45 return plugin(timeout) 46 raise RuntimeError("VisitManager plugin missing: %s" % plugin_name)
47
48 -class MonkeyDecodingFilter(BaseFilter):
49
50 - def decode(from_enc):
51 """Recursively decode all values in an iterable from specified encoding.""" 52 53 def decode_from(value, from_enc): 54 if isinstance(value, dict): 55 for k, v in value.items(): 56 value[k] = decode_from(v, from_enc) 57 elif isinstance(value, list): 58 newlist = list() 59 for item in value: 60 newlist.append(decode_from(item, from_enc)) 61 value = newlist 62 elif isinstance(value, str): 63 return value.decode(from_enc) 64 return value
65 66 decoded_params = decode_from(cherrypy.request.params, from_enc) 67 # This is done in two steps to make sure the exception in 68 # before_main can retry a decode with another encoding if needed. 69 # DON'T merge those two lines. 70 cherrypy.request.params = decoded_params
71 decode = staticmethod(decode) 72
73 - def before_main(self):
74 """Monkey patch CherryPy's decoding filter.""" 75 conf = cherrypy.config.get 76 if not conf('decoding_filter.on', False): 77 return 78 if getattr(cherrypy.request, "_decoding_attempted", False): 79 return 80 cherrypy.request._decoding_attempted = True 81 enc = conf('decoding_filter.encoding', None) 82 if not enc: 83 ct = cherrypy.request.headers.elements("Content-Type") 84 if ct: 85 ct = ct[0] 86 enc = ct.params.get("charset", None) 87 if not enc and ct.value.lower().startswith("text/"): 88 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1 89 # When no explicit charset parameter is provided by the 90 # sender, media subtypes of the "text" type are defined 91 # to have a default charset value of "ISO-8859-1" when 92 # received via HTTP. 93 enc = "ISO-8859-1" 94 if not enc: 95 enc = conf('decoding_filter.default_encoding', "utf-8") 96 try: 97 self.decode(enc) 98 except UnicodeDecodeError: 99 # IE and Firefox don't supply a charset when submitting form 100 # params with a CT of application/x-www-form-urlencoded. 101 # So after all our guessing, it could *still* be wrong. 102 # Start over with ISO-8859-1, since that seems to be preferred. 103 self.decode("ISO-8859-1")
104 105 106 # Interface for the TurboGears extension 107
108 -def start_extension():
109 # TODO: This should alway be active even when visit is not on, 110 # we should find a better place to load this code... 111 112 # Monkey patch CP Decoding filter 113 # TODO: Is there a better way to inject this? 114 # (maybe earlier than start_extension?) 115 # monkey inject our replacement filter into the CP2 filter chain 116 decoding_filter = MonkeyDecodingFilter() 117 for index, active_filter in enumerate( 118 cherrypy.filters._filterhooks.get('before_main', [])): 119 if active_filter.im_class == \ 120 cherrypy.filters.decodingfilter.DecodingFilter: 121 cherrypy.filters._filterhooks['before_main'].pop(index) 122 cherrypy.filters._filterhooks['before_main'].insert( 123 index, decoding_filter.before_main) 124 125 # Here is the real visit code. 126 # Bail out if the application hasn't enabled this extension 127 if not config.get("visit.on", False): 128 return 129 # Bail out if this extension is already running 130 global _manager 131 if _manager: 132 return 133 134 log.info("Visit Tracking starting") 135 # How long may the visit be idle before a new visit ID is assigned? 136 # The default is 20 minutes. 137 timeout = timedelta(minutes=config.get("visit.timeout", 20)) 138 # Create the thread that manages updating the visits 139 _manager = _create_visit_manager(timeout) 140 141 visit_filter = VisitFilter() 142 # Install Filter into the root filter chain 143 if not hasattr(cherrypy.root, "_cp_filters"): 144 cherrypy.root._cp_filters = list() 145 cherrypy.root._cp_filters.append(visit_filter)
146
147 -def shutdown_extension():
148 # Bail out if this extension is not running. 149 global _manager 150 if not _manager: 151 return 152 log.info("Visit Tracking shutting down") 153 _manager.shutdown() 154 _manager = None
155
156 -def create_extension_model():
157 """Create the data model of the VisitManager if one exists.""" 158 if _manager: 159 _manager.create_model()
160
161 -def enable_visit_plugin(plugin):
162 """Register a visit tracking plugin. 163 164 These plugins will be called for each request. 165 166 """ 167 _plugins.append(plugin)
168
169 -class Visit(object):
170 """Basic container for visit related data.""" 171
172 - def __init__(self, key, is_new):
173 self.key = key 174 self.is_new = is_new
175 176
177 -class VisitFilter(BaseFilter):
178 """A filter that automatically tracks visitors.""" 179
180 - def __init__(self):
181 log.info("Visit filter initialised") 182 get = config.get 183 # Where to look for the session key in the request and in which order 184 self.source = [s.strip().lower() for s in 185 get("visit.source", "cookie").split(',')] 186 if set(self.source).difference(('cookie', 'form')): 187 log.warning("Unsupported 'visit.source' '%s' in configuration.") 188 # Get the name to use for the identity cookie. 189 self.cookie_name = get("visit.cookie.name", "tg-visit") 190 # and the name of the request param. MUST NOT contain dashes or dots, 191 # otherwise the NestedVariablesFilter will chocke on it. 192 self.visit_key_param = get("visit.form.name", "tg_visit") 193 # TODO: The path should probably default to whatever 194 # the root is masquerading as in the event of a 195 # virtual path filter. 196 self.cookie_path = get("visit.cookie.path", "/") 197 # The secure bit should be set for HTTPS only sites 198 self.cookie_secure = get("visit.cookie.secure", False) 199 # By default, I don't specify the cookie domain. 200 self.cookie_domain = get("visit.cookie.domain", None) 201 assert self.cookie_domain != "localhost", "localhost" \ 202 " is not a valid value for visit.cookie.domain. Try None instead." 203 # Use max age only if the cookie shall explicitly be permanent 204 self.cookie_max_age = get("visit.cookie.permanent", 205 False) and int(get("visit.timeout", "20")) * 60 or None
206
207 - def before_main(self):
208 """Check whether submitted request belongs to an existing visit.""" 209 if not config.get("visit.on", True): 210 set_current(None) 211 return 212 cpreq = cherrypy.request 213 visit = current() 214 if not visit: 215 visit_key = None 216 for source in self.source: 217 if source == 'cookie': 218 visit_key = cpreq.simple_cookie.get(self.cookie_name) 219 if visit_key: 220 visit_key = visit_key.value 221 log.debug("Retrieved visit key '%s' from cookie '%s'.", 222 visit_key, self.cookie_name) 223 elif source == 'form': 224 visit_key = cpreq.params.pop(self.visit_key_param, None) 225 log.debug( 226 "Retrieved visit key '%s' from request param '%s'.", 227 visit_key, self.visit_key_param) 228 if visit_key: 229 visit = _manager.visit_for_key(visit_key) 230 break 231 if not visit: 232 visit_key = self._generate_key() 233 visit = _manager.new_visit_with_key(visit_key) 234 log.debug("Created new visit with key: %s", visit_key) 235 else: 236 log.debug("Using visit from request with key: %s", visit_key) 237 self.send_cookie(visit_key) 238 set_current(visit) 239 # Inform all the plugins that a request has been made for the current 240 # visit. This gives plugins the opportunity to track click-path or 241 # retrieve the visitor's identity. 242 try: 243 for plugin in _plugins: 244 plugin.record_request(visit) 245 except cherrypy.InternalRedirect, e: 246 # Can't allow an InternalRedirect here because CherryPy is dumb, 247 # instead change cherrypy.request.object_path to the url desired. 248 cherrypy.request.object_path = e.path
249
250 - def _generate_key():
251 """Return a (pseudo)random hash based on seed.""" 252 # Adding remoteHost and remotePort doesn't make this any more secure, 253 # but it makes people feel secure... It's not like I check to make 254 # certain you're actually making requests from that host and port. So 255 # it's basically more noise. 256 key_string = '%s%s%s%s' % (random(), datetime.now(), 257 cherrypy.request.remote_host, cherrypy.request.remote_port) 258 return sha.new(key_string).hexdigest()
259 _generate_key = staticmethod(_generate_key) 260 270
291 292
293 -class BaseVisitManager(threading.Thread):
294
295 - def __init__(self, timeout):
296 super(BaseVisitManager, self).