Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/formparser.py: 68%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

179 statements  

1from __future__ import annotations 

2 

3import collections.abc as cabc 

4import typing as t 

5import wsgiref.types as wt 

6from tempfile import SpooledTemporaryFile 

7from types import TracebackType 

8from urllib.parse import parse_qsl 

9 

10from ._internal import _plain_int 

11from .datastructures.file_storage import FileStorage 

12from .datastructures.headers import Headers 

13from .datastructures.structures import ImmutableMultiDict 

14from .datastructures.structures import MultiDict 

15from .exceptions import RequestEntityTooLarge 

16from .http import parse_options_header 

17from .sansio.multipart import Data 

18from .sansio.multipart import Epilogue 

19from .sansio.multipart import Field 

20from .sansio.multipart import File 

21from .sansio.multipart import MultipartDecoder 

22from .sansio.multipart import NeedData 

23 

24F = t.TypeVar("F", bound=t.Callable[..., t.Any]) 

25t_parse_result = tuple[t.IO[bytes], MultiDict[str, str], MultiDict[str, FileStorage]] 

26 

27 

28class TStreamFactory(t.Protocol): 

29 def __call__( 

30 self, 

31 total_content_length: int | None, 

32 content_type: str | None, 

33 filename: str | None, 

34 content_length: int | None = None, 

35 ) -> t.IO[bytes]: ... 

36 

37 

38def _make_stream_factory(max_size: int | None) -> TStreamFactory: 

39 if max_size is None: 

40 # Could return BytesIO for this, keeping 500 kB default for now. 

41 max_size = 1024 * 500 

42 

43 def stream_factory( 

44 total_content_length: int | None, 

45 content_type: str | None, 

46 filename: str | None, 

47 content_length: int | None = None, 

48 ) -> t.IO[bytes]: 

49 return SpooledTemporaryFile(max_size=max_size, mode="rb+") 

50 

51 return stream_factory 

52 

53 

54_default_stream_factory = _make_stream_factory(1024 * 500) 

55 

56 

57def _parse_form_data( 

58 environ: wt.WSGIEnvironment, 

59 stream_factory: TStreamFactory | None = None, 

60 max_form_memory_size: int | None = None, 

61 max_content_length: int | None = None, 

62 silent: bool = True, 

63 *, 

64 max_form_parts: int | None = None, 

65 **kwargs: t.Any, 

66) -> t_parse_result: 

67 """Parse the form data from the request body and return it as a 

68 tuple ``(stream, form, files)``. 

69 

70 If the mimetype of the data transmitted is `multipart/form-data` the 

71 files multidict will be filled with `FileStorage` objects. If the 

72 mimetype is unknown the input stream is wrapped and returned as first 

73 argument, else the stream is empty. 

74 

75 This is a shortcut for the common usage of :class:`FormDataParser`. 

76 

77 :param environ: the WSGI environment to be used for parsing. 

78 :param stream_factory: A callable that returns a file in ``rb+`` mode. 

79 Defaults to creating a :class:`tempfile.SpooledTemporaryFile` with 

80 ``max_form_memory_size``. 

81 :param max_content_length: If the data is larger than this many bytes, raise 

82 :exc:`.RequestEntityTooLarge`. This is used by :meth:`parse_from_environ` 

83 to set up a limited stream. When using :meth:`parse`, you must get a 

84 limited stream with :func:`.get_input_stream`. 

85 :param max_form_memory_size: If a ``multipart/form-data`` text part is larger 

86 than this many bytes, raise :exc:`~exceptions.RequestEntityTooLarge`. 

87 File parts are written to disk after this size. This is an additional 

88 check, it does not replace using a limited stream and 

89 ``max_content_length``. 

90 :param max_form_parts: If more than this number of ``multipart/form-data`` 

91 parts are received, raise :exc:`.RequestEntityTooLarge`. This is an 

92 additional check, it does not replace using a limited stream and 

93 ``max_content_length``. 

94 :param cls: an optional dict class to use. If this is not specified 

95 or `None` the default :class:`MultiDict` is used. 

96 :param silent: If set to False parsing errors will not be caught. 

97 :return: A tuple in the form ``(stream, form, files)``. 

98 

99 .. deprecated:: 3.2 

100 Will be removed in Werkzeug 4.0. Use ``Request.form`` and ``files`` 

101 instead. 

102 

103 .. versionchanged:: 3.2 

104 The ``cls`` parameter is deprecated and will be removed in Werkzeug 4.0. It will 

105 always be ``ImmutableMultiDict``. 

106 

107 .. versionchanged:: 3.1.9 

108 ``max_form_memory_size`` is not applied to 

109 ``application/x-www-form-urlencoded``. 

110 

111 .. versionchanged:: 3.0 

112 The ``charset`` and ``errors`` parameters were removed. 

113 

114 .. versionchanged:: 2.3 

115 Added the ``max_form_parts`` parameter. 

116 

117 .. versionadded:: 0.5.1 

118 Added the ``silent`` parameter. 

119 

120 .. versionadded:: 0.5 

121 Added the ``max_form_memory_size``, ``max_content_length``, and ``cls`` 

122 parameters. 

123 """ 

