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
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.warning("Unsupported 'visit.source' '%s' in configuration.")
145
146 self.cookie_name = get("visit.cookie.name", "tg-visit")
147
148
149 self.visit_key_param = get("visit.form.name", "tg_visit")
150
151
152
153 self.cookie_path = get("visit.cookie.path", "/")
154
155 self.cookie_secure = get("visit.cookie.secure", False)
156
157 self.cookie_domain = get("visit.cookie.domain", None)
158 assert self.cookie_domain != "localhost", "localhost" \
159 " is not a valid value for visit.cookie.domain. Try None instead."
160
161 self.cookie_max_age = get("visit.cookie.permanent",
162 False) and int(get("visit.timeout", "20")) * 60 or None
163 log.info("Visit filter initialized")
164
165 - def before_main(self):
166 """Check whether submitted request belongs to an existing visit."""
167 if not config.get("visit.on", True):
168 set_current(None)
169 return
170 visit = current()
171 if not visit:
172 visit_key = None
173 for source in self.source:
174 if source == 'cookie':
175 visit_key = cherrypy.request.simple_cookie.get(
176 self.cookie_name)
177 if visit_key:
178 visit_key = visit_key.value
179 log.debug("Retrieved visit key '%s' from cookie '%s'.",
180 visit_key, self.cookie_name)
181 elif source == 'form':
182 visit_key = cherrypy.request.params.pop(
183 self.visit_key_param, None)
184 log.debug(
185 "Retrieved visit key '%s' from request param '%s'.",
186 visit_key, self.visit_key_param)
187 if visit_key:
188 visit = _manager.visit_for_key(visit_key)
189 break
190 if visit:
191 log.debug("Using visit from request with key: %s", visit_key)
192 else:
193 visit_key = self._generate_key()
194 visit = _manager.new_visit_with_key(visit_key)
195 log.debug("Created new visit with key: %s", visit_key)
196 self.send_cookie(visit_key)
197 set_current(visit)
198
199
200
201 try:
202 for plugin in _plugins:
203 plugin.record_request(visit)
204 except cherrypy.InternalRedirect, e:
205
206
207 cherrypy.request.object_path = e.path
208
209 @staticmethod
211 """Return a (pseudo)random hash based on seed."""
212
213
214
215
216 key_string = '%s%s%s%s' % (random(), datetime.now(),
217 cherrypy.request.remote_host, cherrypy.request.remote_port)
218 return sha1(key_string).hexdigest()
219
221 """Clear any existing visit ID cookie."""
222 cookies = cherrypy.response.simple_cookie
223
224 log.debug("Clearing visit ID cookie")
225 cookies[self.cookie_name] = ''
226 cookies[self.cookie_name]['path'] = self.cookie_path
227 cookies[self.cookie_name]['expires'] = ''
228 cookies[self.cookie_name]['max-age'] = 0
229
231 """Send an visit ID cookie back to the browser."""
232 cookies = cherrypy.response.simple_cookie
233 cookies[self.cookie_name] = visit_key
234 cookies[self.cookie_name]['path'] = self.cookie_path
235 if self.cookie_secure:
236 cookies[self.cookie_name]['secure'] = True
237 if self.cookie_domain:
238 cookies[self.cookie_name]['domain'] = self.cookie_domain
239 max_age = self.cookie_max_age
240 if max_age:
241
242 cookies[self.cookie_name]['expires'] = '"%s"' % time.strftime(
243 "%a, %d-%b-%Y %H:%M:%S GMT",
244 time.gmtime(time.time() + max_age))
245
246
247 cookies[self.cookie_name]['max-age'] = max_age
248 log.debug("Sending visit ID cookie: %s",
249 cookies[self.cookie_name].output())
250
253
255 super(BaseVisitManager, self).__init__(name="VisitManager")
256 self.timeout = timeout
257 self.queue = dict()
258 self.lock = threading.Lock()
259 self._shutdown = threading.Event()
260 self.interval = config.get('visit.interval', 30)
261
262 self.create_model()
263 self.setDaemon(True)
264 self.start()
265
268
270 """Return a new Visit object with the given key."""
271 raise NotImplementedError
272
274 """Return the visit for this key.
275
276 Return None if the visit doesn't exist or has expired.
277
278 """
279 raise NotImplementedError
280
282 """Extend the expiration of the queued visits."""
283 raise NotImplementedError
284
291
293 try:
294 self.lock.acquire()
295 self._shutdown.set()
296 self.join(timeout)
297 finally:
298 self.lock.release()
299 if self.isAlive():
300 log.error("Visit Manager thread failed to shutdown.")
301
303 while not self._shutdown.isSet():
304 self.lock.acquire()
305 if self._shutdown.isSet():
306 self.lock.release()
307 continue
308 queue = None
309 try:
310
311 if self.queue:
312 queue = self.queue.copy()
313 self.queue.clear()
314 if queue is not None:
315 self.update_queued_visits(queue)
316 finally:
317 self.lock.release()
318 self._shutdown.wait(self.interval)
319