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 string
13 import sys
14 import types
15 try:
16 classtype = (type, types.ClassType)
17 except AttributeError:
18 classtype = type
19
20 import cherrypy
21 from cherrypy._cpcompat import set
22
23
24 -class PageHandler(object):
25 """Callable which sets response.body."""
26
27 - def __init__(self, callable, *args, **kwargs):
31
33 try:
34 return self.callable(*self.args, **self.kwargs)
35 except TypeError:
36 x = sys.exc_info()[1]
37 try:
38 test_callable_spec(self.callable, self.args, self.kwargs)
39 except cherrypy.HTTPError:
40 raise sys.exc_info()[1]
41 except:
42 raise x
43 raise
44
45
47 """
48 Inspect callable and test to see if the given args are suitable for it.
49
50 When an error occurs during the handler's invoking stage there are 2
51 erroneous cases:
52 1. Too many parameters passed to a function which doesn't define
53 one of *args or **kwargs.
54 2. Too little parameters are passed to the function.
55
56 There are 3 sources of parameters to a cherrypy handler.
57 1. query string parameters are passed as keyword parameters to the handler.
58 2. body parameters are also passed as keyword parameters.
59 3. when partial matching occurs, the final path atoms are passed as
60 positional args.
61 Both the query string and path atoms are part of the URI. If they are
62 incorrect, then a 404 Not Found should be raised. Conversely the body
63 parameters are part of the request; if they are invalid a 400 Bad Request.
64 """
65 show_mismatched_params = getattr(
66 cherrypy.serving.request, 'show_mismatched_params', False)
67 try:
68 (args, varargs, varkw, defaults) = inspect.getargspec(callable)
69 except TypeError:
70 if isinstance(callable, object) and hasattr(callable, '__call__'):
71 (args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__)
72 else:
73
74
75 raise
76
77 if args and args[0] == 'self':
78 args = args[1:]
79
80 arg_usage = dict([(arg, 0,) for arg in args])
81 vararg_usage = 0
82 varkw_usage = 0
83 extra_kwargs = set()
84
85 for i, value in enumerate(callable_args):
86 try:
87 arg_usage[args[i]] += 1
88 except IndexError:
89 vararg_usage += 1
90
91 for key in callable_kwargs.keys():
92 try:
93 arg_usage[key] += 1
94 except KeyError:
95 varkw_usage += 1
96 extra_kwargs.add(key)
97
98
99 args_with_defaults = args[-len(defaults or []):]
100 for i, val in enumerate(defaults or []):
101
102 if arg_usage[args_with_defaults[i]] == 0:
103 arg_usage[args_with_defaults[i]] += 1
104
105 missing_args = []
106 multiple_args = []
107 for key, usage in arg_usage.items():
108 if usage == 0:
109 missing_args.append(key)
110 elif usage > 1:
111 multiple_args.append(key)
112
113 if missing_args:
114
115
116
117
118
119
120
121
122
123
124
125
126 message = None
127 if show_mismatched_params:
128 message="Missing parameters: %s" % ",".join(missing_args)
129 raise cherrypy.HTTPError(404, message=message)
130
131
132 if not varargs and vararg_usage > 0:
133 raise cherrypy.HTTPError(404)
134
135 body_params = cherrypy.serving.request.body.params or {}
136 body_params = set(body_params.keys())
137 qs_params = set(callable_kwargs.keys()) - body_params
138
139 if multiple_args:
140 if qs_params.intersection(set(multiple_args)):
141
142
143 error = 404
144 else:
145
146 error = 400
147
148 message = None
149 if show_mismatched_params:
150 message="Multiple values for parameters: "\
151 "%s" % ",".join(multiple_args)
152 raise cherrypy.HTTPError(error, message=message)
153
154 if not varkw and varkw_usage > 0:
155
156
157 extra_qs_params = set(qs_params).intersection(extra_kwargs)
158 if extra_qs_params:
159 message = None
160 if show_mismatched_params:
161 message="Unexpected query string "\
162 "parameters: %s" % ", ".join(extra_qs_params)
163 raise cherrypy.HTTPError(404, message=message)
164
165
166 extra_body_params = set(body_params).intersection(extra_kwargs)
167 if extra_body_params:
168 message = None
169 if show_mismatched_params:
170 message="Unexpected body parameters: "\
171 "%s" % ", ".join(extra_body_params)
172 raise cherrypy.HTTPError(400, message=message)
173
174
175 try:
176 import inspect
177 except ImportError:
178 test_callable_spec = lambda callable, args, kwargs: None
179
180
181
182 -class LateParamPageHandler(PageHandler):
183 """When passing cherrypy.request.params to the page handler, we do not
184 want to capture that dict too early; we want to give tools like the
185 decoding tool a chance to modify the params dict in-between the lookup
186 of the handler and the actual calling of the handler. This subclass
187 takes that into account, and allows request.params to be 'bound late'
188 (it's more complicated than that, but that's the effect).
189 """
190
191 - def _get_kwargs(self):
192 kwargs = cherrypy.serving.request.params.copy()
193 if self._kwargs:
194 kwargs.update(self._kwargs)
195 return kwargs
196
197 - def _set_kwargs(self, kwargs):
198 self._kwargs = kwargs
199
200 kwargs = property(_get_kwargs, _set_kwargs,
201 doc='page handler kwargs (with '
202 'cherrypy.request.params copied in)')
203
204
205 if sys.version_info < (3, 0):
206 punctuation_to_underscores = string.maketrans(
207 string.punctuation, '_' * len(string.punctuation))
209 if not isinstance(t, str) or len(t) != 256:
210 raise ValueError("The translate argument must be a str of len 256.")
211 else:
212 punctuation_to_underscores = str.maketrans(
213 string.punctuation, '_' * len(string.punctuation))
215 if not isinstance(t, dict):
216 raise ValueError("The translate argument must be a dict.")
217
219 """CherryPy Dispatcher which walks a tree of objects to find a handler.
220
221 The tree is rooted at cherrypy.request.app.root, and each hierarchical
222 component in the path_info argument is matched to a corresponding nested
223 attribute of the root object. Matching handlers must have an 'exposed'
224 attribute which evaluates to True. The special method name "index"
225 matches a URI which ends in a slash ("/"). The special method name
226 "default" may match a portion of the path_info (but only when no longer
227 substring of the path_info matches some other object).
228
229 This is the default, built-in dispatcher for CherryPy.
230 """
231
232 dispatch_method_name = '_cp_dispatch'
233 """
234 The name of the dispatch method that nodes may optionally implement
235 to provide their own dynamic dispatch algorithm.
236 """
237
244
256
258 """Return the appropriate page handler, plus any virtual path.
259
260 This will return two objects. The first will be a callable,
261 which can be used to generate page output. Any parameters from
262 the query string or request body will be sent to that callable
263 as keyword arguments.
264
265 The callable is found by traversing the application's tree,
266 starting from cherrypy.request.app.root, and matching path
267 components to successive objects in the tree. For example, the
268 URL "/path/to/handler" might return root.path.to.handler.
269
270 The second object returned will be a list of names which are
271 'virtual path' components: parts of the URL which are dynamic,
272 and were not used when looking up the handler.
273 These virtual path components are passed to the handler as
274 positional arguments.
275 """
276 request = cherrypy.serving.request
277 app = request.app
278 root = app.root
279 dispatch_name = self.dispatch_method_name
280
281
282 fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
283 fullpath_len = len(fullpath)
284 segleft = fullpath_len
285 nodeconf = {}
286 if hasattr(root, "_cp_config"):
287 nodeconf.update(root._cp_config)
288 if "/" in app.config:
289 nodeconf.update(app.config["/"])
290 object_trail = [['root', root, nodeconf, segleft]]
291
292 node = root
293 iternames = fullpath[:]
294 while iternames:
295 name = iternames[0]
296
297 objname = name.translate(self.translate)
298
299 nodeconf = {}
300 subnode = getattr(node, objname, None)
301 pre_len = len(iternames)
302 if subnode is None:
303 dispatch = getattr(node, dispatch_name, None)
304 if dispatch and hasattr(dispatch, '__call__') and not \
305 getattr(dispatch, 'exposed', False) and \
306 pre_len > 1:
307
308
309
310 index_name = iternames.pop()
311 subnode = dispatch(vpath=iternames)
312 iternames.append(index_name)
313 else:
314
315
316 iternames.pop(0)
317 else:
318
319 iternames.pop(0)
320 segleft = len(iternames)
321 if segleft > pre_len:
322
323 raise cherrypy.CherryPyException(
324 "A vpath segment was added. Custom dispatchers may only "
325 + "remove elements. While trying to process "
326 + "{0} in {1}".format(name, fullpath)
327 )
328 elif segleft == pre_len:
329
330
331
332 iternames.pop(0)
333 segleft -= 1
334 node = subnode
335
336 if node is not None:
337
338 if hasattr(node, "_cp_config"):
339 nodeconf.update(node._cp_config)
340
341
342 existing_len = fullpath_len - pre_len
343 if existing_len != 0:
344 curpath = '/' + '/'.join(fullpath[0:existing_len])
345 else:
346 curpath = ''
347 new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
348 for seg in new_segs:
349 curpath += '/' + seg
350 if curpath in app.config:
351 nodeconf.update(app.config[curpath])
352
353 object_trail.append([name, node, nodeconf, segleft])
354
355 def set_conf():
356 """Collapse all object_trail config into cherrypy.request.config."""
357 base = cherrypy.config.copy()
358
359
360 for name, obj, conf, segleft in object_trail:
361 base.update(conf)
362 if 'tools.staticdir.dir' in conf:
363 base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft])
364 return base
365
366
367 num_candidates = len(object_trail) - 1
368 for i in range(num_candidates, -1, -1):
369
370 name, candidate, nodeconf, segleft = object_trail[i]
371 if candidate is None:
372 continue
373
374
375 if hasattr(candidate, "default"):
376 defhandler = candidate.default
377 if getattr(defhandler, 'exposed', False):
378
379 conf = getattr(defhandler, "_cp_config", {})
380 object_trail.insert(i+1, ["default", defhandler, conf, segleft])
381 request.config = set_conf()
382
383 request.is_index = path.endswith("/")
384 return defhandler, fullpath[fullpath_len - segleft:-1]
385
386
387
388
389
390 if getattr(candidate, 'exposed', False):
391 request.config = set_conf()
392 if i == num_candidates:
393
394
395 request.is_index = True
396 else:
397
398
399
400
401 request.is_index = False
402 return candidate, fullpath[fullpath_len - segleft:-1]
403
404
405 request.config = set_conf()
406 return None, []
407
408
410 """Additional dispatch based on cherrypy.request.method.upper().
411
412 Methods named GET, POST, etc will be called on an exposed class.
413 The method names must be all caps; the appropriate Allow header
414 will be output showing all capitalized method names as allowable
415 HTTP verbs.
416
417 Note that the containing class must be exposed, not the methods.
418 """
419
450
451
453 """A Routes based dispatcher for CherryPy."""
454
456 """
457 Routes dispatcher
458
459 Set full_result to True if you wish the controller
460 and the action to be passed on to the page handler
461 parameters. By default they won't be.
462 """
463 import routes
464 self.full_result = full_result
465 self.controllers = {}
466 self.mapper = routes.Mapper()
467 self.mapper.controller_scan = self.controllers.keys
468
469 - def connect(self, name, route, controller, **kwargs):
472
475
483
517
518 app = request.app
519 root = app.root
520 if hasattr(root, "_cp_config"):
521 merge(root._cp_config)
522 if "/" in app.config:
523 merge(app.config["/"])
524
525
526 atoms = [x for x in path_info.split("/") if x]
527 if atoms:
528 last = atoms.pop()
529 else:
530 last = None
531 for atom in atoms:
532 curpath = "/".join((curpath, atom))
533 if curpath in app.config:
534 merge(app.config[curpath])
535
536 handler = None
537 if result:
538 controller = result.get('controller')
539 controller = self.controllers.get(controller, controller)
540 if controller:
541 if isinstance(controller, classtype):
542 controller = controller()
543
544 if hasattr(controller, "_cp_config"):
545 merge(controller._cp_config)
546
547 action = result.get('action')
548 if action is not None:
549 handler = getattr(controller, action, None)
550
551 if hasattr(handler, "_cp_config"):
552 merge(handler._cp_config)
553 else:
554 handler = controller
555
556
557
558 if last:
559 curpath = "/".join((curpath, last))
560 if curpath in app.config:
561 merge(app.config[curpath])
562
563 return handler
564
565
571 return xmlrpc_dispatch
572
573
575 """
576 Select a different handler based on the Host header.
577
578 This can be useful when running multiple sites within one CP server.
579 It allows several domains to point to different parts of a single
580 website structure. For example::
581
582 http://www.domain.example -> root
583 http://www.domain2.example -> root/domain2/
584 http://www.domain2.example:443 -> root/secure
585
586 can be accomplished via the following config::
587
588 [/]
589 request.dispatch = cherrypy.dispatch.VirtualHost(
590 **{'www.domain2.example': '/domain2',
591 'www.domain2.example:443': '/secure',
592 })
593
594 next_dispatcher
595 The next dispatcher object in the dispatch chain.
596 The VirtualHost dispatcher adds a prefix to the URL and calls
597 another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
598
599 use_x_forwarded_host
600 If True (the default), any "X-Forwarded-Host"
601 request header will be used instead of the "Host" header. This
602 is commonly added by HTTP servers (such as Apache) when proxying.
603
604 ``**domains``
605 A dict of {host header value: virtual prefix} pairs.
606 The incoming "Host" request header is looked up in this dict,
607 and, if a match is found, the corresponding "virtual prefix"
608 value will be prepended to the URL path before calling the
609 next dispatcher. Note that you often need separate entries
610 for "example.com" and "www.example.com". In addition, "Host"
611 headers may contain the port number.
612 """
613 from cherrypy.lib import httputil
614 def vhost_dispatch(path_info):
615 request = cherrypy.serving.request
616 header = request.headers.get
617
618 domain = header('Host', '')
619 if use_x_forwarded_host:
620 domain = header("X-Forwarded-Host", domain)
621
622 prefix = domains.get(domain, "")
623 if prefix:
624 path_info = httputil.urljoin(prefix, path_info)
625
626 result = next_dispatcher(path_info)
627
628
629 section = request.config.get('tools.staticdir.section')
630 if section:
631 section = section[len(prefix):]
632 request.config['tools.staticdir.section'] = section
633
634 return result
635 return vhost_dispatch
636