124 parser_kwargs: dict[str, t.Any] = dict( 

125 stream_factory=stream_factory, 

126 max_form_memory_size=max_form_memory_size, 

127 max_content_length=max_content_length, 

128 max_form_parts=max_form_parts, 

129 silent=silent, 

130 ) 

131 

132 if "cls" in kwargs: 

133 import warnings 

134 

135 warnings.warn( 

136 "The 'cls' parameter is deprecated and will be removed in Werkzeug 4.0." 

137 " It will always be 'ImmutableMultiDict'.", 

138 DeprecationWarning, 

139 stacklevel=2, 

140 ) 

141 parser_kwargs["cls"] = kwargs["cls"] 

142 

143 return _FormDataParser(**parser_kwargs).parse_from_environ(environ) 

144 

145 

146class _FormDataParser: 

147 """This class implements parsing of form data for Werkzeug. By itself 

148 it can parse multipart and url encoded form data. It can be subclassed 

149 and extended but for most mimetypes it is a better idea to use the 

150 untouched stream and expose it as separate attributes on a request 

151 object. 

152 

153 :param max_content_length: If the data is larger than this many bytes, raise 

154 :exc:`.RequestEntityTooLarge`. This is used by :meth:`parse_from_environ` 

155 to set up a limited stream. When using :meth:`parse`, you must get a 

156 limited stream with :func:`.get_input_stream`. 

157 :param max_form_memory_size: If a ``multipart/form-data`` text part is larger 

158 than this many bytes, raise :exc:`~exceptions.RequestEntityTooLarge`. 

159 File parts are written to disk after this size. This is an additional 

160 check, it does not replace using a limited stream and 

161 ``max_content_length``. 

162 :param max_form_parts: If more than this number of ``multipart/form-data`` 

163 parts are received, raise :exc:`.RequestEntityTooLarge`. This is an 

164 additional check, it does not replace using a limited stream and 

165 ``max_content_length``. 

166 :param cls: an optional dict class to use. If this is not specified 

167 or `None` the default :class:`MultiDict` is used. 

168 :param silent: If set to False parsing errors will not be caught. 

169 

170 .. deprecated:: 3.2 

171 Will be removed in Werkzeug 4.0. Use ``Request.form`` and ``files`` 

172 instead. 

173 

174 .. versionchanged:: 3.2 

175 The ``cls`` parameter and attribute are deprecated and will be removed 

176 in Werkzeug 4.0. They will always be ``ImmutableMultiDict``. 

177 

178 .. versionchanged:: 3.1.9 

179 ``max_form_memory_size`` is not applied to 

180 ``application/x-www-form-urlencoded``. 

181 

182 .. versionchanged:: 3.0 

183 The ``charset`` and ``errors`` parameters were removed. 

184 

185 .. versionchanged:: 3.0 

186 The ``parse_functions`` attribute and ``get_parse_func`` methods were removed. 

187 

188 .. versionchanged:: 2.2.3 

189 Added the ``max_form_parts`` parameter. 

190 

191 .. versionadded:: 0.8 

192 """ 

