1"""
2The Request class is used as a wrapper around the standard request object.
3
4The wrapped request then offers a richer API, in particular :
5
6    - content automatically parsed according to `Content-Type` header,
7      and available as `request.data`
8    - full support of PUT method, including support for file uploads
9    - form overloading of HTTP method, content type and content
10"""
11import io
12import sys
13from contextlib import contextmanager
14
15from django.conf import settings
16from django.http import HttpRequest, QueryDict
17from django.http.multipartparser import parse_header
18from django.http.request import RawPostDataException
19from django.utils.datastructures import MultiValueDict
20
21from rest_framework import HTTP_HEADER_ENCODING, exceptions
22from rest_framework.settings import api_settings
23
24
25def is_form_media_type(media_type):
26    """
27    Return True if the media type is a valid form media type.
28    """
29    base_media_type, params = parse_header(media_type.encode(HTTP_HEADER_ENCODING))
30    return (base_media_type == 'application/x-www-form-urlencoded' or
31            base_media_type == 'multipart/form-data')
32
33
34class override_method:
35    """
36    A context manager that temporarily overrides the method on a request,
37    additionally setting the `view.request` attribute.
38
39    Usage:
40
41        with override_method(view, request, 'POST') as request:
42            ... # Do stuff with `view` and `request`
43    """
44
45    def __init__(self, view, request, method):
46        self.view = view
47        self.request = request
48        self.method = method
49        self.action = getattr(view, 'action', None)
50
51    def __enter__(self):
52        self.view.request = clone_request(self.request, self.method)
53        # For viewsets we also set the `.action` attribute.
54        action_map = getattr(self.view, 'action_map', {})
55        self.view.action = action_map.get(self.method.lower())
56        return self.view.request
57
58    def __exit__(self, *args, **kwarg):
59        self.view.request = self.request
60        self.view.action = self.action
61
62
63class WrappedAttributeError(Exception):
64    pass
65
66
67@contextmanager
68def wrap_attributeerrors():
69    """
70    Used to re-raise AttributeErrors caught during authentication, preventing
71    these errors from otherwise being handled by the attribute access protocol.
72    """
73    try:
74        yield
75    except AttributeError:
76        info = sys.exc_info()
77        exc = WrappedAttributeError(str(info[1]))
78        raise exc.with_traceback(info[2])
79
80
81class Empty:
82    """
83    Placeholder for unset attributes.
84    Cannot use `None`, as that may be a valid value.
85    """
86    pass
87
88
89def _hasattr(obj, name):
90    return not getattr(obj, name) is Empty
91
92
93def clone_request(request, method):
94    """
95    Internal helper method to clone a request, replacing with a different
96    HTTP method.  Used for checking permissions against other methods.
97    """
98    ret = Request(request=request._request,
99                  parsers=request.parsers,
100                  authenticators=request.authenticators,
101                  negotiator=request.negotiator,
102                  parser_context=request.parser_context)
103    ret._data = request._data
104    ret._files = request._files
105    ret._full_data = request._full_data
106    ret._content_type = request._content_type
107    ret._stream = request._stream
108    ret.method = method
109    if hasattr(request, '_user'):
110        ret._user = request._user
111    if hasattr(request, '_auth'):
112        ret._auth = request._auth
113    if hasattr(request, '_authenticator'):
114        ret._authenticator = request._authenticator
115    if hasattr(request, 'accepted_renderer'):
116        ret.accepted_renderer = request.accepted_renderer
117    if hasattr(request, 'accepted_media_type'):
118        ret.accepted_media_type = request.accepted_media_type
119    if hasattr(request, 'version'):
120        ret.version = request.version
121    if hasattr(request, 'versioning_scheme'):
122        ret.versioning_scheme = request.versioning_scheme
123    return ret
124
125
126class ForcedAuthentication:
127    """
128    This authentication class is used if the test client or request factory
129    forcibly authenticated the request.
130    """
131
132    def __init__(self, force_user, force_token):
133        self.force_user = force_user
134        self.force_token = force_token
135
136    def authenticate(self, request):
137        return (self.force_user, self.force_token)
138
139
140class Request:
141    """
142    Wrapper allowing to enhance a standard `HttpRequest` instance.
143
144    Kwargs:
145        - request(HttpRequest). The original request instance.
146        - parsers(list/tuple). The parsers to use for parsing the
147          request content.
148        - authenticators(list/tuple). The authenticators used to try
149          authenticating the request's user.
150    """
151
152    def __init__(self, request, parsers=None, authenticators=None,
153                 negotiator=None, parser_context=None):
154        assert isinstance(request, HttpRequest), (
155            'The `request` argument must be an instance of '
156            '`django.http.HttpRequest`, not `{}.{}`.'
157            .format(request.__class__.__module__, request.__class__.__name__)
158        )
159
160        self._request = request
161        self.parsers = parsers or ()
162        self.authenticators = authenticators or ()
163        self.negotiator = negotiator or self._default_negotiator()
164        self.parser_context = parser_context
165        self._data = Empty
166        self._files = Empty
167        self._full_data = Empty
168        self._content_type = Empty
169        self._stream = Empty
170
171        if self.parser_context is None:
172            self.parser_context = {}
173        self.parser_context['request'] = self
174        self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET
175
176        force_user = getattr(request, '_force_auth_user', None)
177        force_token = getattr(request, '_force_auth_token', None)
178        if force_user is not None or force_token is not None:
179            forced_auth = ForcedAuthentication(force_user, force_token)
180            self.authenticators = (forced_auth,)
181
182    def __repr__(self):
183        return '<%s.%s: %s %r>' % (
184            self.__class__.__module__,
185            self.__class__.__name__,
186            self.method,
187            self.get_full_path())
188
189    def _default_negotiator(self):
190        return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()
191
192    @property
193    def content_type(self):
194        meta = self._request.META
195        return meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', ''))
196
197    @property
198    def stream(self):
199        """
200        Returns an object that may be used to stream the request content.
201        """
202        if not _hasattr(self, '_stream'):
203            self._load_stream()
204        return self._stream
205
206    @property
207    def query_params(self):
208        """
209        More semantically correct name for request.GET.
210        """
211        return self._request.GET
212
213    @property
214    def data(self):
215        if not _hasattr(self, '_full_data'):
216            self._load_data_and_files()
217        return self._full_data
218
219    @property
220    def user(self):
221        """
222        Returns the user associated with the current request, as authenticated
223        by the authentication classes provided to the request.
224        """
225        if not hasattr(self, '_user'):
226            with wrap_attributeerrors():
227                self._authenticate()
228        return self._user
229
230    @user.setter
231    def user(self, value):
232        """
233        Sets the user on the current request. This is necessary to maintain
234        compatibility with django.contrib.auth where the user property is
235        set in the login and logout functions.
236
237        Note that we also set the user on Django's underlying `HttpRequest`
238        instance, ensuring that it is available to any middleware in the stack.
239        """
240        self._user = value
241        self._request.user = value
242
243    @property
244    def auth(self):
245        """
246        Returns any non-user authentication information associated with the
247        request, such as an authentication token.
248        """
249        if not hasattr(self, '_auth'):
250            with wrap_attributeerrors():
251                self._authenticate()
252        return self._auth
253
254    @auth.setter
255    def auth(self, value):
256        """
257        Sets any non-user authentication information associated with the
258        request, such as an authentication token.
259        """
260        self._auth = value
261        self._request.auth = value
262
263    @property
264    def successful_authenticator(self):
265        """
266        Return the instance of the authentication instance class that was used
267        to authenticate the request, or `None`.
268        """
269        if not hasattr(self, '_authenticator'):
270            with wrap_attributeerrors():
271                self._authenticate()
272        return self._authenticator
273
274    def _load_data_and_files(self):
275        """
276        Parses the request content into `self.data`.
277        """
278        if not _hasattr(self, '_data'):
279            self._data, self._files = self._parse()
280            if self._files:
281                self._full_data = self._data.copy()
282                self._full_data.update(self._files)
283            else:
284                self._full_data = self._data
285
286            # if a form media type, copy data & files refs to the underlying
287            # http request so that closable objects are handled appropriately.
288            if is_form_media_type(self.content_type):
289                self._request._post = self.POST
290                self._request._files = self.FILES
291
292    def _load_stream(self):
293        """
294        Return the content body of the request, as a stream.
295        """
296        meta = self._request.META
297        try:
298            content_length = int(
299                meta.get('CONTENT_LENGTH', meta.get('HTTP_CONTENT_LENGTH', 0))
300            )
301        except (ValueError, TypeError):
302            content_length = 0
303
304        if content_length == 0:
305            self._stream = None
306        elif not self._request._read_started:
307            self._stream = self._request
308        else:
309            self._stream = io.BytesIO(self.body)
310
311    def _supports_form_parsing(self):
312        """
313        Return True if this requests supports parsing form data.
314        """
315        form_media = (
316            'application/x-www-form-urlencoded',
317            'multipart/form-data'
318        )
319        return any([parser.media_type in form_media for parser in self.parsers])
320
321    def _parse(self):
322        """
323        Parse the request content, returning a two-tuple of (data, files)
324
325        May raise an `UnsupportedMediaType`, or `ParseError` exception.
326        """
327        media_type = self.content_type
328        try:
329            stream = self.stream
330        except RawPostDataException:
331            if not hasattr(self._request, '_post'):
332                raise
333            # If request.POST has been accessed in middleware, and a method='POST'
334            # request was made with 'multipart/form-data', then the request stream
335            # will already have been exhausted.
336            if self._supports_form_parsing():
337                return (self._request.POST, self._request.FILES)
338            stream = None
339
340        if stream is None or media_type is None:
341            if media_type and is_form_media_type(media_type):
342                empty_data = QueryDict('', encoding=self._request._encoding)
343            else:
344                empty_data = {}
345            empty_files = MultiValueDict()
346            return (empty_data, empty_files)
347
348        parser = self.negotiator.select_parser(self, self.parsers)
349
350        if not parser:
351            raise exceptions.UnsupportedMediaType(media_type)
352
353        try:
354            parsed = parser.parse(stream, media_type, self.parser_context)
355        except Exception:
356            # If we get an exception during parsing, fill in empty data and
357            # re-raise.  Ensures we don't simply repeat the error when
358            # attempting to render the browsable renderer response, or when
359            # logging the request or similar.
360            self._data = QueryDict('', encoding=self._request._encoding)
361            self._files = MultiValueDict()
362            self._full_data = self._data
363            raise
364
365        # Parser classes may return the raw data, or a
366        # DataAndFiles object.  Unpack the result as required.
367        try:
368            return (parsed.data, parsed.files)
369        except AttributeError:
370            empty_files = MultiValueDict()
371            return (parsed, empty_files)
372
373    def _authenticate(self):
374        """
375        Attempt to authenticate the request using each authentication instance
376        in turn.
377        """
378        for authenticator in self.authenticators:
379            try:
380                user_auth_tuple = authenticator.authenticate(self)
381            except exceptions.APIException:
382                self._not_authenticated()
383                raise
384
385            if user_auth_tuple is not None:
386                self._authenticator = authenticator
387                self.user, self.auth = user_auth_tuple
388                return
389
390        self._not_authenticated()
391
392    def _not_authenticated(self):
393        """
394        Set authenticator, user & authtoken representing an unauthenticated request.
395
396        Defaults are None, AnonymousUser & None.
397        """
398        self._authenticator = None
399
400        if api_settings.UNAUTHENTICATED_USER:
401            self.user = api_settings.UNAUTHENTICATED_USER()
402        else:
403            self.user = None
404
405        if api_settings.UNAUTHENTICATED_TOKEN:
406            self.auth = api_settings.UNAUTHENTICATED_TOKEN()
407        else:
408            self.auth = None
409
410    def __getattr__(self, attr):
411        """
412        If an attribute does not exist on this instance, then we also attempt
413        to proxy it to the underlying HttpRequest object.
414        """
415        try:
416            return getattr(self._request, attr)
417        except AttributeError:
418            return self.__getattribute__(attr)
419
420    @property
421    def DATA(self):
422        raise NotImplementedError(
423            '`request.DATA` has been deprecated in favor of `request.data` '
424            'since version 3.0, and has been fully removed as of version 3.2.'
425        )
426
427    @property
428    def POST(self):
429        # Ensure that request.POST uses our request parsing.
430        if not _hasattr(self, '_data'):
431            self._load_data_and_files()
432        if is_form_media_type(self.content_type):
433            return self._data
434        return QueryDict('', encoding=self._request._encoding)
435
436    @property
437    def FILES(self):
438        # Leave this one alone for backwards compat with Django's request.FILES
439        # Different from the other two cases, which are not valid property
440        # names on the WSGIRequest class.
441        if not _hasattr(self, '_files'):
442            self._load_data_and_files()
443        return self._files
444
445    @property
446    def QUERY_PARAMS(self):
447        raise NotImplementedError(
448            '`request.QUERY_PARAMS` has been deprecated in favor of `request.query_params` '
449            'since version 3.0, and has been fully removed as of version 3.2.'
450        )
451
452    def force_plaintext_errors(self, value):
453        # Hack to allow our exception handler to force choice of
454        # plaintext or html error responses.
455        self._request.is_ajax = lambda: value
456