1 """CherryPy dispatchers.
2
3 A 'dispatcher' is the object which looks up the 'page handler' callable
4 and collects config for the current request based on the path_info, other
5 request attributes, and the application architecture. The core calls the
6 dispatcher as early as possible, passing it a 'path_info' argument.
7
8 The default dispatcher discovers the page handler by matching path_info
9 to a hierarchical arrangement of objects, starting at request.app.root.
10 """
11
12 import cherrypy
13
14
15 -class PageHandler(object):
16 """Callable which sets response.body."""
17
18 - def __init__(self, callable, *args, **kwargs):
22
24 try:
25 return self.callable(*self.args, **self.kwargs)
26 except TypeError, x:
27 test_callable_spec(self.callable, self.args, self.kwargs)
28 raise
29
31 """
32 Inspect callable and test to see if the given args are suitable for it.
33
34 When an error occurs during the handler's invoking stage there are 2
35 erroneous cases:
36 1. Too many parameters passed to a function which doesn't define
37 one of *args or **kwargs.
38 2. Too little parameters are passed to the function.
39
40 There are 3 sources of parameters to a cherrypy handler.
41 1. query string parameters are passed as keyword parameters to the handler.
42 2. body parameters are also passed as keyword parameters.
43 3. when partial matching occurs, the final path atoms are passed as
44 positional args.
45 Both the query string and path atoms are part of the URI. If they are
46 incorrect, then a 404 Not Found should be raised. Conversely the body
47 parameters are part of the request; if they are invalid a 400 Bad Request.
48 """
49 (args, varargs, varkw, defaults) = inspect.getargspec(callable)
50
51 if args and args[0] == 'self':
52 args = args[1:]
53
54 arg_usage = dict([(arg, 0,) for arg in args])
55 vararg_usage = 0
56 varkw_usage = 0
57 extra_kwargs = set()
58
59 for i, value in enumerate(callable_args):
60 try:
61 arg_usage[args[i]] += 1
62 except IndexError:
63 vararg_usage += 1
64
65 for key in callable_kwargs.keys():
66 try:
67 arg_usage[key] += 1
68 except KeyError:
69 varkw_usage += 1
70 extra_kwargs.add(key)
71
72 for i, val in enumerate(defaults or []):
73
74 if arg_usage[args[i]] == 0:
75 arg_usage[args[i]] += 1
76
77 missing_args = []
78 multiple_args = []
79 for key, usage in arg_usage.iteritems():
80 if usage == 0:
81 missing_args.append(key)
82 elif usage > 1:
83 multiple_args.append(key)
84
85 if missing_args:
86
87
88
89
90
91
92
93
94
95
96
97
98 raise cherrypy.HTTPError(404,
99 message="Missing parameters: %s" % ",".join(missing_args))
100
101
102 if not varargs and vararg_usage > 0:
103 raise cherrypy.HTTPError(404)
104
105 body_params = cherrypy.request.body_params or {}
106 body_params = set(body_params.keys())
107 qs_params = set(callable_kwargs.keys()) - body_params
108
109 if multiple_args:
110
111 if qs_params.intersection(set(multiple_args)):
112
113
114 error = 404
115 else:
116
117 error = 400
118
119 raise cherrypy.HTTPError(error,
120 message="Multiple values for parameters: "\
121 "%s" % ",".join(multiple_args))
122
123 if not varkw and varkw_usage > 0:
124
125
126 extra_qs_params = set(qs_params).intersection(extra_kwargs)
127 if extra_qs_params:
128 raise cherrypy.HTTPError(404,
129 message="Unexpected query string "\
130 "parameters: %s" % ", ".join(extra_qs_params))
131
132
133 extra_body_params = set(body_params).intersection(extra_kwargs)
134 if extra_body_params:
135 raise cherrypy.HTTPError(400,
136 message="Unexpected body parameters: "\
137 "%s" % ", ".join(extra_body_params))
138
139
140 try:
141 import inspect
142 except ImportError:
143 test_callable_spec = lambda callable, args, kwargs: None
144
145
146
147 -class LateParamPageHandler(PageHandler):
148 """When passing cherrypy.request.params to the page handler, we do not
149 want to capture that dict too early; we want to give tools like the
150 decoding tool a chance to modify the params dict in-between the lookup
151 of the handler and the actual calling of the handler. This subclass
152 takes that into account, and allows request.params to be 'bound late'
153 (it's more complicated than that, but that's the effect).
154 """
155
156 - def _get_kwargs(self):
157 kwargs = cherrypy.request.params.copy()
158 if self._kwargs:
159 kwargs.update(self._kwargs)
160 return kwargs
161
162 - def _set_kwargs(self, kwargs):
163 self._kwargs = kwargs
164
165 kwargs = property(_get_kwargs, _set_kwargs,
166 doc='page handler kwargs (with '
167 'cherrypy.request.params copied in)')
168
169
171 """CherryPy Dispatcher which walks a tree of objects to find a handler.
172
173 The tree is rooted at cherrypy.request.app.root, and each hierarchical
174 component in the path_info argument is matched to a corresponding nested
175 attribute of the root object. Matching handlers must have an 'exposed'
176 attribute which evaluates to True. The special method name "index"
177 matches a URI which ends in a slash ("/"). The special method name
178 "default" may match a portion of the path_info (but only when no longer
179 substring of the path_info matches some other object).
180
181 This is the default, built-in dispatcher for CherryPy.
182 """
183
195
197 """Return the appropriate page handler, plus any virtual path.
198
199 This will return two objects. The first will be a callable,
200 which can be used to generate page output. Any parameters from
201 the query string or request body will be sent to that callable
202 as keyword arguments.
203
204 The callable is found by traversing the application's tree,
205 starting from cherrypy.request.app.root, and matching path
206 components to successive objects in the tree. For example, the
207 URL "/path/to/handler" might return root.path.to.handler.
208
209 The second object returned will be a list of names which are
210 'virtual path' components: parts of the URL which are dynamic,
211 and were not used when looking up the handler.
212 These virtual path components are passed to the handler as
213 positional arguments.
214 """
215 request = cherrypy.request
216 app = request.app
217 root = app.root
218
219
220 curpath = ""
221 nodeconf = {}
222 if hasattr(root, "_cp_config"):
223 nodeconf.update(root._cp_config)
224 if "/" in app.config:
225 nodeconf.update(app.config["/"])
226 object_trail = [['root', root, nodeconf, curpath]]
227
228 node = root
229 names = [x for x in path.strip('/').split('/') if x] + ['index']
230 for name in names:
231
232 objname = name.replace('.', '_')
233
234 nodeconf = {}
235 node = getattr(node, objname, None)
236 if node is not None:
237
238 if hasattr(node, "_cp_config"):
239 nodeconf.update(node._cp_config)
240
241
242 curpath = "/".join((curpath, name))
243 if curpath in app.config:
244 nodeconf.update(app.config[curpath])
245
246 object_trail.append([name, node, nodeconf, curpath])
247
248 def set_conf():
249 """Collapse all object_trail config into cherrypy.request.config."""
250 base = cherrypy.config.copy()
251
252
253 for name, obj, conf, curpath in object_trail:
254 base.update(conf)
255 if 'tools.staticdir.dir' in conf:
256 base['tools.staticdir.section'] = curpath
257 return base
258
259
260 num_candidates = len(object_trail) - 1
261 for i in xrange(num_candidates, -1, -1):
262
263 name, candidate, nodeconf, curpath = object_trail[i]
264 if candidate is None:
265 continue
266
267
268 if hasattr(candidate, "default"):
269 defhandler = candidate.default
270 if getattr(defhandler, 'exposed', False):
271
272 conf = getattr(defhandler, "_cp_config", {})
273 object_trail.insert(i+1, ["default", defhandler, conf, curpath])
274 request.config = set_conf()
275
276 request.is_index = path.endswith("/")
277 return defhandler, names[i:-1]
278
279
280
281
282
283 if getattr(candidate, 'exposed', False):
284 request.config = set_conf()
285 if i == num_candidates:
286
287
288 request.is_index = True
289 else:
290
291
292
293
294 request.is_index = False
295 return candidate, names[i:-1]
296
297
298 request.config = set_conf()
299 return None, []
300
301
303 """Additional dispatch based on cherrypy.request.method.upper().
304
305 Methods named GET, POST, etc will be called on an exposed class.
306 The method names must be all caps; the appropriate Allow header
307 will be output showing all capitalized method names as allowable
308 HTTP verbs.
309
310 Note that the containing class must be exposed, not the methods.
311 """
312
339
340
342 """A Routes based dispatcher for CherryPy."""
343
345 """
346 Routes dispatcher
347
348 Set full_result to True if you wish the controller
349 and the action to be passed on to the page handler
350 parameters. By default they won't be.
351 """
352 import routes
353 self.full_result = full_result
354 self.controllers = {}
355 self.mapper = routes.Mapper()
356 self.mapper.controller_scan = self.controllers.keys
357
358 - def connect(self, name, route, controller, **kwargs):
361
364
372
406
407 app = request.app
408 root = app.root
409 if hasattr(root, "_cp_config"):
410 merge(root._cp_config)
411 if "/" in app.config:
412 merge(app.config["/"])
413
414
415 atoms = [x for x in path_info.split("/") if x]
416 if atoms:
417 last = atoms.pop()
418 else:
419 last = None
420 for atom in atoms:
421 curpath = "/".join((curpath, atom))
422 if curpath in app.config:
423 merge(app.config[curpath])
424
425 handler = None
426 if result:
427 controller = result.get('controller', None)
428 controller = self.controllers.get(controller)
429 if controller:
430
431 if hasattr(controller, "_cp_config"):
432 merge(controller._cp_config)
433
434 action = result.get('action', None)
435 if action is not None:
436 handler = getattr(controller, action, None)
437
438 if hasattr(handler, "_cp_config"):
439 merge(handler._cp_config)
440
441
442
443 if last:
444 curpath = "/".join((curpath, last))
445 if curpath in app.config:
446 merge(app.config[curpath])
447
448 return handler
449
450
456 return xmlrpc_dispatch
457
458
460 """Select a different handler based on the Host header.
461
462 This can be useful when running multiple sites within one CP server.
463 It allows several domains to point to different parts of a single
464 website structure. For example:
465
466 http://www.domain.example -> root
467 http://www.domain2.example -> root/domain2/
468 http://www.domain2.example:443 -> root/secure
469
470 can be accomplished via the following config:
471
472 [/]
473 request.dispatch = cherrypy.dispatch.VirtualHost(
474 **{'www.domain2.example': '/domain2',
475 'www.domain2.example:443': '/secure',
476 })
477
478 next_dispatcher: the next dispatcher object in the dispatch chain.
479 The VirtualHost dispatcher adds a prefix to the URL and calls
480 another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
481
482 use_x_forwarded_host: if True (the default), any "X-Forwarded-Host"
483 request header will be used instead of the "Host" header. This
484 is commonly added by HTTP servers (such as Apache) when proxying.
485
486 **domains: a dict of {host header value: virtual prefix} pairs.
487 The incoming "Host" request header is looked up in this dict,
488 and, if a match is found, the corresponding "virtual prefix"
489 value will be prepended to the URL path before calling the
490 next dispatcher. Note that you often need separate entries
491 for "example.com" and "www.example.com". In addition, "Host"
492 headers may contain the port number.
493 """
494 from cherrypy.lib import http
495 def vhost_dispatch(path_info):
496 header = cherrypy.request.headers.get
497
498 domain = header('Host', '')
499 if use_x_forwarded_host:
500 domain = header("X-Forwarded-Host", domain)
501
502 prefix = domains.get(domain, "")
503 if prefix:
504 path_info = http.urljoin(prefix, path_info)
505
506 result = next_dispatcher(path_info)
507
508
509 section = cherrypy.request.config.get('tools.staticdir.section')
510 if section:
511 section = section[len(prefix):]
512 cherrypy.request.config['tools.staticdir.section'] = section
513
514 return result
515 return vhost_dispatch
516