Hide keyboard shortcuts

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

1import binascii 

2import io 

3import os 

4import re 

5import sys 

6import tempfile 

7import mimetypes 

8try: 

9 import simplejson as json 

10except ImportError: 

11 import json 

12import warnings 

13 

14from webob.acceptparse import ( 

15 accept_charset_property, 

16 accept_encoding_property, 

17 accept_language_property, 

18 accept_property, 

19 ) 

20 

21from webob.cachecontrol import ( 

22 CacheControl, 

23 serialize_cache_control, 

24 ) 

25 

26from webob.compat import ( 

27 PY2, 

28 bytes_, 

29 native_, 

30 parse_qsl_text, 

31 reraise, 

32 text_type, 

33 url_encode, 

34 url_quote, 

35 url_unquote, 

36 quote_plus, 

37 urlparse, 

38 cgi_FieldStorage 

39 ) 

40 

41from webob.cookies import RequestCookies 

42 

43from webob.descriptors import ( 

44 CHARSET_RE, 

45 SCHEME_RE, 

46 converter, 

47 converter_date, 

48 environ_getter, 

49 environ_decoder, 

50 parse_auth, 

51 parse_int, 

52 parse_int_safe, 

53 parse_range, 

54 serialize_auth, 

55 serialize_if_range, 

56 serialize_int, 

57 serialize_range, 

58 upath_property, 

59 ) 

60 

61from webob.etag import ( 

62 IfRange, 

63 AnyETag, 

64 NoETag, 

65 etag_property, 

66 ) 

67 

68from webob.headers import EnvironHeaders 

69 

70from webob.multidict import ( 

71 NestedMultiDict, 

72 MultiDict, 

73 NoVars, 

74 GetDict, 

75 ) 

76 

77__all__ = ['BaseRequest', 'Request', 'LegacyRequest'] 

78 

79class _NoDefault: 

80 def __repr__(self): 

81 return '(No Default)' 

82NoDefault = _NoDefault() 

83 

84PATH_SAFE = "/~!$&'()*+,;=:@" 

85 

86_LATIN_ENCODINGS = ( 

87 'ascii', 'latin-1', 'latin', 'latin_1', 'l1', 'latin1', 

88 'iso-8859-1', 'iso8859_1', 'iso_8859_1', 'iso8859', '8859', 

89 ) 

90 

91class BaseRequest(object): 

92 # The limit after which request bodies should be stored on disk 

93 # if they are read in (under this, and the request body is stored 

94 # in memory): 

95 request_body_tempfile_limit = 10 * 1024 

96 

97 _charset = None 

98 

99 def __init__(self, environ, charset=None, unicode_errors=None, 

100 decode_param_names=None, **kw): 

101 

102 if type(environ) is not dict: 

103 raise TypeError( 

104 "WSGI environ must be a dict; you passed %r" % (environ,)) 

105 

106 if unicode_errors is not None: 

107 warnings.warn( 

108 "You unicode_errors=%r to the Request constructor. Passing a " 

109 "``unicode_errors`` value to the Request is no longer " 

110 "supported in WebOb 1.2+. This value has been ignored " % ( 

111 unicode_errors,), 

112 DeprecationWarning 

113 ) 

114 

115 if decode_param_names is not None: 

116 warnings.warn( 

117 "You passed decode_param_names=%r to the Request constructor. " 

118 "Passing a ``decode_param_names`` value to the Request " 

119 "is no longer supported in WebOb 1.2+. This value has " 

120 "been ignored " % (decode_param_names,), 

121 DeprecationWarning 

122 ) 

123 

124 if not _is_utf8(charset): 

125 raise DeprecationWarning( 

126 "You passed charset=%r to the Request constructor. As of " 

127 "WebOb 1.2, if your application needs a non-UTF-8 request " 

128 "charset, please construct the request without a charset or " 

129 "with a charset of 'None', then use ``req = " 

130 "req.decode(charset)``" % charset 

131 ) 

132 

133 d = self.__dict__ 

134 d['environ'] = environ 

135 

136 if kw: 

137 cls = self.__class__ 

138 

139 if 'method' in kw: 

140 # set method first, because .body setters 

141 # depend on it for checks 

142 self.method = kw.pop('method') 

143 

144 for name, value in kw.items(): 

145 if not hasattr(cls, name): 

146 raise TypeError( 

147 "Unexpected keyword: %s=%r" % (name, value)) 

148 setattr(self, name, value) 

149 

150 def encget(self, key, default=NoDefault, encattr=None): 

151 val = self.environ.get(key, default) 

152 if val is NoDefault: 

153 raise KeyError(key) 

154 if val is default: 

155 return default 

156 if not encattr: 

157 return val 

158 encoding = getattr(self, encattr) 

159 

160 if PY2: 

161 return val.decode(encoding) 

162 

163 if encoding in _LATIN_ENCODINGS: # shortcut 

164 return val 

165 return bytes_(val, 'latin-1').decode(encoding) 

166 

167 def encset(self, key, val, encattr=None): 

168 if encattr: 

169 encoding = getattr(self, encattr) 

170 else: 

171 encoding = 'ascii' 

172 if PY2: # pragma: no cover 

173 self.environ[key] = bytes_(val, encoding) 

174 else: 

175 self.environ[key] = bytes_(val, encoding).decode('latin-1') 

176 

177 @property 

178 def charset(self): 

179 if self._charset is None: 

180 charset = detect_charset(self._content_type_raw) 

181 if _is_utf8(charset): 

182 charset = 'UTF-8' 

183 self._charset = charset 

184 return self._charset 

185 

186 @charset.setter 

187 def charset(self, charset): 

188 if _is_utf8(charset): 

189 charset = 'UTF-8' 

190 if charset != self.charset: 

191 raise DeprecationWarning("Use req = req.decode(%r)" % charset) 

192 

193 def decode(self, charset=None, errors='strict'): 

194 charset = charset or self.charset 

195 if charset == 'UTF-8': 

196 return self 

197 # cookies and path are always utf-8 

198 t = Transcoder(charset, errors) 

199 

200 new_content_type = CHARSET_RE.sub('; charset="UTF-8"', 

201 self._content_type_raw) 

202 content_type = self.content_type 

203 r = self.__class__( 

204 self.environ.copy(), 

205 query_string=t.transcode_query(self.query_string), 

206 content_type=new_content_type, 

207 ) 

208 

209 if content_type == 'application/x-www-form-urlencoded': 

210 r.body = bytes_(t.transcode_query(native_(self.body))) 

211 return r 

212 elif content_type != 'multipart/form-data': 

213 return r 

214 

215 fs_environ = self.environ.copy() 

216 fs_environ.setdefault('CONTENT_LENGTH', '0') 

217 fs_environ['QUERY_STRING'] = '' 

218 if PY2: 

219 fs = cgi_FieldStorage(fp=self.body_file, 

220 environ=fs_environ, 

221 keep_blank_values=True) 

222 else: 

223 fs = cgi_FieldStorage(fp=self.body_file, 

224 environ=fs_environ, 

225 keep_blank_values=True, 

226 encoding=charset, 

227 errors=errors) 

228 

229 fout = t.transcode_fs(fs, r._content_type_raw) 

230 

231 # this order is important, because setting body_file 

232 # resets content_length 

233 r.body_file = fout 

234 r.content_length = fout.tell() 

235 fout.seek(0) 

236 return r 

237 

238 # this is necessary for correct warnings depth for both 

239 # BaseRequest and Request (due to AdhocAttrMixin.__setattr__) 

240 _setattr_stacklevel = 2 

241 

242 @property 

243 def body_file(self): 

244 """ 

245 Input stream of the request (wsgi.input). 

246 Setting this property resets the content_length and seekable flag 

247 (unlike setting req.body_file_raw). 

248 """ 