193 

194 def __init__( 

195 self, 

196 stream_factory: TStreamFactory | None = None, 

197 max_form_memory_size: int | None = None, 

198 max_content_length: int | None = None, 

199 silent: bool = True, 

200 *, 

201 max_form_parts: int | None = None, 

202 **kwargs: t.Any, 

203 ) -> None: 

204 self.stream_factory = stream_factory 

205 self.max_content_length = max_content_length 

206 self.max_form_memory_size = max_form_memory_size 

207 self.max_form_parts = max_form_parts 

208 

209 if "cls" in kwargs: 

210 import warnings 

211 

212 warnings.warn( 

213 "The 'cls' parameter is deprecated and will be removed in Werkzeug 4.0." 

214 " It will always be 'ImmutableMultiDict'.", 

215 DeprecationWarning, 

216 stacklevel=2, 

217 ) 

218 

219 self.cls: type[ImmutableMultiDict[str, t.Any]] | None = kwargs.get("cls") 

220 self.silent = silent 

221 

222 def parse_from_environ(self, environ: wt.WSGIEnvironment) -> t_parse_result: 

223 """Parses the information from the environment as form data. 

224 

225 :param environ: the WSGI environment to be used for parsing. 

226 :return: A tuple in the form ``(stream, form, files)``. 

227 """ 

228 from .wrappers.request import Request 

229 

230 request = Request(environ) 

231 request.max_content_length = self.max_content_length 

232 return self.parse( 

233 request.stream, 

234 content_length=request.content_length, 

235 mimetype=request.mimetype, 

236 options=request.mimetype_params, 

237 ) 

238 

239 def parse( 

240 self, 

241 stream: t.IO[bytes], 

242 mimetype: str, 

243 content_length: int | None, 

244 options: cabc.Mapping[str, str] | None = None, 

245 ) -> t_parse_result: 

246 """Parses the information from the given stream, mimetype, 

247 content length and mimetype parameters. 

248 

249 :param stream: A limited input stream, from :meth:`.get_input_stream`. 

250 :param mimetype: The mimetype used to choose how to parse the form. 

251 ``multipart/form-data`` and ``application/x-www-form-urlencoded`` 

252 are supported. 

253 :param content_length: the content length of the incoming data 

254 :param options: optional mimetype parameters (used for 

255 the multipart boundary for instance) 

256 :return: A tuple in the form ``(stream, form, files)``. 

257 

258 .. versionchanged:: 3.0 

259 The invalid ``application/x-url-encoded`` content type is not 

260 treated as ``application/x-www-form-urlencoded``. 

261 """ 

262 if mimetype == "multipart/form-data": 

263 parse_func = self._parse_multipart 

264 elif mimetype == "application/x-www-form-urlencoded": 

265 parse_func = self._parse_urlencoded 

266 else: 

267 if self.cls is not None: 

268 return stream, self.cls(), self.cls() 

269 

270 return stream, ImmutableMultiDict(), ImmutableMultiDict() 

271 

272 if options is None: 

273 options = {} 

274 

275 try: 

276 return parse_func(stream, mimetype, content_length, options) 

277 except ValueError: 

278 if not self.silent: 

279 raise 

280 

281 if self.cls is not None: 

282 return stream, self.cls(), self.cls() 

283 

284 return stream, ImmutableMultiDict(), ImmutableMultiDict() 

285 

286 def _parse_multipart( 

287 self, 

288 stream: t.IO[bytes], 

289 mimetype: str, 

290 content_length: int | None, 

291 options: cabc.Mapping[str, str], 

292 ) -> t_parse_result: 

293 boundary = options.get("boundary", "").encode("ascii") 

294 

295 if not boundary: 

296 raise ValueError("Missing boundary") 

297 

