root/trunk/twisted/web2/resource.py

Revision 22911, 10.0 KB (checked in by wsanchez, 2 years ago)

Merge r22859 and r22910 from branches/dav-take-two-3081-1: Invalid request object from renderHTTP()

renderHTTP() calls http_* with bad request if checkPreconditions() defers.

Author: wsanchez
Reviewer: foom
Fixes #3084

Line 
1# -*- test-case-name: twisted.web2.test.test_server,twisted.web2.test.test_resource -*-
2# Copyright (c) 2001-2007 Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6I hold the lowest-level L{Resource} class and related mix-in classes.
7"""
8
9# System Imports
10from zope.interface import implements
11
12from twisted.web2 import iweb, http, server, responsecode
13
14class RenderMixin(object):
15    """
16    Mix-in class for L{iweb.IResource} which provides a dispatch mechanism for
17    handling HTTP methods.
18    """
19    def allowedMethods(self):
20        """
21        @return: A tuple of HTTP methods that are allowed to be invoked on this resource.
22        """
23        if not hasattr(self, "_allowed_methods"):
24            self._allowed_methods = tuple([name[5:] for name in dir(self) if name.startswith('http_')])
25        return self._allowed_methods
26
27    def checkPreconditions(self, request):
28        """
29        Checks all preconditions imposed by this resource upon a request made
30        against it.
31        @param request: the request to process.
32        @raise http.HTTPError: if any precondition fails.
33        @return: C{None} or a deferred whose callback value is C{request}.
34        """
35        #
36        # http.checkPreconditions() gets called by the server after every
37        # GET or HEAD request.
38        #
39        # For other methods, we need to know to bail out before request
40        # processing, especially for methods that modify server state (eg. PUT).
41        # We also would like to do so even for methods that don't, if those
42        # methods might be expensive to process.  We're assuming that GET and
43        # HEAD are not expensive.
44        #
45        if request.method not in ("GET", "HEAD"):
46            http.checkPreconditions(request)
47
48        # Check per-method preconditions
49        method = getattr(self, "preconditions_" + request.method, None)
50        if method:
51            return method(request)
52
53    def renderHTTP(self, request):
54        """
55        See L{iweb.IResource.renderHTTP}.
56
57        This implementation will dispatch the given C{request} to another method
58        of C{self} named C{http_}METHOD, where METHOD is the HTTP method used by
59        C{request} (eg. C{http_GET}, C{http_POST}, etc.).
60
61        Generally, a subclass should implement those methods instead of
62        overriding this one.
63
64        C{http_*} methods are expected provide the same interface and return the
65        same results as L{iweb.IResource}C{.renderHTTP} (and therefore this method).
66
67        C{etag} and C{last-modified} are added to the response returned by the
68        C{http_*} header, if known.
69
70        If an appropriate C{http_*} method is not found, a
71        L{responsecode.NOT_ALLOWED}-status response is returned, with an
72        appropriate C{allow} header.
73
74        @param request: the request to process.
75        @return: an object adaptable to L{iweb.IResponse}.
76        """
77        method = getattr(self, "http_" + request.method, None)
78        if not method:
79            response = http.Response(responsecode.NOT_ALLOWED)
80            response.headers.setHeader("allow", self.allowedMethods())
81            return response
82
83        d = self.checkPreconditions(request)
84        if d is None:
85            return method(request)
86        else:
87            return d.addCallback(lambda _: method(request))
88
89    def http_OPTIONS(self, request):
90        """
91        Respond to a OPTIONS request.
92        @param request: the request to process.
93        @return: an object adaptable to L{iweb.IResponse}.
94        """
95        response = http.Response(responsecode.OK)
96        response.headers.setHeader("allow", self.allowedMethods())
97        return response
98
99    def http_TRACE(self, request):
100        """
101        Respond to a TRACE request.
102        @param request: the request to process.
103        @return: an object adaptable to L{iweb.IResponse}.
104        """
105        return server.doTrace(request)
106
107    def http_HEAD(self, request):
108        """
109        Respond to a HEAD request.
110        @param request: the request to process.
111        @return: an object adaptable to L{iweb.IResponse}.
112        """
113        return self.http_GET(request)
114
115    def http_GET(self, request):
116        """
117        Respond to a GET request.
118
119        This implementation validates that the request body is empty and then
120        dispatches the given C{request} to L{render} and returns its result.
121
122        @param request: the request to process.
123        @return: an object adaptable to L{iweb.IResponse}.
124        """
125        if request.stream.length != 0:
126            return responsecode.REQUEST_ENTITY_TOO_LARGE
127
128        return self.render(request)
129
130    def render(self, request):
131        """
132        Subclasses should implement this method to do page rendering.
133        See L{http_GET}.
134        @param request: the request to process.
135        @return: an object adaptable to L{iweb.IResponse}.
136        """
137        raise NotImplementedError("Subclass must implement render method.")
138
139class Resource(RenderMixin):
140    """
141    An L{iweb.IResource} implementation with some convenient mechanisms for
142    locating children.
143    """
144    implements(iweb.IResource)
145
146    addSlash = False
147
148    def locateChild(self, request, segments):
149        """
150        Locates a child resource of this resource.
151        @param request: the request to process.
152        @param segments: a sequence of URL path segments.
153        @return: a tuple of C{(child, segments)} containing the child
154        of this resource which matches one or more of the given C{segments} in
155        sequence, and a list of remaining segments.
156        """
157        w = getattr(self, 'child_%s' % (segments[0], ), None)
158
159        if w:
160            r = iweb.IResource(w, None)
161            if r:
162                return r, segments[1:]
163            return w(request), segments[1:]
164
165        factory = getattr(self, 'childFactory', None)
166        if factory is not None:
167            r = factory(request, segments[0])
168            if r:
169                return r, segments[1:]
170
171        return None, []
172
173    def child_(self, request):
174        """
175        This method locates a child with a trailing C{"/"} in the URL.
176        @param request: the request to process.
177        """
178        if self.addSlash and len(request.postpath) == 1:
179            return self
180        return None
181
182    def putChild(self, path, child):
183        """
184        Register a static child.
185
186        This implementation registers children by assigning them to attributes
187        with a C{child_} prefix.  C{resource.putChild("foo", child)} is
188        therefore same as C{o.child_foo = child}.
189
190        @param path: the name of the child to register.  You almost certainly
191            don't want C{"/"} in C{path}. If you want to add a "directory"
192            resource (e.g. C{/foo/}) specify C{path} as C{""}.
193        @param child: an object adaptable to L{iweb.IResource}.
194        """
195        setattr(self, 'child_%s' % (path, ), child)
196
197    def http_GET(self, request):
198        if self.addSlash and request.prepath[-1] != '':
199            # If this is a directory-ish resource...
200            return http.RedirectResponse(request.unparseURL(path=request.path+'/'))
201
202        return super(Resource, self).http_GET(request)
203
204
205class PostableResource(Resource):
206    """
207    A L{Resource} capable of handling the POST request method.
208
209    @cvar maxMem: maximum memory used during the parsing of the data.
210    @type maxMem: C{int}
211    @cvar maxFields: maximum number of form fields allowed.
212    @type maxFields: C{int}
213    @cvar maxSize: maximum size of the whole post allowed.
214    @type maxSize: C{int}
215    """
216    maxMem = 100 * 1024
217    maxFields = 1024
218    maxSize = 10 * 1024 * 1024
219
220    def http_POST(self, request):
221        """
222        Respond to a POST request.
223        Reads and parses the incoming body data then calls L{render}.
224
225        @param request: the request to process.
226        @return: an object adaptable to L{iweb.IResponse}.
227        """
228        return server.parsePOSTData(request,
229            self.maxMem, self.maxFields, self.maxSize
230            ).addCallback(lambda res: self.render(request))
231
232
233class LeafResource(RenderMixin):
234    """
235    A L{Resource} with no children.
236    """
237    implements(iweb.IResource)
238
239    def locateChild(self, request, segments):
240        return self, server.StopTraversal
241
242class RedirectResource(LeafResource):
243    """
244    A L{LeafResource} which always performs a redirect.
245    """
246    implements(iweb.IResource)
247
248    def __init__(self, *args, **kwargs):
249        """
250        Parameters are URL components and are the same as those for
251        L{urlparse.urlunparse}.  URL components which are not specified will
252        default to the corresponding component of the URL of the request being
253        redirected.
254        """
255        self._args   = args
256        self._kwargs = kwargs
257
258    def renderHTTP(self, request):
259        return http.RedirectResponse(request.unparseURL(*self._args, **self._kwargs))
260
261class WrapperResource(object):
262    """
263    An L{iweb.IResource} implementation which wraps a L{RenderMixin} instance
264    and provides a hook in which a subclass can implement logic that is called
265    before request processing on the contained L{Resource}.
266    """
267    implements(iweb.IResource)
268
269    def __init__(self, resource):
270        self.resource=resource
271
272    def hook(self, request):
273        """
274        Override this method in order to do something before passing control on
275        to the wrapped resource's C{renderHTTP} and C{locateChild} methods.
276        @return: None or a L{Deferred}.  If a deferred object is
277            returned, it's value is ignored, but C{renderHTTP} and
278            C{locateChild} are chained onto the deferred as callbacks.
279        """
280        raise NotImplementedError()
281
282    def locateChild(self, request, segments):
283        x = self.hook(request)
284        if x is not None:
285            return x.addCallback(lambda data: (self.resource, segments))
286        return self.resource, segments
287
288    def renderHTTP(self, request):
289        x = self.hook(request)
290        if x is not None:
291            return x.addCallback(lambda data: self.resource)
292        return self.resource
293
294
295__all__ = ['RenderMixin', 'Resource', 'PostableResource', 'LeafResource', 'WrapperResource']
Note: See TracBrowser for help on using the browser.