249 

250 if not self.is_body_readable: 

251 return io.BytesIO() 

252 

253 r = self.body_file_raw 

254 clen = self.content_length 

255 

256 if not self.is_body_seekable and clen is not None: 

257 # we need to wrap input in LimitedLengthFile 

258 # but we have to cache the instance as well 

259 # otherwise this would stop working 

260 # (.remaining counter would reset between calls): 

261 # req.body_file.read(100) 

262 # req.body_file.read(100) 

263 env = self.environ 

264 wrapped, raw = env.get('webob._body_file', (0, 0)) 

265 

266 if raw is not r: 

267 wrapped = LimitedLengthFile(r, clen) 

268 wrapped = io.BufferedReader(wrapped) 

269 env['webob._body_file'] = wrapped, r 

270 r = wrapped 

271 

272 return r 

273 

274 @body_file.setter 

275 def body_file(self, value): 

276 if isinstance(value, bytes): 

277 raise ValueError('Excepted fileobj but received bytes.') 

278 

279 self.content_length = None 

280 self.body_file_raw = value 

281 self.is_body_seekable = False 

282 self.is_body_readable = True 

283 

284 @body_file.deleter 

285 def body_file(self): 

286 self.body = b'' 

287 

288 body_file_raw = environ_getter('wsgi.input') 

289 

290 @property 

291 def body_file_seekable(self): 

292 """ 

293 Get the body of the request (wsgi.input) as a seekable file-like 

294 object. Middleware and routing applications should use this 

295 attribute over .body_file. 

296 

297 If you access this value, CONTENT_LENGTH will also be updated. 

298 """ 

299 if not self.is_body_seekable: 

300 self.make_body_seekable() 

301 return self.body_file_raw 

302 

303 url_encoding = environ_getter('webob.url_encoding', 'UTF-8') 

304 scheme = environ_getter('wsgi.url_scheme') 

305 method = environ_getter('REQUEST_METHOD', 'GET') 

306 http_version = environ_getter('SERVER_PROTOCOL') 

307 content_length = converter( 

308 environ_getter('CONTENT_LENGTH', None, '14.13'), 

309 parse_int_safe, serialize_int, 'int') 

310 remote_user = environ_getter('REMOTE_USER', None) 

311 remote_host = environ_getter('REMOTE_HOST', None) 

312 remote_addr = environ_getter('REMOTE_ADDR', None) 

313 query_string = environ_getter('QUERY_STRING', '') 

314 server_name = environ_getter('SERVER_NAME') 

315 server_port = converter( 

316 environ_getter('SERVER_PORT'), 

317 parse_int, serialize_int, 'int') 

318 

319 script_name = environ_decoder('SCRIPT_NAME', '', encattr='url_encoding') 

320 path_info = environ_decoder('PATH_INFO', encattr='url_encoding') 

321 

322 # bw compat 

323 uscript_name = script_name 

324 upath_info = path_info 

325 

326 _content_type_raw = environ_getter('CONTENT_TYPE', '') 

327 

328 def _content_type__get(self): 

329 """Return the content type, but leaving off any parameters (like 

330 charset, but also things like the type in ``application/atom+xml; 

331 type=entry``) 

332 

333 If you set this property, you can include parameters, or if 

334 you don't include any parameters in the value then existing 

335 parameters will be preserved. 

336 """ 

337 return self._content_type_raw.split(';', 1)[0] 

338 def _content_type__set(self, value=None): 

339 if value is not None: 

340 value = str(value) 

341 if ';' not in value: 

342 content_type = self._content_type_raw 

343 if ';' in content_type: 

344 value += ';' + content_type.split(';', 1)[1] 

345 self._content_type_raw = value 

346 

347 content_type = property(_content_type__get, 

348 _content_type__set, 

349 _content_type__set, 

350 _content_type__get.__doc__) 

351 

352 _headers = None 

353 

354 def _headers__get(self): 

355 """ 

356 All the request headers as a case-insensitive dictionary-like 

357 object. 

358 """ 

359 if self._headers is None: 

360 self._headers = EnvironHeaders(self.environ) 

361 return self._headers 

362 

363 def _headers__set(self, value): 

364 self.headers.clear() 

365 self.headers.update(value) 

366 

367 headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__) 

368 

369 @property 

370 def client_addr(self): 

371 """ 

372 The effective client IP address as a string. If the 

373 ``HTTP_X_FORWARDED_FOR`` header exists in the WSGI environ, this 

374 attribute returns the client IP address present in that header 

375 (e.g. if the header value is ``192.168.1.1, 192.168.1.2``, the value 

376 will be ``192.168.1.1``). If no ``HTTP_X_FORWARDED_FOR`` header is 

377 present in the environ at all, this attribute will return the value 

378 of the ``REMOTE_ADDR`` header. If the ``REMOTE_ADDR`` header is 

379 unset, this attribute will return the value ``None``. 

380 

381 .. warning:: 

382 

383 It is possible for user agents to put someone else's IP or just 

384 any string in ``HTTP_X_FORWARDED_FOR`` as it is a normal HTTP 

385 header. Forward proxies can also provide incorrect values (private 

386 IP addresses etc). You cannot "blindly" trust the result of this 

387 method to provide you with valid data unless you're certain that 

388 ``HTTP_X_FORWARDED_FOR`` has the correct values. The WSGI server 

389 must be behind a trusted proxy for this to be true. 

390 """ 

391 e = self.environ 

392 xff = e.get('HTTP_X_FORWARDED_FOR') 

393 if xff is not None: 

394 addr = xff.split(',')[0].strip() 

395 else: 

396 addr = e.get('REMOTE_ADDR') 

397 return addr 

398 

399 @property 

400 def host_port(self): 

401 """ 

402 The effective server port number as a string. If the ``HTTP_HOST`` 

403 header exists in the WSGI environ, this attribute returns the port 

404 number present in that header. If the ``HTTP_HOST`` header exists but 

405 contains no explicit port number: if the WSGI url scheme is "https" , 

406 this attribute returns "443", if the WSGI url scheme is "http", this 

407 attribute returns "80" . If no ``HTTP_HOST`` header is present in 

408 the environ at all, this attribute will return the value of the 

409 ``SERVER_PORT`` header (which is guaranteed to be present). 

410 """ 

411 e = self.environ 

412 host = e.get('HTTP_HOST') 

413 if host is not None: 

414 if ':' in host and host[-1] != ']': 

415 host, port = host.rsplit(':', 1) 

416 else: 

417 url_scheme = e['wsgi.url_scheme'] 

418 if url_scheme == 'https': 

419 port = '443' 

420 else: 

421 port = '80' 

422 else: 

423 port = e['SERVER_PORT'] 

424 return port 

425 

426 @property 

427 def host_url(self): 

428 """ 

429 The URL through the host (no path) 

430 """ 

431 e = self.environ 

432 scheme = e.get('wsgi.url_scheme') 

433 url = scheme + '://' 

434 host = e.get('HTTP_HOST') 

435 if host is not None: 

436 if ':' in host and host[-1] != ']': 

437 host, port = host.rsplit(':', 1) 

438 else: 

439 port = None 

440 else: 

441 host = e.get('SERVER_NAME') 

442 port = e.get('SERVER_PORT') 

443 if scheme == 'https': 

444 if port == '443': 

445 port = None 

446 elif scheme == 'http': 

447 if port == '80': 

448 port = None 

449 url += host 

450 if port: 

451 url += ':%s' % port 

452 return url 

453 

454 @property 

455 def application_url(self): 

456 """ 

457 The URL including SCRIPT_NAME (no PATH_INFO or query string) 

458 """ 

459 bscript_name = bytes_(self.script_name, self.url_encoding) 

460 return self.host_url + url_quote(bscript_name, PATH_SAFE) 

