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
26 """Callable which sets response.body."""
27
28 - def __init__(self, callable, *args, **kwargs):
32
35
36 - def set_args(self, args):
39
40 args = property(
41 get_args,
42 set_args,
43 doc="The ordered args should be accessible from post dispatch hooks"
44 )
45
46 - def get_kwargs(self):
48
49 - def set_kwargs(self, kwargs):
52
53 kwargs = property(
54 get_kwargs,
55 set_kwargs,
56 doc="The named kwargs should be accessible from post dispatch hooks"
57 )
58
60 try:
61 return self.callable(*self.args, **self.kwargs)
62 except TypeError:
63 x = sys.exc_info()[1]
64 try:
65 test_callable_spec(self.callable, self.args, self.kwargs)
66 except cherrypy.HTTPError:
67 raise sys.exc_info()[1]
68 except:
69 raise x
70 raise
71
72
74 """
75 Inspect callable and test to see if the given args are suitable for it.
76
77 When an error occurs during the handler's invoking stage there are 2
78 erroneous cases:
79 1. Too many parameters passed to a function which doesn't define
80 one of *args or **kwargs.
81 2. Too little parameters are passed to the function.
82
83 There are 3 sources of parameters to a cherrypy handler.
84 1. query string parameters are passed as keyword parameters to the
85 handler.
86 2. body parameters are also passed as keyword parameters.
87 3. when partial matching occurs, the final path atoms are passed as
88 positional args.
89 Both the query string and path atoms are part of the URI. If they are
90 incorrect, then a 404 Not Found should be raised. Conversely the body
91 parameters are part of the request; if they are invalid a 400 Bad Request.
92 """
93 show_mismatched_params = getattr(
94 cherrypy.serving.request, 'show_mismatched_params', False)
95 try:
96 (args, varargs, varkw, defaults) = inspect.getargspec(callable)
97 except TypeError:
98 if isinstance(callable, object) and hasattr(callable, '__call__'):
99 (args, varargs, varkw,
100 defaults) = inspect.getargspec(callable.__call__)
101 else:
102
103
104 raise
105
106 if args and args[0] == 'self':
107 args = args[1:]
108
109 arg_usage = dict([(arg, 0,) for arg in args])
110 vararg_usage = 0
111 varkw_usage = 0
112 extra_kwargs = set()
113
114 for i, value in enumerate(callable_args):
115 try:
116 arg_usage[args[i]] += 1
117 except IndexError:
118 vararg_usage += 1
119
120 for key in callable_kwargs.keys():
121 try:
122 arg_usage[key] += 1
123 except KeyError:
124 varkw_usage += 1
125 extra_kwargs.add(key)
126
127
128 args_with_defaults = args[-len(defaults or []):]
129 for i, val in enumerate(defaults or []):
130
131 if arg_usage[args_with_defaults[i]] == 0:
132 arg_usage[args_with_defaults[i]] += 1
133
134 missing_args = []
135 multiple_args = []
136 for key, usage in arg_usage.items():
137 if usage == 0:
138 missing_args.append(key)
139 elif usage > 1:
140 multiple_args.append(key)
141
142 if missing_args:
143
144
145
146
147
148
149
150
151
152
153
154
155 message = None
156 if show_mismatched_params:
157 message = "Missing parameters: %s" % ",".join(missing_args)
158 raise cherrypy.HTTPError(404, message=message)
159
160
161 if not varargs and vararg_usage > 0:
162 raise cherrypy.HTTPError(404)
163
164 body_params = cherrypy.serving.request.body.params or {}
165 body_params = set(body_params.keys())
166 qs_params = set(callable_kwargs.keys()) - body_params
167
168 if multiple_args:
169 if qs_params.intersection(set(multiple_args)):
170
171
172 error = 404
173 else:
174
175 error = 400
176
177 message = None
178 if show_mismatched_params:
179 message = "Multiple values for parameters: "\
180 "%s" % ",".join(multiple_args)
181 raise cherrypy.HTTPError(error, message=message)
182
183 if not varkw and varkw_usage > 0:
184
185
186 extra_qs_params = set(qs_params).intersection(extra_kwargs)
187 if extra_qs_params:
188 message = None
189 if show_mismatched_params:
190 message = "Unexpected query string "\
191 "parameters: %s" % ", ".join(extra_qs_params)
192 raise cherrypy.HTTPError(404, message=message)
193
194
195 extra_body_params = set(body_params).intersection(extra_kwargs)
196 if extra_body_params:
197 message = None
198 if show_mismatched_params:
199 message = "Unexpected body parameters: "\
200 "%s" % ", ".join(extra_body_params)
201 raise cherrypy.HTTPError(400, message=message)
202
203
204 try:
205 import inspect
206 except ImportError:
207 test_callable_spec = lambda callable, args, kwargs: None
208
209
210 -class LateParamPageHandler(PageHandler):
211
212 """When passing cherrypy.request.params to the page handler, we do not
213 want to capture that dict too early; we want to give tools like the
214 decoding tool a chance to modify the params dict in-between the lookup
215 of the handler and the actual calling of the handler. This subclass
216 takes that into account, and allows request.params to be 'bound late'
217 (it's more complicated than that, but that's the effect).
218 """
219
220 - def _get_kwargs(self):
221 kwargs = cherrypy.serving.request.params.copy()
222 if self._kwargs:
223 kwargs.update(self._kwargs)
224 return kwargs
225
226 - def _set_kwargs(self, kwargs):
229
230 kwargs = property(_get_kwargs, _set_kwargs,
231 doc='page handler kwargs (with '
232 'cherrypy.request.params copied in)')
233
234
235 if sys.version_info < (3, 0):
236 punctuation_to_underscores = string.maketrans(
237 string.punctuation, '_' * len(string.punctuation))
238
240 if not isinstance(t, str) or len(t) != 256:
241 raise ValueError(
242 "The translate argument must be a str of len 256.")
243 else:
244 punctuation_to_underscores = str.maketrans(
245 string.punctuation, '_' * len(string.punctuation))
246
248 if not isinstance(t, dict):
249 raise ValueError("The translate argument must be a dict.")
250
251
253
254 """CherryPy Dispatcher which walks a tree of objects to find a handler.
255
256 The tree is rooted at cherrypy.request.app.root, and each hierarchical
257 component in the path_info argument is matched to a corresponding nested
258 attribute of the root object. Matching handlers must have an 'exposed'
259 attribute which evaluates to True. The special method name "index"
260 matches a URI which ends in a slash ("/"). The special method name
261 "default" may match a portion of the path_info (but only when no longer
262 substring of the path_info matches some other object).
263
264 This is the default, built-in dispatcher for CherryPy.
265 """
266
267 dispatch_method_name = '_cp_dispatch'
268 """
269 The name of the dispatch method that nodes may optionally implement
270 to provide their own dynamic dispatch algorithm.
271 """
272
279
291
293 """Return the appropriate page handler, plus any virtual path.
294
295 This will return two objects. The first will be a callable,
296 which can be used to generate page output. Any parameters from
297 the query string or request body will be sent to that callable
298 as keyword arguments.
299
300 The callable is found by traversing the application's tree,
301 starting from cherrypy.request.app.root, and matching path
302 components to successive objects in the tree. For example, the
303 URL "/path/to/handler" might return root.path.to.handler.
304
305 The second object returned will be a list of names which are
306 'virtual path' components: parts of the URL which are dynamic,
307 and were not used when looking up the handler.
308 These virtual path components are passed to the handler as
309 positional arguments.
310 """
311 request = cherrypy.serving.request
312 app = request.app
313 root = app.root
314 dispatch_name = self.dispatch_method_name
315
316
317 fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
318 fullpath_len = len(fullpath)
319 segleft = fullpath_len
320 nodeconf = {}
321 if hasattr(root, "_cp_config"):
322 nodeconf.update(root._cp_config)
323 if "/" in app.config:
324 nodeconf.update(app.config["/"])
325 object_trail = [['root', root, nodeconf, segleft]]
326
327 node = root
328 iternames = fullpath[:]
329 while iternames:
330 name = iternames[0]
331
332 objname = name.translate(self.translate)
333
334 nodeconf = {}
335 subnode = getattr(node, objname, None)
336 pre_len = len(iternames)
337 if subnode is None:
338 dispatch = getattr(node, dispatch_name, None)
339 if dispatch and hasattr(dispatch, '__call__') and not \
340 getattr(dispatch, 'exposed', False) and \
341 pre_len > 1:
342
343
344
345 index_name = iternames.pop()
346 subnode = dispatch(vpath=iternames)
347 iternames.append(index_name)
348 else:
349
350
351 iternames.pop(0)
352 else:
353
354 iternames.pop(0)
355 segleft = len(iternames)
356 if segleft > pre_len:
357
358 raise cherrypy.CherryPyException(
359 "A vpath segment was added. Custom dispatchers may only "
360 + "remove elements. While trying to process "
361 + "{0} in {1}".format(name, fullpath)
362 )
363 elif segleft == pre_len:
364
365
366
367 iternames.pop(0)
368 segleft -= 1
369 node = subnode
370
371 if node is not None:
372
373 if hasattr(node, "_cp_config"):
374 nodeconf.update(node._cp_config)
375
376
377 existing_len = fullpath_len - pre_len
378 if existing_len != 0:
379 curpath = '/' + '/'.join(fullpath[0:existing_len])
380 else:
381 curpath = ''
382 new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
383 for seg in new_segs:
384 curpath += '/' + seg
385 if curpath in app.config:
386 nodeconf.update(app.config[curpath])
387
388 object_trail.append([name, node, nodeconf, segleft])
389
390 def set_conf():
391 """Collapse all object_trail config into cherrypy.request.config.
392 """
393 base = cherrypy.config.copy()
394
395
396 for name, obj, conf, segleft in object_trail:
397 base.update(conf)
398 if 'tools.staticdir.dir' in conf:
399 base['tools.staticdir.section'] = '/' + \
400 '/'.join(fullpath[0:fullpath_len - segleft])
401 return base
402
403
404 num_candidates = len(object_trail) - 1
405 for i in range(num_candidates, -1, -1):
406
407 name, candidate, nodeconf, segleft = object_trail[i]
408 if candidate is None:
409 continue
410
411
412 if hasattr(candidate, "default"):
413 defhandler = candidate.default
414 if getattr(defhandler, 'exposed', False):
415
416 conf = getattr(defhandler, "_cp_config", {})
417 object_trail.insert(
418 i + 1, ["default", defhandler, conf, segleft])
419 request.config = set_conf()
420
421 request.is_index = path.endswith("/")
422 return defhandler, fullpath[fullpath_len - segleft:-1]
423
424
425
426
427
428
429 if getattr(candidate, 'exposed', False):
430 request.config = set_conf()
431 if i == num_candidates:
432
433
434 request.is_index = True
435 else:
436
437
438
439
440 request.is_index = False
441 return candidate, fullpath[fullpath_len - segleft:-1]
442
443
444 request.config = set_conf()
445 return None, []
446
447
449
450 """Additional dispatch based on cherrypy.request.method.upper().
451
452 Methods named GET, POST, etc will be called on an exposed class.
453 The method names must be all caps; the appropriate Allow header
454 will be output showing all capitalized method names as allowable
455 HTTP verbs.
456
457 Note that the containing class must be exposed, not the methods.
458 """
459
490
491
493
494 """A Routes based dispatcher for CherryPy."""
495
496 - def __init__(self, full_result=False, **mapper_options):
497 """
498 Routes dispatcher
499
500 Set full_result to True if you wish the controller
501 and the action to be passed on to the page handler
502 parameters. By default they won't be.
503 """
504 import routes
505 self.full_result = full_result
506 self.controllers = {}
507 self.mapper = routes.Mapper(**mapper_options)
508 self.mapper.controller_scan = self.controllers.keys
509
510 - def connect(self, name, route, controller, **kwargs):
513
516
524
558
559 app = request.app
560 root = app.root
561 if hasattr(root, "_cp_config"):
562 merge(root._cp_config)
563 if "/" in app.config:
564 merge(app.config["/"])
565
566
567 atoms = [x for x in path_info.split("/") if x]
568 if atoms:
569 last = atoms.pop()
570 else:
571 last = None
572 for atom in atoms:
573 curpath = "/".join((curpath, atom))
574 if curpath in app.config:
575 merge(app.config[curpath])
576
577 handler = None
578 if result:
579 controller = result.get('controller')
580 controller = self.controllers.get(controller, controller)
581 if controller:
582 if isinstance(controller, classtype):
583 controller = controller()
584
585 if hasattr(controller, "_cp_config"):
586 merge(controller._cp_config)
587
588 action = result.get('action')
589 if action is not None:
590 handler = getattr(controller, action, None)
591
592 if hasattr(handler, "_cp_config"):
593 merge(handler._cp_config)
594 else:
595 handler = controller
596
597
598
599 if last:
600 curpath = "/".join((curpath, last))
601 if curpath in app.config:
602 merge(app.config[curpath])
603
604 return handler
605
606
613 return xmlrpc_dispatch
614
615
618 """
619 Select a different handler based on the Host header.
620
621 This can be useful when running multiple sites within one CP server.
622 It allows several domains to point to different parts of a single
623 website structure. For example::
624
625 http://www.domain.example -> root
626 http://www.domain2.example -> root/domain2/
627 http://www.domain2.example:443 -> root/secure
628
629 can be accomplished via the following config::
630
631 [/]
632 request.dispatch = cherrypy.dispatch.VirtualHost(
633 **{'www.domain2.example': '/domain2',
634 'www.domain2.example:443': '/secure',
635 })
636
637 next_dispatcher
638 The next dispatcher object in the dispatch chain.
639 The VirtualHost dispatcher adds a prefix to the URL and calls
640 another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
641
642 use_x_forwarded_host
643 If True (the default), any "X-Forwarded-Host"
644 request header will be used instead of the "Host" header. This
645 is commonly added by HTTP servers (such as Apache) when proxying.
646
647 ``**domains``
648 A dict of {host header value: virtual prefix} pairs.
649 The incoming "Host" request header is looked up in this dict,
650 and, if a match is found, the corresponding "virtual prefix"
651 value will be prepended to the URL path before calling the
652 next dispatcher. Note that you often need separate entries
653 for "example.com" and "www.example.com". In addition, "Host"
654 headers may contain the port number.
655 """
656 from cherrypy.lib import httputil
657
658 def vhost_dispatch(path_info):
659 request = cherrypy.serving.request
660 header = request.headers.get
661
662 domain = header('Host', '')
663 if use_x_forwarded_host:
664 domain = header("X-Forwarded-Host", domain)
665
666 prefix = domains.get(domain, "")
667 if prefix:
668 path_info = httputil.urljoin(prefix, path_info)
669
670 result = next_dispatcher(path_info)
671
672
673
674 section = request.config.get('tools.staticdir.section')
675 if section:
676 section = section[len(prefix):]
677 request.config['tools.staticdir.section'] = section
678
679 return result
680 return vhost_dispatch
681