Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

""" 

The Request class is used as a wrapper around the standard request object. 

 

The wrapped request then offers a richer API, in particular : 

 

    - content automatically parsed according to `Content-Type` header, 

      and available as `request.DATA` 

    - full support of PUT method, including support for file uploads 

    - form overloading of HTTP method, content type and content 

""" 

from __future__ import unicode_literals 

from django.conf import settings 

from django.http import QueryDict 

from django.http.multipartparser import parse_header 

from django.utils.datastructures import MultiValueDict 

from rest_framework import HTTP_HEADER_ENCODING 

from rest_framework import exceptions 

from rest_framework.compat import BytesIO 

from rest_framework.settings import api_settings 

 

 

def is_form_media_type(media_type): 

    """ 

    Return True if the media type is a valid form media type. 

    """ 

    base_media_type, params = parse_header(media_type.encode(HTTP_HEADER_ENCODING)) 

    return (base_media_type == 'application/x-www-form-urlencoded' or 

            base_media_type == 'multipart/form-data') 

 

 

class Empty(object): 

    """ 

    Placeholder for unset attributes. 

    Cannot use `None`, as that may be a valid value. 

    """ 

    pass 

 

 

def _hasattr(obj, name): 

    return not getattr(obj, name) is Empty 

 

 

def clone_request(request, method): 

    """ 

    Internal helper method to clone a request, replacing with a different 

    HTTP method.  Used for checking permissions against other methods. 

    """ 

    ret = Request(request=request._request, 

                  parsers=request.parsers, 

                  authenticators=request.authenticators, 

                  negotiator=request.negotiator, 

                  parser_context=request.parser_context) 

    ret._data = request._data 

    ret._files = request._files 

    ret._content_type = request._content_type 

    ret._stream = request._stream 

    ret._method = method 

    if hasattr(request, '_user'): 

        ret._user = request._user 

    if hasattr(request, '_auth'): 

        ret._auth = request._auth 

    if hasattr(request, '_authenticator'): 

        ret._authenticator = request._authenticator 

    return ret 

 

 