461 

462 @property 

463 def path_url(self): 

464 """ 

465 The URL including SCRIPT_NAME and PATH_INFO, but not QUERY_STRING 

466 """ 

467 bpath_info = bytes_(self.path_info, self.url_encoding) 

468 return self.application_url + url_quote(bpath_info, PATH_SAFE) 

469 

470 @property 

471 def path(self): 

472 """ 

473 The path of the request, without host or query string 

474 """ 

475 bscript = bytes_(self.script_name, self.url_encoding) 

476 bpath = bytes_(self.path_info, self.url_encoding) 

477 return url_quote(bscript, PATH_SAFE) + url_quote(bpath, PATH_SAFE) 

478 

479 @property 

480 def path_qs(self): 

481 """ 

482 The path of the request, without host but with query string 

483 """ 

484 path = self.path 

485 qs = self.environ.get('QUERY_STRING') 

486 if qs: 

487 path += '?' + qs 

488 return path 

489 

490 @property 

491 def url(self): 

492 """ 

493 The full request URL, including QUERY_STRING 

494 """ 

495 url = self.path_url 

496 qs = self.environ.get('QUERY_STRING') 

497 if qs: 

498 url += '?' + qs 

499 return url 

500 

501 def relative_url(self, other_url, to_application=False): 

502 """ 

503 Resolve other_url relative to the request URL. 

504 

505 If ``to_application`` is True, then resolve it relative to the 

506 URL with only SCRIPT_NAME 

507 """ 

508 if to_application: 

509 url = self.application_url 

510 if not url.endswith('/'): 

511 url += '/' 

512 else: 

513 url = self.path_url 

514 return urlparse.urljoin(url, other_url) 

515 

516 def path_info_pop(self, pattern=None): 

517 """ 

518 'Pops' off the next segment of PATH_INFO, pushing it onto 

519 SCRIPT_NAME, and returning the popped segment. Returns None if 

520 there is nothing left on PATH_INFO. 

521 

522 Does not return ``''`` when there's an empty segment (like 

523 ``/path//path``); these segments are just ignored. 

524 

525 Optional ``pattern`` argument is a regexp to match the return value 

526 before returning. If there is no match, no changes are made to the 

527 request and None is returned. 

528 """ 

529 path = self.path_info 

530 if not path: 

531 return None 

532 slashes = '' 

533 while path.startswith('/'): 

534 slashes += '/' 

535 path = path[1:] 

536 idx = path.find('/') 

537 if idx == -1: 

538 idx = len(path) 

539 r = path[:idx] 

540 if pattern is None or re.match(pattern, r): 

541 self.script_name += slashes + r 

542 self.path_info = path[idx:] 

543 return r 

544 

545 def path_info_peek(self): 

546 """ 

547 Returns the next segment on PATH_INFO, or None if there is no 

548 next segment. Doesn't modify the environment. 

549 """ 

550 path = self.path_info 

551 if not path: 

552 return None 

553 path = path.lstrip('/') 

554 return path.split('/', 1)[0] 

555 

556 def _urlvars__get(self): 

557 """ 

558 Return any *named* variables matched in the URL. 

559 

560 Takes values from ``environ['wsgiorg.routing_args']``. 

561 Systems like ``routes`` set this value. 

562 """ 

563 if 'paste.urlvars' in self.environ: 

564 return self.environ['paste.urlvars'] 

565 elif 'wsgiorg.routing_args' in self.environ: 

566 return self.environ['wsgiorg.routing_args'][1] 

567 else: 

568 result = {} 

569 self.environ['wsgiorg.routing_args'] = ((), result) 

570 return result 

571 

572 def _urlvars__set(self, value): 

573 environ = self.environ 

574 if 'wsgiorg.routing_args' in environ: 

575 environ['wsgiorg.routing_args'] = ( 

576 environ['wsgiorg.routing_args'][0], value) 

577 if 'paste.urlvars' in environ: 

578 del environ['paste.urlvars'] 

579 elif 'paste.urlvars' in environ: 

580 environ['paste.urlvars'] = value 

581 else: 

582 environ['wsgiorg.routing_args'] = ((), value) 

583 

584 def _urlvars__del(self): 

585 if 'paste.urlvars' in self.environ: 

586 del self.environ['paste.urlvars'] 

587 if 'wsgiorg.routing_args' in self.environ: 

588 if not self.environ['wsgiorg.routing_args'][0]: 

589 del self.environ['wsgiorg.routing_args'] 

590 else: 

591 self.environ['wsgiorg.routing_args'] = ( 

592 self.environ['wsgiorg.routing_args'][0], {}) 

593 

594 urlvars = property(_urlvars__get, 

595 _urlvars__set, 

596 _urlvars__del, 

597 doc=_urlvars__get.__doc__) 

598 

599 def _urlargs__get(self): 

600 """ 

601 Return any *positional* variables matched in the URL. 

602 

603 Takes values from ``environ['wsgiorg.routing_args']``. 

604 Systems like ``routes`` set this value. 

605 """ 

606 if 'wsgiorg.routing_args' in self.environ: 

607 return self.environ['wsgiorg.routing_args'][0] 

608 else: 

609 # Since you can't update this value in-place, we don't need 

610 # to set the key in the environment 

611 return () 

612 

613 def _urlargs__set(self, value): 

614 environ = self.environ 

615 if 'paste.urlvars' in environ: 

616 # Some overlap between this and wsgiorg.routing_args; we need 

617 # wsgiorg.routing_args to make this work 

618 routing_args = (value, environ.pop('paste.urlvars')) 

619 elif 'wsgiorg.routing_args' in environ: 

620 routing_args = (value, environ['wsgiorg.routing_args'][1]) 

621 else: 

622 routing_args = (value, {}) 

623 environ['wsgiorg.routing_args'] = routing_args 

624 

625 def _urlargs__del(self): 

626 if 'wsgiorg.routing_args' in self.environ: 

627 if not self.environ['wsgiorg.routing_args'][1]: 

628 del self.environ['wsgiorg.routing_args'] 

629 else: 

630 self.environ['wsgiorg.routing_args'] = ( 

631 (), self.environ['wsgiorg.routing_args'][1]) 

632 

633 urlargs = property(_urlargs__get, 

634 _urlargs__set, 

635 _urlargs__del, 

636 _urlargs__get.__doc__) 

637 

638 @property 

639 def is_xhr(self): 

640 """Is X-Requested-With header present and equal to ``XMLHttpRequest``? 

641 

642 Note: this isn't set by every XMLHttpRequest request, it is 

643 only set if you are using a Javascript library that sets it 

644 (or you set the header yourself manually). Currently 

645 Prototype and jQuery are known to set this header.""" 

646 return self.environ.get('HTTP_X_REQUESTED_WITH', '') == 'XMLHttpRequest' 

647 

648 def _host__get(self): 

649 """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME""" 

650 if 'HTTP_HOST' in self.environ: 

651 return self.environ['HTTP_HOST'] 

652 else: 

653 return '%(SERVER_NAME)s:%(SERVER_PORT)s' % self.environ 

654 def _host__set(self, value): 

655 self.environ['HTTP_HOST'] = value 

656 def _host__del(self): 

657 if 'HTTP_HOST' in self.environ: 

658 del self.environ['HTTP_HOST'] 

659 host = property(_host__get, _host__set, _host__del, doc=_host__get.__doc__) 

660 

661 @property 

662 def domain(self): 