298 kwargs: dict[str, t.Any] = dict( 

299 stream_factory=self.stream_factory, 

300 max_form_memory_size=self.max_form_memory_size, 

301 max_form_parts=self.max_form_parts, 

302 ) 

303 

304 if self.cls is not None: 

305 kwargs["cls"] = self.cls 

306 

307 with MultiPartParser(**kwargs) as parser: 

308 form, files = parser.parse(stream, boundary, content_length) 

309 

310 return stream, form, files 

311 

312 def _parse_urlencoded( 

313 self, 

314 stream: t.IO[bytes], 

315 mimetype: str, 

316 content_length: int | None, 

317 options: cabc.Mapping[str, str], 

318 ) -> t_parse_result: 

319 # The stream must already be limited to max_content_length. 

320 # max_form_memory_size can't apply, since the entire stream is read at 

321 # once instead of incrementally parsed. 

322 # max_form_parts could be applied here, but doesn't impose a useful 

323 # limit given the previous points and because the parser is much simpler 

324 # and faster already. 

325 items = parse_qsl( 

326 stream.read().decode(), 

327 keep_blank_values=True, 

328 errors="werkzeug.url_quote", 

329 ) 

330 

331 if self.cls is not None: 

332 return stream, self.cls(items), self.cls() 

333 

334 return stream, ImmutableMultiDict(items), ImmutableMultiDict() 

335 

336 

337class MultiPartParser: 

338 def __init__( 

339 self, 

340 stream_factory: TStreamFactory | None = None, 

341 max_form_memory_size: int | None = None, 

342 buffer_size: int = 64 * 1024, 

343 max_form_parts: int | None = None, 

344 **kwargs: t.Any, 

345 ) -> None: 

346 self.max_form_memory_size = max_form_memory_size 

347 self.max_form_parts = max_form_parts 

348 

349 if stream_factory is None: 

350 stream_factory = _make_stream_factory(max_form_memory_size) 

351 

352 self.stream_factory = stream_factory 

353 self._files: list[t.IO[bytes]] = [] 

354 

355 if "cls" in kwargs: 

356 import warnings 

357 

358 warnings.warn( 

359 "The 'cls' parameter is deprecated and will be removed in Werkzeug 4.0." 

360 " It will always be 'ImmutableMultiDict'.", 

361 DeprecationWarning, 

362 stacklevel=2, 

363 ) 

364 

365 self.cls: type[ImmutableMultiDict[str, t.Any]] | None = kwargs.get("cls") 

366 self.buffer_size = buffer_size 

367 

368 def __enter__(self) -> t.Self: 

369 return self 

370 

371 def __exit__( 

372 self, 

373 exc_type: type[BaseException] | None, 

374 exc_val: BaseException | None, 

375 exc_tb: TracebackType | None, 

376 ) -> None: 

377 if exc_val is not None: 

378 for file in self._files: 

379 file.close() 

380 

381 def get_part_charset(self, headers: Headers) -> str: 

382 # Figure out input charset for current part 

383 content_type = headers.get("Content-Type") 

384 

385 if content_type: 

386 parameters = parse_options_header(content_type)[1] 

387 ct_charset = parameters.get("charset", "").lower() 

388 

389 # A safe list of encodings. Modern clients should only send ASCII or UTF-8. 

390 # This list will not be extended further. 

391 if ct_charset in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}: 

392 return ct_charset 

393 

394 return "utf-8" 

395 

396 def start_file_streaming( 

397 self, event: File, total_content_length: int | None 

398 ) -> t.IO[bytes]: 

399 content_type = event.headers.get("Content-Type") 

400 

401 try: 

402 content_length = _plain_int(event.headers["Content-Length"]) 

403 except (KeyError, ValueError): 

404 content_length = 0 

405 

406 container = self.stream_factory( 

407 total_content_length=total_content_length, 

408 filename=event.filename, 

409 content_type=content_type, 

410 content_length=content_length, 

411 ) 

412 self._files.append(container) 

413 return container 

414 

