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