663 """ Returns the domain portion of the host value. Equivalent to: 

664 

665 .. code-block:: python 

666 

667 domain = request.host 

668 if ':' in domain and domain[-1] != ']': # Check for ] because of IPv6 

669 domain = domain.rsplit(':', 1)[0] 

670 

671 This will be equivalent to the domain portion of the ``HTTP_HOST`` 

672 value in the environment if it exists, or the ``SERVER_NAME`` value in 

673 the environment if it doesn't. For example, if the environment 

674 contains an ``HTTP_HOST`` value of ``foo.example.com:8000``, 

675 ``request.domain`` will return ``foo.example.com``. 

676 

677 Note that this value cannot be *set* on the request. To set the host 

678 value use :meth:`webob.request.Request.host` instead. 

679 """ 

680 domain = self.host 

681 if ':' in domain and domain[-1] != ']': 

682 domain = domain.rsplit(':', 1)[0] 

683 return domain 

684 

685 @property 

686 def body(self): 

687 """ 

688 Return the content of the request body. 

689 """ 

690 if not self.is_body_readable: 

691 return b'' 

692 

693 self.make_body_seekable() # we need this to have content_length 

694 r = self.body_file.read(self.content_length) 

695 self.body_file_raw.seek(0) 

696 return r 

697 

698 @body.setter 

699 def body(self, value): 

700 if value is None: 

701 value = b'' 

702 if not isinstance(value, bytes): 

703 raise TypeError("You can only set Request.body to bytes (not %r)" 

704 % type(value)) 

705 self.content_length = len(value) 

706 self.body_file_raw = io.BytesIO(value) 

707 self.is_body_seekable = True 

708 

709 @body.deleter 

710 def body(self): 

711 self.body = b'' 

712 

713 def _json_body__get(self): 

714 """Access the body of the request as JSON""" 

715 return json.loads(self.body.decode(self.charset)) 

716 

717 def _json_body__set(self, value): 

718 self.body = json.dumps(value, separators=(',', ':')).encode(self.charset) 

719 

720 def _json_body__del(self): 

721 del self.body 

722 

723 json = json_body = property(_json_body__get, _json_body__set, _json_body__del) 

724 

725 def _text__get(self): 

726 """ 

727 Get/set the text value of the body 

728 """ 

729 if not self.charset: 

730 raise AttributeError( 

731 "You cannot access Request.text unless charset is set") 

732 body = self.body 

733 return body.decode(self.charset) 

734 

735 def _text__set(self, value): 

736 if not self.charset: 

737 raise AttributeError( 

738 "You cannot access Response.text unless charset is set") 

739 if not isinstance(value, text_type): 

740 raise TypeError( 

741 "You can only set Request.text to a unicode string " 

742 "(not %s)" % type(value)) 

743 self.body = value.encode(self.charset) 

744 

745 def _text__del(self): 

746 del self.body 

747 

748 text = property(_text__get, _text__set, _text__del, doc=_text__get.__doc__) 

749 

750 @property 

751 def POST(self): 

752 """ 

753 Return a MultiDict containing all the variables from a form 

754 request. Returns an empty dict-like object for non-form requests. 

755 

756 Form requests are typically POST requests, however any other 

757 requests with an appropriate Content-Type are also supported. 

758 """ 

759 env = self.environ 

760 if 'webob._parsed_post_vars' in env: 

761 vars, body_file = env['webob._parsed_post_vars'] 

762 if body_file is self.body_file_raw: 

763 return vars 

764 content_type = self.content_type 

765 if ((self.method != 'POST' and not content_type) 

766 or content_type not in 

767 ('', 

768 'application/x-www-form-urlencoded', 

769 'multipart/form-data') 

770 ): 

771 # Not an HTML form submission 

772 return NoVars('Not an HTML form submission (Content-Type: %s)' 

773 % content_type) 

774 self._check_charset() 

775 

776 self.make_body_seekable() 

777 self.body_file_raw.seek(0) 

778 

779 fs_environ = env.copy() 

780 # FieldStorage assumes a missing CONTENT_LENGTH, but a 

781 # default of 0 is better: 

782 fs_environ.setdefault('CONTENT_LENGTH', '0') 

783 fs_environ['QUERY_STRING'] = '' 

784 if PY2: 

785 fs = cgi_FieldStorage( 

786 fp=self.body_file, 

787 environ=fs_environ, 

788 keep_blank_values=True) 

789 else: 

790 fs = cgi_FieldStorage( 

791 fp=self.body_file, 

792 environ=fs_environ, 

793 keep_blank_values=True, 

794 encoding='utf8') 

795 

796 vars = MultiDict.from_fieldstorage(fs) 

797 env['webob._parsed_post_vars'] = (vars, self.body_file_raw) 

798 return vars 

799 

800 @property 

801 def GET(self): 

802 """ 

803 Return a MultiDict containing all the variables from the 

804 QUERY_STRING. 

805 """ 

806 env = self.environ 

807 source = env.get('QUERY_STRING', '') 

808 if 'webob._parsed_query_vars' in env: 

809 vars, qs = env['webob._parsed_query_vars'] 

810 if qs == source: 

811 return vars 

812 

813 data = [] 

814 if source: 

815 # this is disabled because we want to access req.GET 

816 # for text/plain; charset=ascii uploads for example 

817 #self._check_charset() 

818 data = parse_qsl_text(source) 

819 #d = lambda b: b.decode('utf8') 

820 #data = [(d(k), d(v)) for k,v in data] 

821 vars = GetDict(data, env) 

822 env['webob._parsed_query_vars'] = (vars, source) 

823 return vars 

824 

825 def _check_charset(self): 

826 if self.charset != 'UTF-8': 

827 raise DeprecationWarning( 

828 "Requests are expected to be submitted in UTF-8, not %s. " 

829 "You can fix this by doing req = req.decode('%s')" % ( 

830 self.charset, self.charset) 

831 ) 

832 

833 @property 

834 def params(self): 

835 """ 

836 A dictionary-like object containing both the parameters from 

837 the query string and request body. 

838 """ 

839 params = NestedMultiDict(self.GET, self.POST) 

840 return params 

841 

842 @property 

843 def cookies(self): 

844 """ 

845 Return a dictionary of cookies as found in the request. 

846 """ 

847 return RequestCookies(self.environ) 

848 

849 @cookies.setter 

850 def cookies(self, val): 

851 self.environ.pop('HTTP_COOKIE', None) 

852 r = RequestCookies(self.environ) 

853 r.update(val) 

854 

855 def copy(self): 

856 """ 

857 Copy the request and environment object. 

858 

859 This only does a shallow copy, except of wsgi.input 

860 """ 

861 self.make_body_seekable() 

862 env = self.environ.copy() 

863 new_req = self.__class__(env) 

864 new_req.copy_body() 

865 return new_req 

866 

867 def copy_get(self): 

868 """ 

869 Copies the request and environment object, but turning this request 

870 into a GET along the way. If this was a POST request (or any other 

871 verb) then it becomes GET, and the request body is thrown away. 

872 """ 

873 env = self.environ.copy() 

874 return self.__class__(env, method='GET', content_type=None, 

875 body=b'') 

876 

877 # webob.is_body_seekable marks input streams that are seekable 

878 # this way we can have seekable input without testing the .seek() method 

879 is_body_seekable = environ_getter('webob.is_body_seekable', False) 

880 

881 @property 

882 def is_body_readable(self): 

883 """ 

884 webob.is_body_readable is a flag that tells us that we can read the 

885 input stream even though CONTENT_LENGTH is missing. 

886 """ 

887 

888 clen = self.content_length 

889 

890 if clen is not None and clen != 0: 

891 return True 

892 elif clen is None: 

893 # Rely on the special flag that signifies that either Chunked 

894 # Encoding is allowed (and works) or we have replaced 

895 # self.body_file with something that is readable and EOF's 

896 # correctly. 

897 return self.environ.get( 

898 'wsgi.input_terminated', 

899 # For backwards compatibility, we fall back to checking if 

900 # webob.is_body_readable is set in the environ 

901 self.environ.get( 

902 'webob.is_body_readable', 

903 False 

904 ) 

905 ) 