class Request(object): 

    """ 

    Wrapper allowing to enhance a standard `HttpRequest` instance. 

 

    Kwargs: 

        - request(HttpRequest). The original request instance. 

        - parsers_classes(list/tuple). The parsers to use for parsing the 

          request content. 

        - authentication_classes(list/tuple). The authentications used to try 

          authenticating the request's user. 

    """ 

 

    _METHOD_PARAM = api_settings.FORM_METHOD_OVERRIDE 

    _CONTENT_PARAM = api_settings.FORM_CONTENT_OVERRIDE 

    _CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE 

 

    def __init__(self, request, parsers=None, authenticators=None, 

                 negotiator=None, parser_context=None): 

        self._request = request 

        self.parsers = parsers or () 

        self.authenticators = authenticators or () 

        self.negotiator = negotiator or self._default_negotiator() 

        self.parser_context = parser_context 

        self._data = Empty 

        self._files = Empty 

        self._method = Empty 

        self._content_type = Empty 

        self._stream = Empty 

 

        if self.parser_context is None: 

            self.parser_context = {} 

        self.parser_context['request'] = self 

        self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET 

 

    def _default_negotiator(self): 

        return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() 

 

    @property 

    def method(self): 

        """ 

        Returns the HTTP method. 

 

        This allows the `method` to be overridden by using a hidden `form` 

        field on a form POST request. 

        """ 

        if not _hasattr(self, '_method'): 

            self._load_method_and_content_type() 

        return self._method 

 

    @property 

    def content_type(self): 

        """ 

        Returns the content type header. 

 

        This should be used instead of `request.META.get('HTTP_CONTENT_TYPE')`, 

        as it allows the content type to be overridden by using a hidden form 

        field on a form POST request. 

        """ 

        if not _hasattr(self, '_content_type'): 

            self._load_method_and_content_type() 

        return self._content_type 

 

    @property 

    def stream(self): 

        """ 

        Returns an object that may be used to stream the request content. 

        """ 

        if not _hasattr(self, '_stream'): 

            self._load_stream() 

        return self._stream 

 

    @property 

    def QUERY_PARAMS(self): 

        """ 

        More semantically correct name for request.GET. 

        """ 

        return self._request.GET 

 

    @property 

    def DATA(self): 

        """ 

        Parses the request body and returns the data. 

 

        Similar to usual behaviour of `request.POST`, except that it handles 

        arbitrary parsers, and also works on methods other than POST (eg PUT). 

        """ 

        if not _hasattr(self, '_data'): 

            self._load_data_and_files() 

        return self._data 

 

    @property 

    def FILES(self): 

        """ 

        Parses the request body and returns any files uploaded in the request. 

 

        Similar to usual behaviour of `request.FILES`, except that it handles 

        arbitrary parsers, and also works on methods other than POST (eg PUT). 

        """ 

        if not _hasattr(self, '_files'): 

            self._load_data_and_files() 

        return self._files 

 

    @property 

    def user(self): 

        """ 

        Returns the user associated with the current request, as authenticated 

        by the authentication classes provided to the request. 

        """ 

        if not hasattr(self, '_user'): 

            self._authenticate() 

        return self._user 

 

    @user.setter 

    def user(self, value): 

        """ 

        Sets the user on the current request. This is necessary to maintain 

        compatilbility with django.contrib.auth where the user proprety is 

        set in the login and logout functions. 

        """ 

        self._user = value 

 

    @property 

    def auth(self): 

        """ 

        Returns any non-user authentication information associated with the 

        request, such as an authentication token. 

        """ 

        if not hasattr(self, '_auth'): 

            self._authenticate() 

        return self._auth 

 

    @auth.setter 

    def auth(self, value): 

        """ 

        Sets any non-user authentication information associated with the 

        request, such as an authentication token. 

        """ 

        self._auth = value 

 

    @property 

    def successful_authenticator(self): 

        """ 

        Return the instance of the authentication instance class that was used 

        to authenticate the request, or `None`. 

        """ 

        if not hasattr(self, '_authenticator'): 

            self._authenticate() 

        return self._authenticator 

 

    def _load_data_and_files(self): 

        """ 

        Parses the request content into self.DATA and self.FILES. 

        """ 

        if not _hasattr(self, '_content_type'): 

            self._load_method_and_content_type() 

 

        if not _hasattr(self, '_data'): 

            self._data, self._files = self._parse() 

 

    def _load_method_and_content_type(self): 

        """ 

        Sets the method and content_type, and then check if they've 

        been overridden. 

        """ 

        self._content_type = self.META.get('HTTP_CONTENT_TYPE', 

                                           self.META.get('CONTENT_TYPE', '')) 

 

        self._perform_form_overloading() 

 

        if not _hasattr(self, '_method'): 

            self._method = self._request.method 

 

            if self._method == 'POST': 

                # Allow X-HTTP-METHOD-OVERRIDE header 

                self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE', 

                                             self._method) 

 

    def _load_stream(self): 

        """ 

        Return the content body of the request, as a stream. 

        """ 

        try: 

            content_length = int(self.META.get('CONTENT_LENGTH', 

                                    self.META.get('HTTP_CONTENT_LENGTH'))) 

        except (ValueError, TypeError): 

            content_length = 0 

 

        if content_length == 0: 

            self._stream = None 

        elif hasattr(self._request, 'read'): 

            self._stream = self._request 

        else: 

            self._stream = BytesIO(self.raw_post_data) 

 

    def _perform_form_overloading(self): 

        """ 

        If this is a form POST request, then we need to check if the method and 

        content/content_type have been overridden by setting them in hidden 

        form fields or not. 

        """ 

 

        USE_FORM_OVERLOADING = ( 

            self._METHOD_PARAM or 

            (self._CONTENT_PARAM and self._CONTENTTYPE_PARAM) 

        ) 

 

        # We only need to use form overloading on form POST requests. 

        if (not USE_FORM_OVERLOADING 

            or self._request.method != 'POST' 

            or not is_form_media_type(self._content_type)): 

            return 

 

        # At this point we're committed to parsing the request as form data. 

        self._data = self._request.POST 

        self._files = self._request.FILES 

 

        # Method overloading - change the method and remove the param from the content. 

        if (self._METHOD_PARAM and 

            self._METHOD_PARAM in self._data): 

            self._method = self._data[self._METHOD_PARAM].upper() 

 

        # Content overloading - modify the content type, and force re-parse. 

        if (self._CONTENT_PARAM and 

            self._CONTENTTYPE_PARAM and 

            self._CONTENT_PARAM in self._data and 

            self._CONTENTTYPE_PARAM in self._data): 

            self._content_type = self._data[self._CONTENTTYPE_PARAM] 

            self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(HTTP_HEADER_ENCODING)) 

            self._data, self._files = (Empty, Empty) 

 

    def _parse(self): 

        """ 

        Parse the request content, returning a two-tuple of (data, files) 

 

        May raise an `UnsupportedMediaType`, or `ParseError` exception. 

        """ 

        stream = self.stream 

        media_type = self.content_type 

 

        if stream is None or media_type is None: 

            empty_data = QueryDict('', self._request._encoding) 

            empty_files = MultiValueDict() 

            return (empty_data, empty_files) 

 

        parser = self.negotiator.select_parser(self, self.parsers) 

 

        if not parser: 

            raise exceptions.UnsupportedMediaType(media_type) 

 

        parsed = parser.parse(stream, media_type, self.parser_context) 

 

        # Parser classes may return the raw data, or a 

        # DataAndFiles object.  Unpack the result as required. 

        try: 

            return (parsed.data, parsed.files) 

        except AttributeError: 

            empty_files = MultiValueDict() 

            return (parsed, empty_files) 

 

    def _authenticate(self): 

        """ 

        Attempt to authenticate the request using each authentication instance 

        in turn. 

        Returns a three-tuple of (authenticator, user, authtoken). 

        """ 

        for authenticator in self.authenticators: 

            try: 

                user_auth_tuple = authenticator.authenticate(self) 

            except exceptions.APIException: 

                self._not_authenticated() 

                raise 

 

            if not user_auth_tuple is None: 

                self._authenticator = authenticator 

                self._user, self._auth = user_auth_tuple 

                return 

 

        self._not_authenticated() 

 

    def _not_authenticated(self): 

        """ 

        Return a three-tuple of (authenticator, user, authtoken), representing 

        an unauthenticated request. 

 

        By default this will be (None, AnonymousUser, None). 

        """ 

        self._authenticator = None 

 

        if api_settings.UNAUTHENTICATED_USER: 

            self._user = api_settings.UNAUTHENTICATED_USER() 

        else: 

            self._user = None 

 

        if api_settings.UNAUTHENTICATED_TOKEN: 

            self._auth = api_settings.UNAUTHENTICATED_TOKEN() 

        else: 

            self._auth = None 

 

    def __getattr__(self, attr): 

        """ 

        Proxy other attributes to the underlying HttpRequest object. 

        """ 

        return getattr(self._request, attr)