| 1 | |
|---|
| 2 | |
|---|
| 3 | |
|---|
| 4 | |
|---|
| 5 | """ |
|---|
| 6 | I hold the lowest-level L{Resource} class and related mix-in classes. |
|---|
| 7 | """ |
|---|
| 8 | |
|---|
| 9 | |
|---|
| 10 | from zope.interface import implements |
|---|
| 11 | |
|---|
| 12 | from twisted.web2 import iweb, http, server, responsecode |
|---|
| 13 | |
|---|
| 14 | class 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 | |
|---|
| 37 | |
|---|
| 38 | |
|---|
| 39 | |
|---|
| 40 | |
|---|
| 41 | |
|---|
| 42 | |
|---|
| 43 | |
|---|
| 44 | |
|---|
| 45 | if request.method not in ("GET", "HEAD"): |
|---|
| 46 | http.checkPreconditions(request) |
|---|
| 47 | |
|---|
| 48 | |
|---|
| 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 | |
|---|
| 139 | class 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 | |
|---|
| 200 | return http.RedirectResponse(request.unparseURL(path=request.path+'/')) |
|---|
| 201 | |
|---|
| 202 | return super(Resource, self).http_GET(request) |
|---|
| 203 | |
|---|
| 204 | |
|---|
| 205 | class 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 | |
|---|
| 233 | class 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 | |
|---|
| 242 | class 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 | |
|---|
| 261 | class 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'] |
|---|