906 

907 return False 

908 

909 @is_body_readable.setter 

910 def is_body_readable(self, flag): 

911 self.environ['wsgi.input_terminated'] = bool(flag) 

912 

913 def make_body_seekable(self): 

914 """ 

915 This forces ``environ['wsgi.input']`` to be seekable. 

916 That means that, the content is copied into a BytesIO or temporary 

917 file and flagged as seekable, so that it will not be unnecessarily 

918 copied again. 

919 

920 After calling this method the .body_file is always seeked to the 

921 start of file and .content_length is not None. 

922 

923 The choice to copy to BytesIO is made from 

924 ``self.request_body_tempfile_limit`` 

925 """ 

926 if self.is_body_seekable: 

927 self.body_file_raw.seek(0) 

928 else: 

929 self.copy_body() 

930 

931 def copy_body(self): 

932 """ 

933 Copies the body, in cases where it might be shared with another request 

934 object and that is not desired. 

935 

936 This copies the body either into a BytesIO object (through setting 

937 req.body) or a temporary file. 

938 """ 

939 

940 if self.is_body_readable: 

941 # Before we copy, if we can, rewind the body file 

942 if self.is_body_seekable: 

943 self.body_file_raw.seek(0) 

944 

945 tempfile_limit = self.request_body_tempfile_limit 

946 todo = self.content_length if self.content_length is not None else 65535 

947 

948 newbody = b'' 

949 fileobj = None 

950 input = self.body_file 

951 

952 while todo > 0: 

953 data = input.read(min(todo, 65535)) 

954 

955 if not data and self.content_length is None: 

956 # We attempted to read more data, but got none, break. 

957 # This can happen if for instance we are reading as much as 

958 # we can because we don't have a Content-Length... 

959 break 

960 elif not data: 

961 # We have a Content-Length and we attempted to read, but 

962 # there was nothing more to read. Oh the humanity! This 

963 # should rarely if never happen because self.body_file 

964 # should be a LimitedLengthFile which should already have 

965 # raised if there was less data than expected. 

966 raise DisconnectionError( 

967 "Client disconnected (%s more bytes were expected)" % todo 

968 ) 

969 

970 if fileobj: 

971 fileobj.write(data) 

972 else: 

973 newbody += data 

974 

975 # When we have enough data that we need a tempfile, let's 

976 # create one, then clear the temporary variable we were 

977 # using 

978 if len(newbody) > tempfile_limit: 

979 fileobj = self.make_tempfile() 

980 fileobj.write(newbody) 

981 newbody = b'' 

982 

983 # Only decrement todo if Content-Length is set 

984 if self.content_length is not None: 

985 todo -= len(data) 

986 

987 if fileobj: 

988 # We apparently had enough data to need a file 

989 

990 # Set the Content-Length to the amount of data that was just 

991 # written. 

992 self.content_length = fileobj.tell() 

993 

994 # Seek it back to the beginning 

995 fileobj.seek(0) 

996 

997 self.body_file_raw = fileobj 

998 

999 # Allow it to be seeked in the future, so we don't need to copy 

1000 # for things like .body 

1001 self.is_body_seekable = True 

1002 

1003 # Not strictly required since Content-Length is set 

1004 self.is_body_readable = True 

1005 else: 

1006 # No file created, set the body and let it deal with creating 

1007 # Content-Length and other vars. 

1008 self.body = newbody 

1009 else: 

1010 # Always leave the request with a valid body, and this is pretty 

1011 # cheap. 

1012 self.body = b'' 

1013 

1014 def make_tempfile(self): 

1015 """ 

1016 Create a tempfile to store big request body. 

1017 This API is not stable yet. A 'size' argument might be added. 

1018 """ 

1019 return tempfile.TemporaryFile() 

1020 

1021 def remove_conditional_headers(self, 

1022 remove_encoding=True, 

1023 remove_range=True, 

1024 remove_match=True, 

1025 remove_modified=True): 

1026 """ 

1027 Remove headers that make the request conditional. 

1028 

1029 These headers can cause the response to be 304 Not Modified, 

1030 which in some cases you may not want to be possible. 

1031 

1032 This does not remove headers like If-Match, which are used for 

1033 conflict detection. 

1034 """ 

1035 check_keys = [] 

1036 if remove_range: 

1037 check_keys += ['HTTP_IF_RANGE', 'HTTP_RANGE'] 

1038 if remove_match: 

1039 check_keys.append('HTTP_IF_NONE_MATCH') 

1040 if remove_modified: 

1041 check_keys.append('HTTP_IF_MODIFIED_SINCE') 

1042 if remove_encoding: 

1043 check_keys.append('HTTP_ACCEPT_ENCODING') 

1044 

1045 for key in check_keys: 

1046 if key in self.environ: 

1047 del self.environ[key] 

1048 

1049 accept = accept_property() 

1050 accept_charset = accept_charset_property() 

1051 accept_encoding = accept_encoding_property() 

1052 accept_language = accept_language_property() 

1053 

1054 authorization = converter( 

1055 environ_getter('HTTP_AUTHORIZATION', None, '14.8'), 

1056 parse_auth, serialize_auth, 

1057 ) 

1058 

1059 def _cache_control__get(self): 

1060 """ 

1061 Get/set/modify the Cache-Control header (`HTTP spec section 14.9 

1062 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_) 

1063 """ 

1064 env = self.environ 

1065 value = env.get('HTTP_CACHE_CONTROL', '') 

1066 cache_header, cache_obj = env.get('webob._cache_control', (None, None)) 

1067 if cache_obj is not None and cache_header == value: 

1068 return cache_obj 

1069 cache_obj = CacheControl.parse(value, 

1070 updates_to=self._update_cache_control, 

1071 type='request') 

1072 env['webob._cache_control'] = (value, cache_obj) 

1073 return cache_obj 

1074 

1075 def _cache_control__set(self, value): 

1076 env = self.environ 

1077 value = value or '' 

1078 if isinstance(value, dict): 

1079 value = CacheControl(value, type='request') 

1080 if isinstance(value, CacheControl): 

1081 str_value = str(value) 

1082 env['HTTP_CACHE_CONTROL'] = str_value 

1083 env['webob._cache_control'] = (str_value, value) 

1084 else: 

1085 env['HTTP_CACHE_CONTROL'] = str(value) 

1086 env['webob._cache_control'] = (None, None) 

1087 

1088 def _cache_control__del(self): 

1089 env = self.environ 

1090 if 'HTTP_CACHE_CONTROL' in env: 

1091 del env['HTTP_CACHE_CONTROL'] 

1092 if 'webob._cache_control' in env: 

1093 del env['webob._cache_control'] 

1094 

1095 def _update_cache_control(self, prop_dict): 

1096 self.environ['HTTP_CACHE_CONTROL'] = serialize_cache_control(prop_dict) 

1097 

1098 cache_control = property(_cache_control__get, 

1099 _cache_control__set, 

1100 _cache_control__del, 

1101 doc=_cache_control__get.__doc__) 

1102 

1103 

1104 if_match = etag_property('HTTP_IF_MATCH', AnyETag, '14.24') 

1105 if_none_match = etag_property('HTTP_IF_NONE_MATCH', NoETag, '14.26', 

1106 strong=False) 

1107 

1108 date = converter_date(environ_getter('HTTP_DATE', None, '14.8')) 

1109 if_modified_since = converter_date( 

1110 environ_getter('HTTP_IF_MODIFIED_SINCE', None, '14.25')) 

1111 if_unmodified_since = converter_date( 

1112 environ_getter('HTTP_IF_UNMODIFIED_SINCE', None, '14.28')) 

