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
23 _manager = None
24
25
26 _plugins = list()
27
28
30 """Retrieve the current visit record from the cherrypy request."""
31 return getattr(cherrypy.request, "tg_visit", None)
32
34 """Set the current visit record on the cherrypy request being processed."""
35 cherrypy.request.tg_visit = visit
36
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
49
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
68
69
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
89
90
91
92
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
100
101
102
103 self.decode("ISO-8859-1")
104
105
106
107
109
110
111
112
113
114
115
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
126
127 if not config.get("visit.on", False):
128 return
129
130 global _manager
131 if _manager:
132 return
133
134 log.info("Visit Tracking starting")
135
136
137 timeout = timedelta(minutes=config.get("visit.timeout", 20))
138
139 _manager = _create_visit_manager(timeout)
140
141 visit_filter = VisitFilter()
142
143 if not hasattr(cherrypy.root, "_cp_filters"):
144 cherrypy.root._cp_filters = list()
145 cherrypy.root._cp_filters.append(visit_filter)
146
155
160
162 """Register a visit tracking plugin.
163
164 These plugins will be called for each request.
165
166 """
167 _plugins.append(plugin)
168
170 """Basic container for visit related data."""
171
173 self.key = key
174 self.is_new = is_new
175
176
178 """A filter that automatically tracks visitors."""
179
181 log.info("Visit filter initialised")
182 get = config.get
183
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
189 self.cookie_name = get("visit.cookie.name", "tg-visit")
190
191
192 self.visit_key_param = get("visit.form.name", "tg_visit")
193
194
195
196 self.cookie_path = get("visit.cookie.path", "/")
197
198 self.cookie_secure = get("visit.cookie.secure", False)
199
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
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
240
241
242 try:
243 for plugin in _plugins:
244 plugin.record_request(visit)
245 except cherrypy.InternalRedirect, e:
246
247
248 cherrypy.request.object_path = e.path
249
251 """Return a (pseudo)random hash based on seed."""
252
253
254
255
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
262 """Clear any existing visit ID cookie."""
263 cookies = cherrypy.response.simple_cookie
264
265 log.debug("Clearing visit ID cookie")
266 cookies[self.cookie_name] = ''
267 cookies[self.cookie_name]['path'] = self.cookie_path
268 cookies[self.cookie_name]['expires'] = ''
269 cookies[self.cookie_name]['max-age'] = 0
270
272 """Send an visit ID cookie back to the browser."""
273 cookies = cherrypy.response.simple_cookie
274 cookies[self.cookie_name] = visit_key
275 cookies[self.cookie_name]['path'] = self.cookie_path
276 if self.cookie_secure:
277 cookies[self.cookie_name]['secure'] = True
278 if self.cookie_domain:
279 cookies[self.cookie_name]['domain'] = self.cookie_domain
280 max_age = self.cookie_max_age
281 if max_age:
282
283 cookies[self.cookie_name]['expires'] = time.strftime(
284 "%a, %d-%b-%Y %H:%M:%S GMT",
285 time.gmtime(time.time() + max_age))
286
287
288 cookies[self.cookie_name]['max-age'] = max_age
289 log.debug("Sending visit ID cookie: %s",
290 cookies[self.cookie_name].output())
291
292