Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/rest_framework/request.py: 60%

239 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

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: 171 ↛ 172line 171 didn't jump to line 172, because the condition on line 171 was never true

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: 178 ↛ 179line 178 didn't jump to line 179, because the condition on line 178 was never true

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'): 202 ↛ 204line 202 didn't jump to line 204, because the condition on line 202 was never false

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'): 215 ↛ 217line 215 didn't jump to line 217, because the condition on line 215 was never false

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'): 269 ↛ 270line 269 didn't jump to line 270, because the condition on line 269 was never true

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'): 278 ↛ exitline 278 didn't return from function '_load_data_and_files', because the condition on line 278 was never false

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: 304 ↛ 305line 304 didn't jump to line 305, because the condition on line 304 was never true

305 self._stream = None 

306 elif not self._request._read_started: 306 ↛ 309line 306 didn't jump to line 309, because the condition on line 306 was never false

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: 340 ↛ 341line 340 didn't jump to line 341, because the condition on line 340 was never true

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: 350 ↛ 351line 350 didn't jump to line 351, because the condition on line 350 was never true

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: 400 ↛ 403line 400 didn't jump to line 403, because the condition on line 400 was never false

401 self.user = api_settings.UNAUTHENTICATED_USER() 

402 else: 

403 self.user = None 

404 

405 if api_settings.UNAUTHENTICATED_TOKEN: 405 ↛ 406line 405 didn't jump to line 406, because the condition on line 405 was never true

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'): 430 ↛ 431line 430 didn't jump to line 431, because the condition on line 430 was never true

431 self._load_data_and_files() 

432 if is_form_media_type(self.content_type): 432 ↛ 434line 432 didn't jump to line 434, because the condition on line 432 was never false

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'): 441 ↛ 442line 441 didn't jump to line 442, because the condition on line 441 was never true

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