1113 if_range = converter( 

1114 environ_getter('HTTP_IF_RANGE', None, '14.27'), 

1115 IfRange.parse, serialize_if_range, 'IfRange object') 

1116 

1117 

1118 max_forwards = converter( 

1119 environ_getter('HTTP_MAX_FORWARDS', None, '14.31'), 

1120 parse_int, serialize_int, 'int') 

1121 

1122 pragma = environ_getter('HTTP_PRAGMA', None, '14.32') 

1123 

1124 range = converter( 

1125 environ_getter('HTTP_RANGE', None, '14.35'), 

1126 parse_range, serialize_range, 'Range object') 

1127 

1128 referer = environ_getter('HTTP_REFERER', None, '14.36') 

1129 referrer = referer 

1130 

1131 user_agent = environ_getter('HTTP_USER_AGENT', None, '14.43') 

1132 

1133 def __repr__(self): 

1134 try: 

1135 name = '%s %s' % (self.method, self.url) 

1136 except KeyError: 

1137 name = '(invalid WSGI environ)' 

1138 msg = '<%s at 0x%x %s>' % ( 

1139 self.__class__.__name__, 

1140 abs(id(self)), name) 

1141 return msg 

1142 

1143 def as_bytes(self, skip_body=False): 

1144 """ 

1145 Return HTTP bytes representing this request. 

1146 If skip_body is True, exclude the body. 

1147 If skip_body is an integer larger than one, skip body 

1148 only if its length is bigger than that number. 

1149 """ 

1150 url = self.url 

1151 host = self.host_url 

1152 assert url.startswith(host) 

1153 url = url[len(host):] 

1154 parts = [bytes_('%s %s %s' % (self.method, url, self.http_version))] 

1155 

1156 # acquire body before we handle headers so that 

1157 # content-length will be set 

1158 body = None 

1159 if self.is_body_readable: 

1160 if skip_body > 1: 

1161 if len(self.body) > skip_body: 

1162 body = bytes_('<body skipped (len=%s)>' % len(self.body)) 

1163 else: 

1164 skip_body = False 

1165 if not skip_body: 

1166 body = self.body 

1167 

1168 for k, v in sorted(self.headers.items()): 

1169 header = bytes_('%s: %s' % (k, v)) 

1170 parts.append(header) 

1171 

1172 if body: 

1173 parts.extend([b'', body]) 

1174 # HTTP clearly specifies CRLF 

1175 return b'\r\n'.join(parts) 

1176 

1177 def as_text(self): 

1178 bytes = self.as_bytes() 

1179 return bytes.decode(self.charset) 

1180 

1181 __str__ = as_text 

1182 

1183 @classmethod 

1184 def from_bytes(cls, b): 

1185 """ 

1186 Create a request from HTTP bytes data. If the bytes contain 

1187 extra data after the request, raise a ValueError. 

1188 """ 

1189 f = io.BytesIO(b) 

1190 r = cls.from_file(f) 

1191 if f.tell() != len(b): 

1192 raise ValueError("The string contains more data than expected") 

1193 return r 

1194 

1195 @classmethod 

1196 def from_text(cls, s): 

1197 b = bytes_(s, 'utf-8') 

1198 return cls.from_bytes(b) 

1199 

1200 @classmethod 

1201 def from_file(cls, fp): 

1202 """Read a request from a file-like object (it must implement 

1203 ``.read(size)`` and ``.readline()``). 

1204 

1205 It will read up to the end of the request, not the end of the 

1206 file (unless the request is a POST or PUT and has no 

1207 Content-Length, in that case, the entire file is read). 

1208 

1209 This reads the request as represented by ``str(req)``; it may 

1210 not read every valid HTTP request properly. 

1211 """ 

1212 start_line = fp.readline() 

1213 is_text = isinstance(start_line, text_type) 

1214 if is_text: 

1215 crlf = '\r\n' 

1216 colon = ':' 

1217 else: 

1218 crlf = b'\r\n' 

1219 colon = b':' 

1220 try: 

1221 header = start_line.rstrip(crlf) 

1222 method, resource, http_version = header.split(None, 2) 

1223 method = native_(method, 'utf-8') 

1224 resource = native_(resource, 'utf-8') 

1225 http_version = native_(http_version, 'utf-8') 

1226 except ValueError: 

1227 raise ValueError('Bad HTTP request line: %r' % start_line) 

1228 r = cls(environ_from_url(resource), 

1229 http_version=http_version, 

1230 method=method.upper() 

1231 ) 

1232 del r.environ['HTTP_HOST'] 

1233 while 1: 

1234 line = fp.readline() 

1235 if not line.strip(): 

1236 # end of headers 

1237 break 

1238 hname, hval = line.split(colon, 1) 

1239 hname = native_(hname, 'utf-8') 

1240 hval = native_(hval, 'utf-8').strip() 

1241 if hname in r.headers: 

1242 hval = r.headers[hname] + ', ' + hval 

1243 r.headers[hname] = hval 

1244 

1245 clen = r.content_length 

1246 if clen is None: 

1247 body = fp.read() 

1248 else: 

1249 body = fp.read(clen) 

1250 if is_text: 

1251 body = bytes_(body, 'utf-8') 

1252 r.body = body 

1253 

1254 return r 

1255 

1256 def call_application(self, application, catch_exc_info=False): 

1257 """ 

1258 Call the given WSGI application, returning ``(status_string, 

1259 headerlist, app_iter)`` 

1260 

1261 Be sure to call ``app_iter.close()`` if it's there. 

1262 

1263 If catch_exc_info is true, then returns ``(status_string, 

1264 headerlist, app_iter, exc_info)``, where the fourth item may 

1265 be None, but won't be if there was an exception. If you don't 

1266 do this and there was an exception, the exception will be 

1267 raised directly. 

1268 """ 

1269 if self.is_body_seekable: 

1270 self.body_file_raw.seek(0) 

1271 captured = [] 

1272 output = [] 

1273 def start_response(status, headers, exc_info=None): 

1274 if exc_info is not None and not catch_exc_info: 

1275 reraise(exc_info) 

1276 captured[:] = [status, headers, exc_info] 

1277 return output.append 

1278 app_iter = application(self.environ, start_response) 

1279 if output or not captured: 

1280 try: 

1281 output.extend(app_iter) 

1282 finally: 

1283 if hasattr(app_iter, 'close'): 

1284 app_iter.close() 

1285 app_iter = output 

1286 if catch_exc_info: 

1287 return (captured[0], captured[1], app_iter, captured[2]) 

1288 else: 

1289 return (captured[0], captured[1], app_iter) 

1290 

1291 # Will be filled in later: 

1292 ResponseClass = None 

1293 

1294 def send(self, application=None, catch_exc_info=False): 

1295 """ 

1296 Like ``.call_application(application)``, except returns a 

1297 response object with ``.status``, ``.headers``, and ``.body`` 

1298 attributes. 

1299 

1300 This will use ``self.ResponseClass`` to figure out the class 

1301 of the response object to return. 

1302 

1303 If ``application`` is not given, this will send the request to 

1304 ``self.make_default_send_app()`` 

1305 """ 

1306 if application is None: 

1307 application = self.make_default_send_app() 

1308 if catch_exc_info: 

1309 status, headers, app_iter, exc_info = self.call_application( 

1310 application, catch_exc_info=True) 

1311 del exc_info 

1312 else: 

1313 status, headers, app_iter = self.call_application( 

1314 application, catch_exc_info=False) 

1315 return self.ResponseClass( 

1316 status=status, headerlist=list(headers), app_iter=app_iter) 

1317 

1318 get_response = send 

1319 

1320 def make_default_send_app(self): 

1321 global _client 

1322 try: 

1323 client = _client 

1324 except NameError: 

1325 from webob import client 

1326 _client = client 

1327 return client.send_request_app 

1328 

1329 @classmethod 

