1
# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
4
# Redistribution and use in source and binary forms, with or without
5
# modification, are permitted provided that the following conditions
7
# 1. Redistributions of source code must retain the above copyright
8
# notice, this list of conditions and the following disclaimer.
9
# 2. Redistributions in binary form must reproduce the above copyright
10
# notice, this list of conditions and the following disclaimer in the
11
# documentation and/or other materials provided with the distribution.
13
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25
# $Id: objectpath.py 1833 2005-12-17 00:58:10Z asaddi $
27
__author__ = 'Allan Saddi <allan@saddi.com>'
28
__version__ = '$Revision: 1833 $'
32
from resolver import *
34
__all__ = ['ObjectPathResolver', 'expose']
36
class NoDefault(object):
39
class ObjectPathResolver(Resolver):
41
Inspired by CherryPy <http://www.cherrypy.org/>. :) For an explanation
42
of how this works, see the excellent tutorial at
43
<http://www.cherrypy.org/wiki/CherryPyTutorial>. We support the index and
44
default methods, though the calling convention for the default method
45
is different - we do not pass PATH_INFO as positional arguments. (It
46
is passed through the request/environ as normal.)
48
Also, we explicitly block certain function names. See below. I don't
49
know if theres really any harm in letting those attributes be followed,
50
but I'd rather not take the chance. And unfortunately, this solution
51
is pretty half-baked as well (I'd rather only allow certain object
52
types to be traversed, rather than disallow based on names.) Better
53
than nothing though...
56
default_page = 'default'
58
def __init__(self, root, index=NoDefault, default=NoDefault,
61
root is the root object of your URL hierarchy. In CherryPy, this
64
When the last component of a path has an index method and some
65
object along the path has a default method, favorIndex determines
66
which method is called when the URL has a trailing slash. If
67
True, the index method will be called. Otherwise, the default method.
70
if index is not NoDefault:
71
self.index_page = index
72
if default is not NoDefault:
73
self.default_page = default
74
self._favorIndex = favorIndex
76
# Certain names should be disallowed for safety. If one of your pages
77
# is showing up unexpectedly as a 404, make sure the function name doesn't
78
# begin with one of these prefixes.
79
_disallowed = re.compile(r'''(?:_|im_|func_|tb_|f_|co_).*''')
81
def _exposed(self, obj, redirect):
82
# If redirecting, allow non-exposed objects as well.
83
return callable(obj) and (getattr(obj, 'exposed', False) or redirect)
85
def resolve(self, request, redirect=False):
86
path_info = request.pathInfo.split(';')[0]
87
path_info = path_info.split('/')
89
assert len(path_info) > 0
90
assert not path_info[0]
93
current_default = None
95
for i in range(1, len(path_info)):
96
component = path_info[i]
98
# See if we have an index page (needed for index/default
99
# disambiguation, unfortunately).
102
current_index = getattr(current, self.index_page, None)
103
if not self._exposed(current_index, redirect):
106
if self.default_page:
107
# Remember the last default page we've seen.
108
new_default = getattr(current, self.default_page, None)
109
if self._exposed(new_default, redirect):
110
current_default = (i - 1, new_default)
112
# Test for trailing slash.
113
if not component and current_index is not None and \
114
(self._favorIndex or current_default is None):
115
# Breaking out of the loop here favors index over default.
118
# Respect __all__ attribute. (Ok to generalize to all objects?)
119
all = getattr(current, '__all__', None)
121
current = getattr(current, component, None)
123
if current is None or self._disallowed.match(component) or \
124
(all is not None and component not in all and not redirect):
125
# Use path up to latest default page.
126
if current_default is not None:
127
i, current = current_default
129
# No default at all, so we fail.
133
if self._exposed(current, redirect): # Exposed?
136
# If not, see if it as an exposed index page
138
index = getattr(current, self.index_page, None)
139
if self._exposed(index, redirect): func = index
140
# How about a default page?
141
if func is None and self.default_page:
142
default = getattr(current, self.default_page, None)
143
if self._exposed(default, redirect): func = default
144
# Lastly, see if we have an ancestor's default page to fall back on.
145
if func is None and current_default is not None:
146
i, func = current_default
149
self._updatePath(request, i)
154
"""Decorator to expose functions."""