summaryrefslogtreecommitdiff
path: root/routes/middleware.py
blob: 4ee47da908775a9f9667c0e5cbe25693fbcf8ec6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""Routes WSGI Middleware"""
import re
import logging

from webob import Request

from routes.base import request_config
from routes.util import URLGenerator

log = logging.getLogger('routes.middleware')


class RoutesMiddleware(object):
    """Routing middleware that handles resolving the PATH_INFO in
    addition to optionally recognizing method overriding.

    .. Note::
        This module requires webob to be installed. To depend on it, you may
        list routes[middleware] in your ``requirements.txt``
    """
    def __init__(self, wsgi_app, mapper, use_method_override=True,
                 path_info=True, singleton=True):
        """Create a Route middleware object

        Using the use_method_override keyword will require Paste to be
        installed, and your application should use Paste's WSGIRequest
        object as it will properly handle POST issues with wsgi.input
        should Routes check it.

        If path_info is True, then should a route var contain
        path_info, the SCRIPT_NAME and PATH_INFO will be altered
        accordingly. This should be used with routes like:

        .. code-block:: python

            map.connect('blog/*path_info', controller='blog', path_info='')

        """
        self.app = wsgi_app
        self.mapper = mapper
        self.singleton = singleton
        self.use_method_override = use_method_override
        self.path_info = path_info
        self.log_debug = logging.DEBUG >= log.getEffectiveLevel()
        if self.log_debug:
            log.debug("Initialized with method overriding = %s, and path "
                      "info altering = %s", use_method_override, path_info)

    def __call__(self, environ, start_response):
        """Resolves the URL in PATH_INFO, and uses wsgi.routing_args
        to pass on URL resolver results."""
        old_method = None
        if self.use_method_override:
            req = None

            # In some odd cases, there's no query string
            try:
                qs = environ['QUERY_STRING']
            except KeyError:
                qs = ''
            if '_method' in qs:
                req = Request(environ)
                req.errors = 'ignore'
                
                try:
                    method = req.GET.get('_method')
                except UnicodeDecodeError:
                    method = None
                
                if method:
                    old_method = environ['REQUEST_METHOD']
                    environ['REQUEST_METHOD'] = method.upper()
                    if self.log_debug:
                        log.debug("_method found in QUERY_STRING, altering "
                                  "request method to %s",
                                  environ['REQUEST_METHOD'])
            elif environ['REQUEST_METHOD'] == 'POST' and is_form_post(environ):
                if req is None:
                    req = Request(environ)
                    req.errors = 'ignore'

                try:
                    method = req.POST.get('_method')
                except UnicodeDecodeError:
                    method = None
                   
                if method:
                    old_method = environ['REQUEST_METHOD']
                    environ['REQUEST_METHOD'] = method.upper()
                    if self.log_debug:
                        log.debug("_method found in POST data, altering "
                                  "request method to %s",
                                  environ['REQUEST_METHOD'])

        # Run the actual route matching
        # -- Assignment of environ to config triggers route matching
        if self.singleton:
            config = request_config()
            config.mapper = self.mapper
            config.environ = environ
            match = config.mapper_dict
            route = config.route
        else:
            results = self.mapper.routematch(environ=environ)
            if results:
                match, route = results[0], results[1]
            else:
                match = route = None

        if old_method:
            environ['REQUEST_METHOD'] = old_method

        if not match:
            match = {}
            if self.log_debug:
                urlinfo = "%s %s" % (environ['REQUEST_METHOD'],
                                     environ['PATH_INFO'])
                log.debug("No route matched for %s", urlinfo)
        elif self.log_debug:
            urlinfo = "%s %s" % (environ['REQUEST_METHOD'],
                                 environ['PATH_INFO'])
            log.debug("Matched %s", urlinfo)
            log.debug("Route path: '%s', defaults: %s", route.routepath,
                      route.defaults)
            log.debug("Match dict: %s", match)

        url = URLGenerator(self.mapper, environ)
        environ['wsgiorg.routing_args'] = ((url), match)
        environ['routes.route'] = route
        environ['routes.url'] = url

        if route and route.redirect:
            route_name = '_redirect_%s' % id(route)
            location = url(route_name, **match)
            log.debug("Using redirect route, redirect to '%s' with status"
                      "code: %s", location, route.redirect_status)
            start_response(route.redirect_status,
                           [('Content-Type', 'text/plain; charset=utf8'),
                            ('Location', location)])
            return []

        # If the route included a path_info attribute and it should be used to
        # alter the environ, we'll pull it out
        if self.path_info and 'path_info' in match:
            oldpath = environ['PATH_INFO']
            newpath = match.get('path_info') or ''
            environ['PATH_INFO'] = newpath
            if not environ['PATH_INFO'].startswith('/'):
                environ['PATH_INFO'] = '/' + environ['PATH_INFO']
            environ['SCRIPT_NAME'] += re.sub(
                r'^(.*?)/' + re.escape(newpath) + '$', r'\1', oldpath)

        response = self.app(environ, start_response)

        # Wrapped in try as in rare cases the attribute will be gone already
        try:
            del self.mapper.environ
        except AttributeError:
            pass
        return response


def is_form_post(environ):
    """Determine whether the request is a POSTed html form"""
    content_type = environ.get('CONTENT_TYPE', '').lower()
    if ';' in content_type:
        content_type = content_type.split(';', 1)[0]
    return content_type in ('application/x-www-form-urlencoded',
                            'multipart/form-data')