415 def parse( 

416 self, stream: t.IO[bytes], boundary: bytes, content_length: int | None 

417 ) -> tuple[MultiDict[str, str], MultiDict[str, FileStorage]]: 

418 current_part: Field | File 

419 field_size: int | None = None 

420 container: t.IO[bytes] | list[bytes] 

421 _write: t.Callable[[bytes], t.Any] 

422 

423 parser = MultipartDecoder( 

424 boundary, 

425 max_form_memory_size=self.max_form_memory_size, 

426 max_parts=self.max_form_parts, 

427 ) 

428 

429 fields = [] 

430 files = [] 

431 

432 for data in _chunk_iter(stream.read, self.buffer_size): 

433 parser.receive_data(data) 

434 event = parser.next_event() 

435 while not isinstance(event, (Epilogue, NeedData)): 

436 if isinstance(event, Field): 

437 current_part = event 

438 field_size = 0 

439 container = [] 

440 _write = container.append 

441 elif isinstance(event, File): 

442 current_part = event 

443 field_size = None 

444 container = self.start_file_streaming(event, content_length) 

445 _write = container.write 

446 elif isinstance(event, Data): 

447 if self.max_form_memory_size is not None and field_size is not None: 

448 # Ensure that accumulated data events do not exceed limit. 

449 # Also checked within single event in MultipartDecoder. 

450 field_size += len(event.data) 

451 

452 if field_size > self.max_form_memory_size: 

453 raise RequestEntityTooLarge() 

454 

455 _write(event.data) 

456 if not event.more_data: 

457 if isinstance(current_part, Field): 

458 value = b"".join(container).decode( 

459 self.get_part_charset(current_part.headers), "replace" 

460 ) 

461 fields.append((current_part.name, value)) 

462 else: 

463 container = t.cast(t.IO[bytes], container) 

464 container.seek(0) 

465 files.append( 

466 ( 

467 current_part.name, 

468 FileStorage( 

469 container, 

470 current_part.filename, 

471 current_part.name, 

472 headers=current_part.headers, 

473 ), 

474 ) 

475 ) 

476 

477 event = parser.next_event() 

478 

479 if self.cls is not None: 

480 return self.cls(fields), self.cls(files) 

481 

482 return ImmutableMultiDict(fields), ImmutableMultiDict(files) 

483 

484 

485def _chunk_iter(read: t.Callable[[int], bytes], size: int) -> t.Iterator[bytes | None]: 

486 """Read data in chunks for multipart/form-data parsing. Stop if no data is read. 

487 Yield ``None`` at the end to signal end of parsing. 

488 """ 

489 while True: 

490 data = read(size) 

491 

492 if not data: 

493 break 

494 

495 yield data 

496 

497 yield None 

498 

499 

500if not t.TYPE_CHECKING: 

501 

502 def __getattr__(name: str) -> t.Any: 

503 import warnings 

504 

505 if name == "default_stream_factory": 

506 warnings.warn( 

507 "'default_stream_factory' is deprecated and will be removed in Werkzeug" 

508 " 4.0. If not passed, 'FormDataParser' will use 'SpooledTemporaryFile'" 

509 " with 'max_form_memory_size'.", 

510 DeprecationWarning, 

511 stacklevel=2, 

512 ) 

513 return _default_stream_factory 

514 

515 if name == "parse_form_data": 

516 warnings.warn( 

517 "'parse_form_data' is deprecated and will be removed in Werkzeug 4.0." 

518 " Use 'Request.form' and 'files' instead.", 

519 DeprecationWarning, 

520 stacklevel=2, 

521 ) 

522 return _parse_form_data 

523 

524 if name == "FormDataParser": 

525 warnings.warn( 

526 "The 'FormDataParser' class is deprecated and will be removed in" 

527 " Werkzeug 4.0. Use 'Request.form' and 'files' instead.", 

528 DeprecationWarning, 

529 stacklevel=2, 

530 ) 

531 return _FormDataParser 

532 

533 raise AttributeError(name)