1330 def blank(cls, path, environ=None, base_url=None, 

1331 headers=None, POST=None, **kw): 

1332 """ 

1333 Create a blank request environ (and Request wrapper) with the 

1334 given path (path should be urlencoded), and any keys from 

1335 environ. 

1336 

1337 The path will become path_info, with any query string split 

1338 off and used. 

1339 

1340 All necessary keys will be added to the environ, but the 

1341 values you pass in will take precedence. If you pass in 

1342 base_url then wsgi.url_scheme, HTTP_HOST, and SCRIPT_NAME will 

1343 be filled in from that value. 

1344 

1345 Any extra keyword will be passed to ``__init__``. 

1346 """ 

1347 env = environ_from_url(path) 

1348 if base_url: 

1349 scheme, netloc, path, query, fragment = urlparse.urlsplit(base_url) 

1350 if query or fragment: 

1351 raise ValueError( 

1352 "base_url (%r) cannot have a query or fragment" 

1353 % base_url) 

1354 if scheme: 

1355 env['wsgi.url_scheme'] = scheme 

1356 if netloc: 

1357 if ':' not in netloc: 

1358 if scheme == 'http': 

1359 netloc += ':80' 

1360 elif scheme == 'https': 

1361 netloc += ':443' 

1362 else: 

1363 raise ValueError( 

1364 "Unknown scheme: %r" % scheme) 

1365 host, port = netloc.split(':', 1) 

1366 env['SERVER_PORT'] = port 

1367 env['SERVER_NAME'] = host 

1368 env['HTTP_HOST'] = netloc 

1369 if path: 

1370 env['SCRIPT_NAME'] = url_unquote(path) 

1371 if environ: 

1372 env.update(environ) 

1373 content_type = kw.get('content_type', env.get('CONTENT_TYPE')) 

1374 if headers and 'Content-Type' in headers: 

1375 content_type = headers['Content-Type'] 

1376 if content_type is not None: 

1377 kw['content_type'] = content_type 

1378 environ_add_POST(env, POST, content_type=content_type) 

1379 obj = cls(env, **kw) 

1380 if headers is not None: 

1381 obj.headers.update(headers) 

1382 return obj 

1383 

1384class LegacyRequest(BaseRequest): 

1385 uscript_name = upath_property('SCRIPT_NAME') 

1386 upath_info = upath_property('PATH_INFO') 

1387 

1388 def encget(self, key, default=NoDefault, encattr=None): 

1389 val = self.environ.get(key, default) 

1390 if val is NoDefault: 

1391 raise KeyError(key) 

1392 if val is default: 

1393 return default 

1394 return val 

1395 

1396class AdhocAttrMixin(object): 

1397 _setattr_stacklevel = 3 

1398 

1399 def __setattr__(self, attr, value, DEFAULT=object()): 

1400 if (getattr(self.__class__, attr, DEFAULT) is not DEFAULT or 

1401 attr.startswith('_')): 

1402 object.__setattr__(self, attr, value) 

1403 else: 

1404 self.environ.setdefault('webob.adhoc_attrs', {})[attr] = value 

1405 

1406 def __getattr__(self, attr, DEFAULT=object()): 

1407 try: 

1408 return self.environ['webob.adhoc_attrs'][attr] 

1409 except KeyError: 

1410 raise AttributeError(attr) 

1411 

1412 def __delattr__(self, attr, DEFAULT=object()): 

1413 if getattr(self.__class__, attr, DEFAULT) is not DEFAULT: 

1414 return object.__delattr__(self, attr) 

1415 try: 

1416 del self.environ['webob.adhoc_attrs'][attr] 

1417 except KeyError: 

1418 raise AttributeError(attr) 

1419 

1420class Request(AdhocAttrMixin, BaseRequest): 

1421 """ The default request implementation """ 

1422 

1423def environ_from_url(path): 

1424 if SCHEME_RE.search(path): 

1425 scheme, netloc, path, qs, fragment = urlparse.urlsplit(path) 

1426 if fragment: 

1427 raise TypeError("Path cannot contain a fragment (%r)" % fragment) 

1428 if qs: 

1429 path += '?' + qs 

1430 if ':' not in netloc: 

1431 if scheme == 'http': 

1432 netloc += ':80' 

1433 elif scheme == 'https': 

1434 netloc += ':443' 

1435 else: 

1436 raise TypeError("Unknown scheme: %r" % scheme) 

1437 else: 

1438 scheme = 'http' 

1439 netloc = 'localhost:80' 

1440 if path and '?' in path: 

1441 path_info, query_string = path.split('?', 1) 

1442 path_info = url_unquote(path_info) 

1443 else: 

1444 path_info = url_unquote(path) 

1445 query_string = '' 

1446 env = { 

1447 'REQUEST_METHOD': 'GET', 

1448 'SCRIPT_NAME': '', 

1449 'PATH_INFO': path_info or '', 

1450 'QUERY_STRING': query_string, 

1451 'SERVER_NAME': netloc.split(':')[0], 

1452 'SERVER_PORT': netloc.split(':')[1], 

1453 'HTTP_HOST': netloc, 

1454 'SERVER_PROTOCOL': 'HTTP/1.0', 

1455 'wsgi.version': (1, 0), 

1456 'wsgi.url_scheme': scheme, 

1457 'wsgi.input': io.BytesIO(), 

1458 'wsgi.errors': sys.stderr, 

1459 'wsgi.multithread': False, 

1460 'wsgi.multiprocess': False, 

1461 'wsgi.run_once': False, 

1462 #'webob.is_body_seekable': True, 

1463 } 

1464 return env 

1465 

1466 

1467def environ_add_POST(env, data, content_type=None): 

1468 if data is None: 

1469 return 

1470 elif isinstance(data, text_type): 

1471 data = data.encode('ascii') 

1472 if env['REQUEST_METHOD'] not in ('POST', 'PUT'): 

1473 env['REQUEST_METHOD'] = 'POST' 

1474 has_files = False 

1475 if hasattr(data, 'items'): 

1476 data = list(data.items()) 

1477 for k, v in data: 

1478 if isinstance(v, (tuple, list)): 

1479 has_files = True 

1480 break 

1481 if content_type is None: 

1482 if has_files: 

1483 content_type = 'multipart/form-data' 

1484 else: 

1485 content_type = 'application/x-www-form-urlencoded' 

1486 if content_type.startswith('multipart/form-data'): 

1487 if not isinstance(data, bytes): 

1488 content_type, data = _encode_multipart(data, content_type) 

1489 elif content_type.startswith('application/x-www-form-urlencoded'): 

1490 if has_files: 

1491 raise ValueError('Submiting files is not allowed for' 

1492 ' content type `%s`' % content_type) 

1493 if not isinstance(data, bytes): 

1494 data = url_encode(data) 

1495 else: 

1496 if not isinstance(data, bytes): 

1497 raise ValueError('Please provide `POST` data as bytes' 

1498 ' for content type `%s`' % content_type) 

1499 data = bytes_(data, 'utf8') 

1500 env['wsgi.input'] = io.BytesIO(data) 

1501 env['webob.is_body_seekable'] = True 

1502 env['CONTENT_LENGTH'] = str(len(data)) 

1503 env['CONTENT_TYPE'] = content_type 

1504 

1505 

1506# 

1507# Helper classes and monkeypatching 

1508# 

1509 

1510class DisconnectionError(IOError): 

1511 pass 

1512 

1513 

1514class LimitedLengthFile(io.RawIOBase): 

1515 def __init__(self, file, maxlen): 

1516 self.file = file 

1517 self.maxlen = maxlen 

1518 self.remaining = maxlen 

1519 

1520 def __repr__(self): 

