1
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
38 _manager = None
39
40
41 _plugins = list()
45 """Retrieve the current visit record from the cherrypy request."""
46 return getattr(cherrypy.request, "tg_visit", None)
47
49 """Set the current visit record on the cherrypy request being processed."""
50 cherrypy.request.tg_visit = visit
51
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
79 global _manager
80
81
82 if not config.get("visit.on", False):
83 return
84
85
86 if _manager:
87 log.warning("Visit manager already running.")
88 return
89
90
91
92 timeout = timedelta(minutes=config.get("visit.timeout", 20))
93 log.info("Visit Tracking starting (timeout = %i sec).", timeout.seconds)
94
95 _manager = _create_visit_manager(timeout)
96
97 visit_filter = VisitFilter()
98
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
112
117
119 """Register a visit tracking plugin.
120
121 These plugins will be called for each request.
122
123 """
124 _plugins.append(plugin)
125
128 """Basic container for visit related data."""
129
131 self.key = key
132 self.is_new = is_new
133
136 """A filter that automatically tracks visitors."""
137
139 get = config.get
140
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
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
150
151 self.visit_key_param = get("visit.form.name", "tg_visit")
152
153
154
155 self.cookie_path = get("visit.cookie.path", "/")
156
157 self.cookie_secure = get("visit.cookie.secure", False)
158
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
164 self.cookie_max_age = get("visit.cookie.permanent",
165 False) and int(get("visit.timeout", "20")) * 60 or None
166
167
168
169 self.cookie_httponly = get("visit.cookie.httponly", False)
170 if self.cookie_httponly and not Morsel().isReservedKey('httponly'):
171
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
211
212
213 try:
214 for plugin in _plugins:
215 plugin.record_request(visit)
216 except cherrypy.InternalRedirect, e:
217
218
219 cherrypy.request.object_path = e.path
220
221 @staticmethod
223 """Return a (pseudo)random hash based on seed."""
224
225
226
227
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
233 """Clear any existing visit ID cookie."""
234 cookies = cherrypy.response.simple_cookie
235
236 log.debug("Clearing visit ID cookie")
237 cookies[self.cookie_name] = ''
238 cookies[self.cookie_name]['path'] = self.cookie_path
239 cookies[self.cookie_name]['expires'] = ''
240 cookies[self.cookie_name]['max-age'] = 0
241
243 """Send an visit ID cookie back to the browser."""
244 cookies = cherrypy.response.simple_cookie
245 cookies[self.cookie_name] = visit_key
246 cookies[self.cookie_name]['path'] = self.cookie_path
247 if self.cookie_secure:
248 cookies[self.cookie_name]['secure'] = True
249 if self.cookie_domain:
250 cookies[self.cookie_name]['domain'] = self.cookie_domain
251 max_age = self.cookie_max_age
252 if max_age:
253
254 cookies[self.cookie_name]['expires'] = '"%s"' % time.strftime(
255 "%a, %d-%b-%Y %H:%M:%S GMT",
256 time.gmtime(time.time() + max_age))
257
258
259 cookies[self.cookie_name]['max-age'] = max_age
260 if self.cookie_httponly:
261 cookies[self.cookie_name]['httponly'] = True
262 log.debug("Sending visit ID cookie: %s",
263 cookies[self.cookie_name].output())
264
267
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)
275
276 self.create_model()
277 self.setDaemon(True)
278 self.start()
279
282
284 """Return a new Visit object with the given key."""
285 raise NotImplementedError
286
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
296 """Extend the expiration of the queued visits."""
297 raise NotImplementedError
298
305
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
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
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