aboutsummaryrefslogtreecommitdiffstats
path: root/djangorestframework/parsers.py
blob: 35003a0f7b02ce4cd012f0a02a8e051c90f1ceda (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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
"""Django supports parsing the content of an HTTP request, but only for form POST requests.
That behaviour is sufficient for dealing with standard HTML forms, but it doesn't map well
to general HTTP requests.

We need a method to be able to:

1) Determine the parsed content on a request for methods other than POST (eg typically also PUT)
2) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded
   and multipart/form-data.  (eg also handle multipart/json)
"""
from django.http.multipartparser import MultiPartParser as DjangoMPParser
from django.utils import simplejson as json

from djangorestframework.response import ResponseException
from djangorestframework import status
from djangorestframework.utils import as_tuple
from djangorestframework.mediatypes import MediaType

try:
    from urlparse import parse_qs
except ImportError:
    from cgi import parse_qs

class ParserMixin(object):
    parsers = ()

    def parse(self, stream, content_type):
        """
        Parse the request content.

        May raise a 415 ResponseException (Unsupported Media Type),
        or a 400 ResponseException (Bad Request).
        """
        parsers = as_tuple(self.parsers)

        parser = None
        for parser_cls in parsers:
            if parser_cls.handles(content_type):
                parser = parser_cls(self)
                break

        if parser is None:
            raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
                                    {'error': 'Unsupported media type in request \'%s\'.' %
                                     content_type.media_type})

        return parser.parse(stream)

    @property
    def parsed_media_types(self):
        """Return an list of all the media types that this ParserMixin can parse."""
        return [parser.media_type for parser in self.parsers]
    
    @property
    def default_parser(self):
        """Return the ParerMixin's most prefered emitter.
        (This has no behavioural effect, but is may be used by documenting emitters)"""        
        return self.parsers[0]


class BaseParser(object):
    """All parsers should extend BaseParser, specifying a media_type attribute,
    and overriding the parse() method."""
    media_type = None

    def __init__(self, view):
        """
        Initialise the parser with the View instance as state,
        in case the parser needs to access any metadata on the View object.
        
        """
        self.view = view
    
    @classmethod
    def handles(self, media_type):
        """
        Returns `True` if this parser is able to deal with the given MediaType.
        """
        return media_type.match(self.media_type)

    def parse(self, stream):
        """Given a stream to read from, return the deserialized output.
        The return value may be of any type, but for many parsers it might typically be a dict-like object."""
        raise NotImplementedError("BaseParser.parse() Must be overridden to be implemented.")


class JSONParser(BaseParser):
    media_type = MediaType('application/json')

    def parse(self, stream):
        try:
            return json.load(stream)
        except ValueError, exc:
            raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})


class DataFlatener(object):
    """Utility object for flatening dictionaries of lists. Useful for "urlencoded" decoded data."""

    def flatten_data(self, data):
        """Given a data dictionary {<key>: <value_list>}, returns a flattened dictionary
        with information provided by the method "is_a_list"."""
        flatdata = dict()
        for key, val_list in data.items():
            if self.is_a_list(key, val_list):
                flatdata[key] = val_list
            else:
                if val_list:
                    flatdata[key] = val_list[0]
                else:
                    # If the list is empty, but the parameter is not a list,
                    # we strip this parameter.
                    data.pop(key)
        return flatdata

    def is_a_list(self, key, val_list):
        """Returns True if the parameter with name *key* is expected to be a list, or False otherwise.
        *val_list* which is the received value for parameter *key* can be used to guess the answer."""
        return False


class FormParser(BaseParser, DataFlatener):
    """The default parser for form data.
    Return a dict containing a single value for each non-reserved parameter.

    In order to handle select multiple (and having possibly more than a single value for each parameter),
    you can customize the output by subclassing the method 'is_a_list'."""

    media_type = MediaType('application/x-www-form-urlencoded')

    """The value of the parameter when the select multiple is empty.
    Browsers are usually stripping the select multiple that have no option selected from the parameters sent.
    A common hack to avoid this is to send the parameter with a value specifying that the list is empty.
    This value will always be stripped before the data is returned."""
    EMPTY_VALUE = '_empty'
    RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',)

    def parse(self, stream):
        data = parse_qs(stream.read(), keep_blank_values=True)

        # removing EMPTY_VALUEs from the lists and flatening the data 
        for key, val_list in data.items():
            self.remove_empty_val(val_list)
        data = self.flatten_data(data)

        # Strip any parameters that we are treating as reserved
        for key in data.keys():
            if key in self.RESERVED_FORM_PARAMS:
                data.pop(key)

        return data

    def remove_empty_val(self, val_list):
        """ """
        while(1): # Because there might be several times EMPTY_VALUE in the list
            try: 
                ind = val_list.index(self.EMPTY_VALUE)
            except ValueError:
                break
            else:
                val_list.pop(ind) 


class MultipartData(dict):
    def __init__(self, data, files):
        dict.__init__(self, data)
        self.FILES = files

class MultipartParser(BaseParser, DataFlatener):
    media_type = MediaType('multipart/form-data')
    RESERVED_FORM_PARAMS = ('csrfmiddlewaretoken',)

    def parse(self, stream):
        upload_handlers = self.view.request._get_upload_handlers()
        django_mpp = DjangoMPParser(self.view.request.META, stream, upload_handlers)
        data, files = django_mpp.parse()

        # Flatening data, files and combining them
        data = self.flatten_data(dict(data.iterlists()))
        files = self.flatten_data(dict(files.iterlists()))

        # Strip any parameters that we are treating as reserved
        for key in data.keys():
            if key in self.RESERVED_FORM_PARAMS:
                data.pop(key)
        
        return MultipartData(data, files)