1521 return '<%s(%r, maxlen=%s)>' % ( 

1522 self.__class__.__name__, 

1523 self.file, 

1524 self.maxlen 

1525 ) 

1526 

1527 def fileno(self): 

1528 return self.file.fileno() 

1529 

1530 @staticmethod 

1531 def readable(): 

1532 return True 

1533 

1534 def readinto(self, buff): 

1535 if not self.remaining: 

1536 return 0 

1537 sz0 = min(len(buff), self.remaining) 

1538 data = self.file.read(sz0) 

1539 sz = len(data) 

1540 self.remaining -= sz 

1541 if sz < sz0 and self.remaining: 

1542 raise DisconnectionError( 

1543 "The client disconnected while sending the body " 

1544 "(%d more bytes were expected)" % (self.remaining,) 

1545 ) 

1546 buff[:sz] = data 

1547 return sz 

1548 

1549 

1550def _cgi_FieldStorage__repr__patch(self): 

1551 """ monkey patch for FieldStorage.__repr__ 

1552 

1553 Unbelievably, the default __repr__ on FieldStorage reads 

1554 the entire file content instead of being sane about it. 

1555 This is a simple replacement that doesn't do that 

1556 """ 

1557 if self.file: 

1558 return "FieldStorage(%r, %r)" % (self.name, self.filename) 

1559 return "FieldStorage(%r, %r, %r)" % (self.name, self.filename, self.value) 

1560 

1561cgi_FieldStorage.__repr__ = _cgi_FieldStorage__repr__patch 

1562 

1563 

1564class FakeCGIBody(io.RawIOBase): 

1565 def __init__(self, vars, content_type): 

1566 warnings.warn( 

1567 "FakeCGIBody is no longer used by WebOb and will be removed from a future " 

1568 "version of WebOb. If you require FakeCGIBody please make a copy into " 

1569 "you own project", 

1570 DeprecationWarning 

1571 ) 

1572 

1573 if content_type.startswith('multipart/form-data'): 

1574 if not _get_multipart_boundary(content_type): 

1575 raise ValueError('Content-type: %r does not contain boundary' 

1576 % content_type) 

1577 self.vars = vars 

1578 self.content_type = content_type 

1579 self.file = None 

1580 

1581 def __repr__(self): 

1582 inner = repr(self.vars) 

1583 if len(inner) > 20: 

1584 inner = inner[:15] + '...' + inner[-5:] 

1585 return '<%s at 0x%x viewing %s>' % ( 

1586 self.__class__.__name__, 

1587 abs(id(self)), inner) 

1588 

1589 def fileno(self): 

1590 return None 

1591 

1592 @staticmethod 

1593 def readable(): 

1594 return True 

1595 

1596 def readinto(self, buff): 

1597 if self.file is None: 

1598 if self.content_type.startswith('application/x-www-form-urlencoded'): 

1599 data = '&'.join( 

1600 '%s=%s' % ( 

1601 quote_plus(bytes_(k, 'utf8')), 

1602 quote_plus(bytes_(v, 'utf8')) 

1603 ) 

1604 for k, v in self.vars.items() 

1605 ) 

1606 self.file = io.BytesIO(bytes_(data)) 

1607 elif self.content_type.startswith('multipart/form-data'): 

1608 self.file = _encode_multipart( 

1609 self.vars.items(), 

1610 self.content_type, 

1611 fout=io.BytesIO() 

1612 )[1] 

1613 self.file.seek(0) 

1614 else: 

1615 assert 0, ('Bad content type: %r' % self.content_type) 

1616 return self.file.readinto(buff) 

1617 

1618def _get_multipart_boundary(ctype): 

1619 m = re.search(r'boundary=([^ ]+)', ctype, re.I) 

1620 if m: 

1621 return native_(m.group(1).strip('"')) 

1622 

1623def _encode_multipart(vars, content_type, fout=None): 

1624 """Encode a multipart request body into a string""" 

1625 f = fout or io.BytesIO() 

1626 w = f.write 

1627 def wt(t): 

1628 w(t.encode('utf8')) 

1629 

1630 CRLF = b'\r\n' 

1631 boundary = _get_multipart_boundary(content_type) 

1632 if not boundary: 

1633 boundary = native_(binascii.hexlify(os.urandom(10))) 

1634 content_type += ('; boundary=%s' % boundary) 

1635 for name, value in vars: 

1636 w(b'--') 

1637 wt(boundary) 

1638 w(CRLF) 

1639 wt('Content-Disposition: form-data') 

1640 if name is not None: 

1641 wt('; name="%s"' % name) 

1642 filename = None 

1643 if getattr(value, 'filename', None): 

1644 filename = value.filename 

1645 elif isinstance(value, (list, tuple)): 

1646 filename, value = value 

1647 if hasattr(value, 'read'): 

1648 value = value.read() 

1649 

1650 if filename is not None: 

1651 wt('; filename="%s"' % filename) 

1652 mime_type = mimetypes.guess_type(filename)[0] 

1653 else: 

1654 mime_type = None 

1655 

1656 w(CRLF) 

1657 

1658 # TODO: should handle value.disposition_options 

1659 if getattr(value, 'type', None): 

1660 wt('Content-type: %s' % value.type) 

1661 if value.type_options: 

1662 for ct_name, ct_value in sorted(value.type_options.items()): 

1663 wt('; %s="%s"' % (ct_name, ct_value)) 

1664 w(CRLF) 

1665 elif mime_type: 

1666 wt('Content-type: %s' % mime_type) 

1667 w(CRLF) 

1668 w(CRLF) 

1669 if hasattr(value, 'value'): 

1670 value = value.value 

1671 if isinstance(value, bytes): 

1672 w(value) 

1673 else: 

1674 wt(value) 

1675 w(CRLF) 

1676 wt('--%s--' % boundary) 

1677 if fout: 

1678 return content_type, fout 

1679 else: 

1680 return content_type, f.getvalue() 

1681 

1682def detect_charset(ctype): 

1683 m = CHARSET_RE.search(ctype) 

1684 if m: 

1685 return m.group(1).strip('"').strip() 

1686 

1687def _is_utf8(charset): 

1688 if not charset: 

1689 return True 

1690 else: 

1691 return charset.lower().replace('-', '') == 'utf8' 

1692 

1693 

1694class Transcoder(object): 

1695 def __init__(self, charset, errors='strict'): 

1696 self.charset = charset # source charset 

1697 self.errors = errors # unicode errors 

1698 self._trans = lambda b: b.decode(charset, errors).encode('utf8') 

1699 

1700 def transcode_query(self, q): 

1701 q_orig = q 

1702 if '=' not in q: 

1703 # this doesn't look like a form submission 

1704 return q_orig 

1705 

1706 if PY2: 

1707 q = urlparse.parse_qsl(q, self.charset) 

1708 t = self._trans 

1709 q = [(t(k), t(v)) for k, v in q] 

1710 else: 

1711 q = list(parse_qsl_text(q, self.charset)) 

1712 

1713 return url_encode(q) 

1714 

1715 def transcode_fs(self, fs, content_type): 

1716 # transcode FieldStorage 

1717 if PY2: 

1718 def decode(b): 

1719 if b is not None: 

1720 return b.decode(self.charset, self.errors) 

1721 else: 

1722 return b 

1723 else: 

1724 def decode(b): 

1725 return b 

1726 

1727 data = [] 

1728 for field in fs.list or (): 

1729 field.name = decode(field.name) 

1730 if field.filename: 

1731 field.filename = decode(field.filename) 

1732 data.append((field.name, field)) 

1733 else: 

1734 data.append((field.name, decode(field.value))) 

1735 

1736 # TODO: transcode big requests to temp file 

1737 content_type, fout = _encode_multipart( 

1738 data, 

1739 content_type, 

1740 fout=io.BytesIO() 

1741 ) 

1742 return fout