1# -*- test-case-name: nevow.test.test_appserver -*-
2# Copyright (c) 2004-2008 Divmod.
3# See LICENSE for details.
4
5"""
6A web application server built using twisted.web
7"""
8
9import cgi
10import warnings
11from collections import MutableMapping
12from urllib import unquote
13
14from zope.interface import implements, classImplements
15
16import twisted.python.components as tpc
17from twisted.web import server
18
19try:
20    from twisted.web import http
21except ImportError:
22    from twisted.protocols import http
23
24from twisted.python import log
25from twisted.internet import defer
26
27from nevow import context
28from nevow import inevow
29from nevow import url
30from nevow import flat
31from nevow import stan
32
33
34
35class _DictHeaders(MutableMapping):
36    """
37    A C{dict}-like wrapper around L{Headers} to provide backwards compatibility
38    for L{twisted.web.http.Request.received_headers} and
39    L{twisted.web.http.Request.headers} which used to be plain C{dict}
40    instances.
41
42    @type _headers: L{Headers}
43    @ivar _headers: The real header storage object.
44    """
45    def __init__(self, headers):
46        self._headers = headers
47
48
49    def __getitem__(self, key):
50        """
51        Return the last value for header of C{key}.
52        """
53        if self._headers.hasHeader(key):
54            return self._headers.getRawHeaders(key)[-1]
55        raise KeyError(key)
56
57
58    def __setitem__(self, key, value):
59        """
60        Set the given header.
61        """
62        self._headers.setRawHeaders(key, [value])
63
64
65    def __delitem__(self, key):
66        """
67        Delete the given header.
68        """
69        if self._headers.hasHeader(key):
70            self._headers.removeHeader(key)
71        else:
72            raise KeyError(key)
73
74
75    def __iter__(self):
76        """
77        Return an iterator of the lowercase name of each header present.
78        """
79        for k, v in self._headers.getAllRawHeaders():
80            yield k.lower()
81
82
83    def __len__(self):
84        """
85        Return the number of distinct headers present.
86        """
87        # XXX Too many _
88        return len(self._headers._rawHeaders)
89
90
91    # Extra methods that MutableMapping doesn't care about but that we do.
92    def copy(self):
93        """
94        Return a C{dict} mapping each header name to the last corresponding
95        header value.
96        """
97        return dict(self.items())
98
99
100    def has_key(self, key):
101        """
102        Return C{True} if C{key} is a header in this collection, C{False}
103        otherwise.
104        """
105        return key in self
106
107
108
109class UninformativeExceptionHandler:
110    implements(inevow.ICanHandleException)
111
112    def renderHTTP_exception(self, ctx, reason):
113        request = inevow.IRequest(ctx)
114        log.err(reason)
115        request.write("<html><head><title>Internal Server Error</title></head>")
116        request.write("<body><h1>Internal Server Error</h1>An error occurred rendering the requested page. To see a more detailed error message, enable tracebacks in the configuration.</body></html>")
117
118        request.finishRequest( False )
119
120    def renderInlineException(self, context, reason):
121        log.err(reason)
122        return """<div style="border: 1px dashed red; color: red; clear: both">[[ERROR]]</div>"""
123
124
125class DefaultExceptionHandler:
126    implements(inevow.ICanHandleException)
127
128    def renderHTTP_exception(self, ctx, reason):
129        log.err(reason)
130        request = inevow.IRequest(ctx)
131        request.setResponseCode(http.INTERNAL_SERVER_ERROR)
132        request.write("<html><head><title>Exception</title></head><body>")
133        from nevow import failure
134        result = failure.formatFailure(reason)
135        request.write(''.join(flat.flatten(result)))
136        request.write("</body></html>")
137
138        request.finishRequest( False )
139
140    def renderInlineException(self, context, reason):
141        from nevow import failure
142        formatted = failure.formatFailure(reason)
143        desc = str(reason)
144        return flat.serialize([
145            stan.xml("""<div style="border: 1px dashed red; color: red; clear: both" onclick="this.childNodes[1].style.display = this.childNodes[1].style.display == 'none' ? 'block': 'none'">"""),
146            desc,
147            stan.xml('<div style="display: none">'),
148            formatted,
149            stan.xml('</div></div>')
150        ], context)
151
152
153errorMarker = object()
154
155
156def processingFailed(reason, request, ctx):
157    try:
158        handler = inevow.ICanHandleException(ctx)
159        handler.renderHTTP_exception(ctx, reason)
160    except:
161        request.setResponseCode(http.INTERNAL_SERVER_ERROR)
162        log.msg("Exception rendering error page:", isErr=1)
163        log.err()
164        log.err("Original exception:", isErr=1)
165        log.err(reason)
166        request.write("<html><head><title>Internal Server Error</title></head>")
167        request.write("<body><h1>Internal Server Error</h1>An error occurred rendering the requested page. Additionally, an error occurred rendering the error page.</body></html>")
168        request.finishRequest( False )
169
170    return errorMarker
171
172
173def defaultExceptionHandlerFactory(ctx):
174    return DefaultExceptionHandler()
175
176
177class NevowRequest(tpc.Componentized, server.Request):
178    """
179    A Request subclass which does additional
180    processing if a form was POSTed. When a form is POSTed,
181    we create a cgi.FieldStorage instance using the data posted,
182    and set it as the request.fields attribute. This way, we can
183    get at information about filenames and mime-types of
184    files that were posted.
185
186    TODO: cgi.FieldStorage blocks while decoding the MIME.
187    Rewrite it to do the work in chunks, yielding from time to
188    time.
189
190    @ivar fields: C{None} or, if the HTTP method is B{POST}, a
191        L{cgi.FieldStorage} instance giving the content of the POST.
192
193    @ivar _lostConnection: A flag which keeps track of whether the response to
194        this request has been interrupted (for example, by the connection being
195        lost) or not.  C{False} until this happens, C{True} afterwards.
196    @type _lostConnection: L{bool}
197    """
198    implements(inevow.IRequest)
199
200    fields = None
201    _lostConnection = False
202
203    def __init__(self, *args, **kw):
204        server.Request.__init__(self, *args, **kw)
205        tpc.Componentized.__init__(self)
206
207        self.notifyFinish().addErrback(self._flagLostConnection)
208
209
210    def _flagLostConnection(self, error):
211        """
212        Observe and record an error trying to deliver the response for this
213        request.
214        """
215        self._lostConnection = True
216
217
218    def process(self):
219        # extra request parsing
220        if self.method == 'POST':
221            t = self.content.tell()
222            self.content.seek(0)
223            self.fields = cgi.FieldStorage(
224                self.content, _DictHeaders(self.requestHeaders),
225                environ={'REQUEST_METHOD': 'POST'})
226            self.content.seek(t)
227
228        # get site from channel
229        self.site = self.channel.site
230
231        # set various default headers
232        self.setHeader('server', server.version)
233        self.setHeader('date', server.http.datetimeToString())
234        self.setHeader('content-type', "text/html; charset=UTF-8")
235
236        # Resource Identification
237        self.prepath = []
238        self.postpath = map(unquote, self.path[1:].split('/'))
239        self.sitepath = []
240
241        self.deferred = defer.Deferred()
242
243        requestContext = context.RequestContext(parent=self.site.context, tag=self)
244        requestContext.remember( (), inevow.ICurrentSegments)
245        requestContext.remember(tuple(self.postpath), inevow.IRemainingSegments)
246
247        return self.site.getPageContextForRequestContext(
248            requestContext
249        ).addErrback(
250            processingFailed, self, requestContext
251        ).addCallback(
252            self.gotPageContext
253        )
254
255    def gotPageContext(self, pageContext):
256        if pageContext is not errorMarker:
257            return defer.maybeDeferred(
258                pageContext.tag.renderHTTP, pageContext
259            ).addBoth(
260                self._cbSetLogger, pageContext
261            ).addErrback(
262                processingFailed, self, pageContext
263            ).addCallback(
264                self._cbFinishRender, pageContext
265            )
266
267    def finish(self):
268        self.deferred.callback("")
269
270
271    def finishRequest(self, success):
272        """
273        Indicate the response to this request has been completely generated
274        (headers have been set, the response body has been completely written).
275
276        @param success: Indicate whether this response is considered successful
277            or not.  Not used.
278        """
279        if not self._lostConnection:
280            # Only bother doing the work associated with finishing if the
281            # connection is still there.
282            server.Request.finish(self)
283
284
285    def _cbFinishRender(self, html, ctx):
286        """
287        Callback for the page rendering process having completed.
288
289        @param html: Either the content of the response body (L{bytes}) or a
290            marker that an exception occurred and has already been handled or
291            an object adaptable to L{IResource} to use to render the response.
292        """
293        if self._lostConnection:
294            # No response can be sent at this point.
295            pass
296        elif isinstance(html, str):
297            self.write(html)
298            self.finishRequest(  True )
299        elif html is errorMarker:
300            ## Error webpage has already been rendered and finish called
301            pass
302        else:
303            res = inevow.IResource(html)
304            pageContext = context.PageContext(tag=res, parent=ctx)
305            return self.gotPageContext(pageContext)
306        return html
307
308    _logger = None
309    def _cbSetLogger(self, result, ctx):
310        try:
311            logger = ctx.locate(inevow.ILogger)
312        except KeyError:
313            pass
314        else:
315            self._logger = lambda : logger.log(ctx)
316
317        return result
318
319    session = None
320
321    def getSession(self, sessionInterface=None):
322        if self.session is not None:
323            self.session.touch()
324            if sessionInterface:
325                return sessionInterface(self.session)
326            return self.session
327        ## temporary until things settle down with the new sessions
328        return server.Request.getSession(self, sessionInterface)
329
330    def URLPath(self):
331        return url.URL.fromContext(self)
332
333    def rememberRootURL(self, url=None):
334        """
335        Remember the currently-processed part of the URL for later
336        recalling.
337        """
338        if url is None:
339            return server.Request.rememberRootURL(self)
340        else:
341            self.appRootURL = url
342
343
344    def _warnHeaders(self, old, new):
345        """
346        Emit a warning related to use of one of the deprecated C{headers} or
347        C{received_headers} attributes.
348
349        @param old: The name of the deprecated attribute to which the warning
350            pertains.
351
352        @param new: The name of the preferred attribute which replaces the old
353            attribute.
354        """
355        warnings.warn(
356            category=DeprecationWarning,
357            message=(
358                "nevow.appserver.NevowRequest.%(old)s was deprecated in "
359                "Nevow 0.13.0: Please use nevow.appserver.NevowRequest."
360                "%(new)s instead." % dict(old=old, new=new)),
361            stacklevel=3)
362
363
364    @property
365    def headers(self):
366        """
367        Transform the L{Headers}-style C{responseHeaders} attribute into a
368        deprecated C{dict}-style C{headers} attribute.
369        """
370        self._warnHeaders("headers", "responseHeaders")
371        return _DictHeaders(self.responseHeaders)
372
373
374    @property
375    def received_headers(self):
376        """
377        Transform the L{Headers}-style C{requestHeaders} attribute into a
378        deprecated C{dict}-style C{received_headers} attribute.
379        """
380        self._warnHeaders("received_headers", "requestHeaders")
381        return _DictHeaders(self.requestHeaders)
382
383
384def sessionFactory(ctx):
385    """Given a RequestContext instance with a Request as .tag, return a session
386    """
387    return ctx.tag.getSession()
388
389requestFactory = lambda ctx: ctx.tag
390
391
392class NevowSite(server.Site):
393    requestFactory = NevowRequest
394
395    def __init__(self, resource, *args, **kwargs):
396        resource.addSlash = True
397        server.Site.__init__(self, resource, *args, **kwargs)
398        self.context = context.SiteContext()
399
400    def remember(self, obj, inter=None):
401        """Remember the given object for the given interfaces (or all interfaces
402        obj implements) in the site's context.
403
404        The site context is the parent of all other contexts. Anything
405        remembered here will be available throughout the site.
406        """
407        self.context.remember(obj, inter)
408
409    def getPageContextForRequestContext(self, ctx):
410        """Retrieve a resource from this site for a particular request. The
411        resource will be wrapped in a PageContext which keeps track
412        of how the resource was located.
413        """
414        path = inevow.IRemainingSegments(ctx)
415        res = inevow.IResource(self.resource)
416        pageContext = context.PageContext(tag=res, parent=ctx)
417        return defer.maybeDeferred(res.locateChild, pageContext, path).addCallback(
418            self.handleSegment, ctx.tag, path, pageContext
419        )
420
421    def handleSegment(self, result, request, path, pageContext):
422        if result is errorMarker:
423            return errorMarker
424
425        newres, newpath = result
426        # If the child resource is None then display a 404 page
427        if newres is None:
428            from nevow.rend import FourOhFour
429            return context.PageContext(tag=FourOhFour(), parent=pageContext)
430
431        # If we got a deferred then we need to call back later, once the
432        # child is actually available.
433        if isinstance(newres, defer.Deferred):
434            return newres.addCallback(
435                lambda actualRes: self.handleSegment(
436                    (actualRes, newpath), request, path, pageContext))
437
438
439        #
440        # FIX A GIANT LEAK. Is this code really useful anyway?
441        #
442        newres = inevow.IResource(newres)#, persist=True)
443        if newres is pageContext.tag:
444            assert not newpath is path, "URL traversal cycle detected when attempting to locateChild %r from resource %r." % (path, pageContext.tag)
445            assert  len(newpath) < len(path), "Infinite loop impending..."
446
447        ## We found a Resource... update the request.prepath and postpath
448        for x in xrange(len(path) - len(newpath)):
449            if request.postpath:
450                request.prepath.append(request.postpath.pop(0))
451
452        ## Create a context object to represent this new resource
453        ctx = context.PageContext(tag=newres, parent=pageContext)
454        ctx.remember(tuple(request.prepath), inevow.ICurrentSegments)
455        ctx.remember(tuple(request.postpath), inevow.IRemainingSegments)
456
457        res = newres
458        path = newpath
459
460        if not path:
461            return ctx
462
463        return defer.maybeDeferred(
464            res.locateChild, ctx, path
465        ).addErrback(
466            processingFailed, request, ctx
467        ).addCallback(
468            self.handleSegment, request, path, ctx
469        )
470
471    def log(self, request):
472        if request._logger is None:
473            server.Site.log(self, request)
474        else:
475            request._logger()
476
477
478## This should be moved somewhere else, it's cluttering up this module.
479
480class OldResourceAdapter(object):
481    implements(inevow.IResource)
482
483    # This is required to properly handle the interaction between
484    # original.isLeaf and request.postpath, from which PATH_INFO is set in
485    # twcgi. Because we have no choice but to consume all elements in
486    # locateChild to terminate the recursion, we do so, but first save the
487    # length of prepath in real_prepath_len. Subsequently in renderHTTP, if
488    # real_prepath_len is not None, prepath is correct to the saved length and
489    # the extra segments moved to postpath. If real_prepath_len is None, then
490    # locateChild has never been called, so we know not the real length, so we
491    # do nothing, which is correct.
492    real_prepath_len = None
493
494    def __init__(self, original):
495        self.original = original
496
497    def __repr__(self):
498        return "<%s @ 0x%x adapting %r>" % (self.__class__.__name__, id(self), self.original)
499
500    def locateChild(self, ctx, segments):
501        request = inevow.IRequest(ctx)
502        if self.original.isLeaf:
503            self.real_prepath_len = len(request.prepath)
504            return self, ()
505        name = segments[0]
506        request.prepath.append(request.postpath.pop(0))
507        res = self.original.getChildWithDefault(name, request)
508        request.postpath.insert(0, request.prepath.pop())
509        if isinstance(res, defer.Deferred):
510            return res.addCallback(lambda res: (res, segments[1:]))
511        return res, segments[1:]
512
513    def _handle_NOT_DONE_YET(self, data, request):
514        if data == server.NOT_DONE_YET:
515            return request.deferred
516        else:
517            return data
518
519    def renderHTTP(self, ctx):
520        request = inevow.IRequest(ctx)
521        if self.real_prepath_len is not None:
522            request.postpath = request.prepath[self.real_prepath_len:]
523            del request.prepath[self.real_prepath_len:]
524        result = defer.maybeDeferred(self.original.render, request).addCallback(
525            self._handle_NOT_DONE_YET, request)
526        return result
527
528    def willHandle_notFound(self, request):
529        if hasattr(self.original, 'willHandle_notFound'):
530            return self.original.willHandle_notFound(request)
531        return False
532
533    def renderHTTP_notFound(self, ctx):
534        return self.original.renderHTTP_notFound(ctx)
535
536
537from nevow import rend
538
539NotFound = rend.NotFound
540FourOhFour = rend.FourOhFour
541
542classImplements(server.Session, inevow.ISession)
543