Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/http.py: 52%

476 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-09 06:08 +0000

1from __future__ import annotations 

2 

3import email.utils 

4import re 

5import typing as t 

6import warnings 

7from datetime import date 

8from datetime import datetime 

9from datetime import time 

10from datetime import timedelta 

11from datetime import timezone 

12from enum import Enum 

13from hashlib import sha1 

14from time import mktime 

15from time import struct_time 

16from urllib.parse import quote 

17from urllib.parse import unquote 

18from urllib.request import parse_http_list as _parse_list_header 

19 

20from ._internal import _dt_as_utc 

21from ._internal import _plain_float 

22from ._internal import _plain_int 

23 

24if t.TYPE_CHECKING: 

25 from _typeshed.wsgi import WSGIEnvironment 

26 

27_token_chars = frozenset( 

28 "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~" 

29) 

30_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)') 

31_entity_headers = frozenset( 

32 [ 

33 "allow", 

34 "content-encoding", 

35 "content-language", 

36 "content-length", 

37 "content-location", 

38 "content-md5", 

39 "content-range", 

40 "content-type", 

41 "expires", 

42 "last-modified", 

43 ] 

44) 

45_hop_by_hop_headers = frozenset( 

46 [ 

47 "connection", 

48 "keep-alive", 

49 "proxy-authenticate", 

50 "proxy-authorization", 

51 "te", 

52 "trailer", 

53 "transfer-encoding", 

54 "upgrade", 

55 ] 

56) 

57HTTP_STATUS_CODES = { 

58 100: "Continue", 

59 101: "Switching Protocols", 

60 102: "Processing", 

61 103: "Early Hints", # see RFC 8297 

62 200: "OK", 

63 201: "Created", 

64 202: "Accepted", 

65 203: "Non Authoritative Information", 

66 204: "No Content", 

67 205: "Reset Content", 

68 206: "Partial Content", 

69 207: "Multi Status", 

70 208: "Already Reported", # see RFC 5842 

71 226: "IM Used", # see RFC 3229 

72 300: "Multiple Choices", 

73 301: "Moved Permanently", 

74 302: "Found", 

75 303: "See Other", 

76 304: "Not Modified", 

77 305: "Use Proxy", 

78 306: "Switch Proxy", # unused 

79 307: "Temporary Redirect", 

80 308: "Permanent Redirect", 

81 400: "Bad Request", 

82 401: "Unauthorized", 

83 402: "Payment Required", # unused 

84 403: "Forbidden", 

85 404: "Not Found", 

86 405: "Method Not Allowed", 

87 406: "Not Acceptable", 

88 407: "Proxy Authentication Required", 

89 408: "Request Timeout", 

90 409: "Conflict", 

91 410: "Gone", 

92 411: "Length Required", 

93 412: "Precondition Failed", 

94 413: "Request Entity Too Large", 

95 414: "Request URI Too Long", 

96 415: "Unsupported Media Type", 

97 416: "Requested Range Not Satisfiable", 

98 417: "Expectation Failed", 

99 418: "I'm a teapot", # see RFC 2324 

100 421: "Misdirected Request", # see RFC 7540 

101 422: "Unprocessable Entity", 

102 423: "Locked", 

103 424: "Failed Dependency", 

104 425: "Too Early", # see RFC 8470 

105 426: "Upgrade Required", 

106 428: "Precondition Required", # see RFC 6585 

107 429: "Too Many Requests", 

108 431: "Request Header Fields Too Large", 

109 449: "Retry With", # proprietary MS extension 

110 451: "Unavailable For Legal Reasons", 

111 500: "Internal Server Error", 

112 501: "Not Implemented", 

113 502: "Bad Gateway", 

114 503: "Service Unavailable", 

115 504: "Gateway Timeout", 

116 505: "HTTP Version Not Supported", 

117 506: "Variant Also Negotiates", # see RFC 2295 

118 507: "Insufficient Storage", 

119 508: "Loop Detected", # see RFC 5842 

120 510: "Not Extended", 

121 511: "Network Authentication Failed", 

122} 

123 

124 

125class COEP(Enum): 

126 """Cross Origin Embedder Policies""" 

127 

128 UNSAFE_NONE = "unsafe-none" 

129 REQUIRE_CORP = "require-corp" 

130 

131 

132class COOP(Enum): 

133 """Cross Origin Opener Policies""" 

134 

135 UNSAFE_NONE = "unsafe-none" 

136 SAME_ORIGIN_ALLOW_POPUPS = "same-origin-allow-popups" 

137 SAME_ORIGIN = "same-origin" 

138 

139 

140def quote_header_value( 

141 value: t.Any, 

142 extra_chars: str | None = None, 

143 allow_token: bool = True, 

144) -> str: 

145 """Add double quotes around a header value. If the header contains only ASCII token 

146 characters, it will be returned unchanged. If the header contains ``"`` or ``\\`` 

147 characters, they will be escaped with an additional ``\\`` character. 

148 

149 This is the reverse of :func:`unquote_header_value`. 

150 

151 :param value: The value to quote. Will be converted to a string. 

152 :param allow_token: Disable to quote the value even if it only has token characters. 

153 

154 .. versionchanged:: 2.3 

155 The value is quoted if it is the empty string. 

156 

157 .. versionchanged:: 2.3 

158 Passing bytes is deprecated and will not be supported in Werkzeug 3.0. 

159 

160 .. versionchanged:: 2.3 

161 The ``extra_chars`` parameter is deprecated and will be removed in Werkzeug 3.0. 

162 

163 .. versionadded:: 0.5 

164 """ 

165 if isinstance(value, bytes): 

166 warnings.warn( 

167 "Passing bytes is deprecated and will not be supported in Werkzeug 3.0.", 

168 DeprecationWarning, 

169 stacklevel=2, 

170 ) 

171 value = value.decode("latin1") 

172 

173 if extra_chars is not None: 

174 warnings.warn( 

175 "The 'extra_chars' parameter is deprecated and will be" 

176 " removed in Werkzeug 3.0.", 

177 DeprecationWarning, 

178 stacklevel=2, 

179 ) 

180 

181 value = str(value) 

182 

183 if not value: 

184 return '""' 

185 

186 if allow_token: 

187 token_chars = _token_chars 

188 

189 if extra_chars: 

190 token_chars |= set(extra_chars) 

191 

192 if token_chars.issuperset(value): 

193 return value 

194 

195 value = value.replace("\\", "\\\\").replace('"', '\\"') 

196 return f'"{value}"' 

197 

198 

199def unquote_header_value(value: str, is_filename: bool | None = None) -> str: 

200 """Remove double quotes and decode slash-escaped ``"`` and ``\\`` characters in a 

201 header value. 

202 

203 This is the reverse of :func:`quote_header_value`. 

204 

205 :param value: The header value to unquote. 

206 

207 .. versionchanged:: 2.3 

208 The ``is_filename`` parameter is deprecated and will be removed in Werkzeug 3.0. 

209 """ 

210 if is_filename is not None: 

211 warnings.warn( 

212 "The 'is_filename' parameter is deprecated and will be" 

213 " removed in Werkzeug 3.0.", 

214 DeprecationWarning, 

215 stacklevel=2, 

216 ) 

217 

218 if len(value) >= 2 and value[0] == value[-1] == '"': 

219 value = value[1:-1] 

220 

221 if not is_filename: 

222 return value.replace("\\\\", "\\").replace('\\"', '"') 

223 

224 return value 

225 

226 

227def dump_options_header(header: str | None, options: t.Mapping[str, t.Any]) -> str: 

228 """Produce a header value and ``key=value`` parameters separated by semicolons 

229 ``;``. For example, the ``Content-Type`` header. 

230 

231 .. code-block:: python 

232 

233 dump_options_header("text/html", {"charset": "UTF-8"}) 

234 'text/html; charset=UTF-8' 

235 

236 This is the reverse of :func:`parse_options_header`. 

237 

238 If a value contains non-token characters, it will be quoted. 

239 

240 If a value is ``None``, the parameter is skipped. 

241 

242 In some keys for some headers, a UTF-8 value can be encoded using a special 

243 ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will 

244 not produce that format automatically, but if a given key ends with an asterisk 

245 ``*``, the value is assumed to have that form and will not be quoted further. 

246 

247 :param header: The primary header value. 

248 :param options: Parameters to encode as ``key=value`` pairs. 

249 

250 .. versionchanged:: 2.3 

251 Keys with ``None`` values are skipped rather than treated as a bare key. 

252 

253 .. versionchanged:: 2.2.3 

254 If a key ends with ``*``, its value will not be quoted. 

255 """ 

256 segments = [] 

257 

258 if header is not None: 

259 segments.append(header) 

260 

261 for key, value in options.items(): 

262 if value is None: 

263 continue 

264 

265 if key[-1] == "*": 

266 segments.append(f"{key}={value}") 

267 else: 

268 segments.append(f"{key}={quote_header_value(value)}") 

269 

270 return "; ".join(segments) 

271 

272 

273def dump_header( 

274 iterable: dict[str, t.Any] | t.Iterable[t.Any], 

275 allow_token: bool | None = None, 

276) -> str: 

277 """Produce a header value from a list of items or ``key=value`` pairs, separated by 

278 commas ``,``. 

279 

280 This is the reverse of :func:`parse_list_header`, :func:`parse_dict_header`, and 

281 :func:`parse_set_header`. 

282 

283 If a value contains non-token characters, it will be quoted. 

284 

285 If a value is ``None``, the key is output alone. 

286 

287 In some keys for some headers, a UTF-8 value can be encoded using a special 

288 ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will 

289 not produce that format automatically, but if a given key ends with an asterisk 

290 ``*``, the value is assumed to have that form and will not be quoted further. 

291 

292 .. code-block:: python 

293 

294 dump_header(["foo", "bar baz"]) 

295 'foo, "bar baz"' 

296 

297 dump_header({"foo": "bar baz"}) 

298 'foo="bar baz"' 

299 

300 :param iterable: The items to create a header from. 

301 

302 .. versionchanged:: 2.3 

303 The ``allow_token`` parameter is deprecated and will be removed in Werkzeug 3.0. 

304 

305 .. versionchanged:: 2.2.3 

306 If a key ends with ``*``, its value will not be quoted. 

307 """ 

308 if allow_token is not None: 

309 warnings.warn( 

310 "'The 'allow_token' parameter is deprecated and will be" 

311 " removed in Werkzeug 3.0.", 

312 DeprecationWarning, 

313 stacklevel=2, 

314 ) 

315 else: 

316 allow_token = True 

317 

318 if isinstance(iterable, dict): 

319 items = [] 

320 

321 for key, value in iterable.items(): 

322 if value is None: 

323 items.append(key) 

324 elif key[-1] == "*": 

325 items.append(f"{key}={value}") 

326 else: 

327 items.append( 

328 f"{key}={quote_header_value(value, allow_token=allow_token)}" 

329 ) 

330 else: 

331 items = [quote_header_value(x, allow_token=allow_token) for x in iterable] 

332 

333 return ", ".join(items) 

334 

335 

336def dump_csp_header(header: ds.ContentSecurityPolicy) -> str: 

337 """Dump a Content Security Policy header. 

338 

339 These are structured into policies such as "default-src 'self'; 

340 script-src 'self'". 

341 

342 .. versionadded:: 1.0.0 

343 Support for Content Security Policy headers was added. 

344 

345 """ 

346 return "; ".join(f"{key} {value}" for key, value in header.items()) 

347 

348 

349def parse_list_header(value: str) -> list[str]: 

350 """Parse a header value that consists of a list of comma separated items according 

351 to `RFC 9110 <https://httpwg.org/specs/rfc9110.html#abnf.extension>`__. 

352 

353 This extends :func:`urllib.request.parse_http_list` to remove surrounding quotes 

354 from values. 

355 

356 .. code-block:: python 

357 

358 parse_list_header('token, "quoted value"') 

359 ['token', 'quoted value'] 

360 

361 This is the reverse of :func:`dump_header`. 

362 

363 :param value: The header value to parse. 

364 """ 

365 result = [] 

366 

367 for item in _parse_list_header(value): 

368 if len(item) >= 2 and item[0] == item[-1] == '"': 

369 item = item[1:-1] 

370 

371 result.append(item) 

372 

373 return result 

374 

375 

376def parse_dict_header(value: str, cls: type[dict] | None = None) -> dict[str, str]: 

377 """Parse a list header using :func:`parse_list_header`, then parse each item as a 

378 ``key=value`` pair. 

379 

380 .. code-block:: python 

381 

382 parse_dict_header('a=b, c="d, e", f') 

383 {"a": "b", "c": "d, e", "f": None} 

384 

385 This is the reverse of :func:`dump_header`. 

386 

387 If a key does not have a value, it is ``None``. 

388 

389 This handles charsets for values as described in 

390 `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__. Only ASCII, UTF-8, 

391 and ISO-8859-1 charsets are accepted, otherwise the value remains quoted. 

392 

393 :param value: The header value to parse. 

394 

395 .. versionchanged:: 2.3 

396 Added support for ``key*=charset''value`` encoded items. 

397 

398 .. versionchanged:: 2.3 

399 Passing bytes is deprecated, support will be removed in Werkzeug 3.0. 

400 

401 .. versionchanged:: 2.3 

402 The ``cls`` argument is deprecated and will be removed in Werkzeug 3.0. 

403 

404 .. versionchanged:: 0.9 

405 The ``cls`` argument was added. 

406 """ 

407 if cls is None: 

408 cls = dict 

409 else: 

410 warnings.warn( 

411 "The 'cls' parameter is deprecated and will be removed in Werkzeug 3.0.", 

412 DeprecationWarning, 

413 stacklevel=2, 

414 ) 

415 

416 result = cls() 

417 

418 if isinstance(value, bytes): 

419 warnings.warn( 

420 "Passing bytes is deprecated and will be removed in Werkzeug 3.0.", 

421 DeprecationWarning, 

422 stacklevel=2, 

423 ) 

424 value = value.decode("latin1") 

425 

426 for item in parse_list_header(value): 

427 key, has_value, value = item.partition("=") 

428 key = key.strip() 

429 

430 if not has_value: 

431 result[key] = None 

432 continue 

433 

434 value = value.strip() 

435 encoding: str | None = None 

436 

437 if key[-1] == "*": 

438 # key*=charset''value becomes key=value, where value is percent encoded 

439 # adapted from parse_options_header, without the continuation handling 

440 key = key[:-1] 

441 match = _charset_value_re.match(value) 

442 

443 if match: 

444 # If there is a charset marker in the value, split it off. 

445 encoding, value = match.groups() 

446 encoding = encoding.lower() 

447 

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

449 # This list will not be extended further. An invalid encoding will leave the 

450 # value quoted. 

451 if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}: 

452 # invalid bytes are replaced during unquoting 

453 value = unquote(value, encoding=encoding) 

454 

455 if len(value) >= 2 and value[0] == value[-1] == '"': 

456 value = value[1:-1] 

457 

458 result[key] = value 

459 

460 return result 

461 

462 

463# https://httpwg.org/specs/rfc9110.html#parameter 

464_parameter_re = re.compile( 

465 r""" 

466 # don't match multiple empty parts, that causes backtracking 

467 \s*;\s* # find the part delimiter 

468 (?: 

469 ([\w!#$%&'*+\-.^`|~]+) # key, one or more token chars 

470 = # equals, with no space on either side 

471 ( # value, token or quoted string 

472 [\w!#$%&'*+\-.^`|~]+ # one or more token chars 

473 | 

474 "(?:\\\\|\\"|.)*?" # quoted string, consuming slash escapes 

475 ) 

476 )? # optionally match key=value, to account for empty parts 

477 """, 

478 re.ASCII | re.VERBOSE, 

479) 

480# https://www.rfc-editor.org/rfc/rfc2231#section-4 

481_charset_value_re = re.compile( 

482 r""" 

483 ([\w!#$%&*+\-.^`|~]*)' # charset part, could be empty 

484 [\w!#$%&*+\-.^`|~]*' # don't care about language part, usually empty 

485 ([\w!#$%&'*+\-.^`|~]+) # one or more token chars with percent encoding 

486 """, 

487 re.ASCII | re.VERBOSE, 

488) 

489# https://www.rfc-editor.org/rfc/rfc2231#section-3 

490_continuation_re = re.compile(r"\*(\d+)$", re.ASCII) 

491 

492 

493def parse_options_header(value: str | None) -> tuple[str, dict[str, str]]: 

494 """Parse a header that consists of a value with ``key=value`` parameters separated 

495 by semicolons ``;``. For example, the ``Content-Type`` header. 

496 

497 .. code-block:: python 

498 

499 parse_options_header("text/html; charset=UTF-8") 

500 ('text/html', {'charset': 'UTF-8'}) 

501 

502 parse_options_header("") 

503 ("", {}) 

504 

505 This is the reverse of :func:`dump_options_header`. 

506 

507 This parses valid parameter parts as described in 

508 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#parameter>`__. Invalid parts are 

509 skipped. 

510 

511 This handles continuations and charsets as described in 

512 `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__, although not as 

513 strictly as the RFC. Only ASCII, UTF-8, and ISO-8859-1 charsets are accepted, 

514 otherwise the value remains quoted. 

515 

516 Clients may not be consistent in how they handle a quote character within a quoted 

517 value. The `HTML Standard <https://html.spec.whatwg.org/#multipart-form-data>`__ 

518 replaces it with ``%22`` in multipart form data. 

519 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#quoted.strings>`__ uses backslash 

520 escapes in HTTP headers. Both are decoded to the ``"`` character. 

521 

522 Clients may not be consistent in how they handle non-ASCII characters. HTML 

523 documents must declare ``<meta charset=UTF-8>``, otherwise browsers may replace with 

524 HTML character references, which can be decoded using :func:`html.unescape`. 

525 

526 :param value: The header value to parse. 

527 :return: ``(value, options)``, where ``options`` is a dict 

528 

529 .. versionchanged:: 2.3 

530 Invalid parts, such as keys with no value, quoted keys, and incorrectly quoted 

531 values, are discarded instead of treating as ``None``. 

532 

533 .. versionchanged:: 2.3 

534 Only ASCII, UTF-8, and ISO-8859-1 are accepted for charset values. 

535 

536 .. versionchanged:: 2.3 

537 Escaped quotes in quoted values, like ``%22`` and ``\\"``, are handled. 

538 

539 .. versionchanged:: 2.2 

540 Option names are always converted to lowercase. 

541 

542 .. versionchanged:: 2.2 

543 The ``multiple`` parameter was removed. 

544 

545 .. versionchanged:: 0.15 

546 :rfc:`2231` parameter continuations are handled. 

547 

548 .. versionadded:: 0.5 

549 """ 

550 if value is None: 

551 return "", {} 

552 

553 value, _, rest = value.partition(";") 

554 value = value.strip() 

555 rest = rest.strip() 

556 

557 if not value or not rest: 

558 # empty (invalid) value, or value without options 

559 return value, {} 

560 

561 rest = f";{rest}" 

562 options: dict[str, str] = {} 

563 encoding: str | None = None 

564 continued_encoding: str | None = None 

565 

566 for pk, pv in _parameter_re.findall(rest): 

567 if not pk: 

568 # empty or invalid part 

569 continue 

570 

571 pk = pk.lower() 

572 

573 if pk[-1] == "*": 

574 # key*=charset''value becomes key=value, where value is percent encoded 

575 pk = pk[:-1] 

576 match = _charset_value_re.match(pv) 

577 

578 if match: 

579 # If there is a valid charset marker in the value, split it off. 

580 encoding, pv = match.groups() 

581 # This might be the empty string, handled next. 

582 encoding = encoding.lower() 

583 

584 # No charset marker, or marker with empty charset value. 

585 if not encoding: 

586 encoding = continued_encoding 

587 

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

589 # This list will not be extended further. An invalid encoding will leave the 

590 # value quoted. 

591 if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}: 

592 # Continuation parts don't require their own charset marker. This is 

593 # looser than the RFC, it will persist across different keys and allows 

594 # changing the charset during a continuation. But this implementation is 

595 # much simpler than tracking the full state. 

596 continued_encoding = encoding 

597 # invalid bytes are replaced during unquoting 

598 pv = unquote(pv, encoding=encoding) 

599 

600 # Remove quotes. At this point the value cannot be empty or a single quote. 

601 if pv[0] == pv[-1] == '"': 

602 # HTTP headers use slash, multipart form data uses percent 

603 pv = pv[1:-1].replace("\\\\", "\\").replace('\\"', '"').replace("%22", '"') 

604 

605 match = _continuation_re.search(pk) 

606 

607 if match: 

608 # key*0=a; key*1=b becomes key=ab 

609 pk = pk[: match.start()] 

610 options[pk] = options.get(pk, "") + pv 

611 else: 

612 options[pk] = pv 

613 

614 return value, options 

615 

616 

617_TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept") 

618 

619 

620@t.overload 

621def parse_accept_header(value: str | None) -> ds.Accept: 

622 ... 

623 

624 

625@t.overload 

626def parse_accept_header(value: str | None, cls: type[_TAnyAccept]) -> _TAnyAccept: 

627 ... 

628 

629 

630def parse_accept_header( 

631 value: str | None, cls: type[_TAnyAccept] | None = None 

632) -> _TAnyAccept: 

633 """Parse an ``Accept`` header according to 

634 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.accept>`__. 

635 

636 Returns an :class:`.Accept` instance, which can sort and inspect items based on 

637 their quality parameter. When parsing ``Accept-Charset``, ``Accept-Encoding``, or 

638 ``Accept-Language``, pass the appropriate :class:`.Accept` subclass. 

639 

640 :param value: The header value to parse. 

641 :param cls: The :class:`.Accept` class to wrap the result in. 

642 :return: An instance of ``cls``. 

643 

644 .. versionchanged:: 2.3 

645 Parse according to RFC 9110. Items with invalid ``q`` values are skipped. 

646 """ 

647 if cls is None: 

648 cls = t.cast(t.Type[_TAnyAccept], ds.Accept) 

649 

650 if not value: 

651 return cls(None) 

652 

653 result = [] 

654 

655 for item in parse_list_header(value): 

656 item, options = parse_options_header(item) 

657 

658 if "q" in options: 

659 try: 

660 # pop q, remaining options are reconstructed 

661 q = _plain_float(options.pop("q")) 

662 except ValueError: 

663 # ignore an invalid q 

664 continue 

665 

666 if q < 0 or q > 1: 

667 # ignore an invalid q 

668 continue 

669 else: 

670 q = 1 

671 

672 if options: 

673 # reconstruct the media type with any options 

674 item = dump_options_header(item, options) 

675 

676 result.append((item, q)) 

677 

678 return cls(result) 

679 

680 

681_TAnyCC = t.TypeVar("_TAnyCC", bound="ds.cache_control._CacheControl") 

682_t_cc_update = t.Optional[t.Callable[[_TAnyCC], None]] 

683 

684 

685@t.overload 

686def parse_cache_control_header( 

687 value: str | None, on_update: _t_cc_update, cls: None = None 

688) -> ds.RequestCacheControl: 

689 ... 

690 

691 

692@t.overload 

693def parse_cache_control_header( 

694 value: str | None, on_update: _t_cc_update, cls: type[_TAnyCC] 

695) -> _TAnyCC: 

696 ... 

697 

698 

699def parse_cache_control_header( 

700 value: str | None, 

701 on_update: _t_cc_update = None, 

702 cls: type[_TAnyCC] | None = None, 

703) -> _TAnyCC: 

704 """Parse a cache control header. The RFC differs between response and 

705 request cache control, this method does not. It's your responsibility 

706 to not use the wrong control statements. 

707 

708 .. versionadded:: 0.5 

709 The `cls` was added. If not specified an immutable 

710 :class:`~werkzeug.datastructures.RequestCacheControl` is returned. 

711 

712 :param value: a cache control header to be parsed. 

713 :param on_update: an optional callable that is called every time a value 

714 on the :class:`~werkzeug.datastructures.CacheControl` 

715 object is changed. 

716 :param cls: the class for the returned object. By default 

717 :class:`~werkzeug.datastructures.RequestCacheControl` is used. 

718 :return: a `cls` object. 

719 """ 

720 if cls is None: 

721 cls = t.cast(t.Type[_TAnyCC], ds.RequestCacheControl) 

722 

723 if not value: 

724 return cls((), on_update) 

725 

726 return cls(parse_dict_header(value), on_update) 

727 

728 

729_TAnyCSP = t.TypeVar("_TAnyCSP", bound="ds.ContentSecurityPolicy") 

730_t_csp_update = t.Optional[t.Callable[[_TAnyCSP], None]] 

731 

732 

733@t.overload 

734def parse_csp_header( 

735 value: str | None, on_update: _t_csp_update, cls: None = None 

736) -> ds.ContentSecurityPolicy: 

737 ... 

738 

739 

740@t.overload 

741def parse_csp_header( 

742 value: str | None, on_update: _t_csp_update, cls: type[_TAnyCSP] 

743) -> _TAnyCSP: 

744 ... 

745 

746 

747def parse_csp_header( 

748 value: str | None, 

749 on_update: _t_csp_update = None, 

750 cls: type[_TAnyCSP] | None = None, 

751) -> _TAnyCSP: 

752 """Parse a Content Security Policy header. 

753 

754 .. versionadded:: 1.0.0 

755 Support for Content Security Policy headers was added. 

756 

757 :param value: a csp header to be parsed. 

758 :param on_update: an optional callable that is called every time a value 

759 on the object is changed. 

760 :param cls: the class for the returned object. By default 

761 :class:`~werkzeug.datastructures.ContentSecurityPolicy` is used. 

762 :return: a `cls` object. 

763 """ 

764 if cls is None: 

765 cls = t.cast(t.Type[_TAnyCSP], ds.ContentSecurityPolicy) 

766 

767 if value is None: 

768 return cls((), on_update) 

769 

770 items = [] 

771 

772 for policy in value.split(";"): 

773 policy = policy.strip() 

774 

775 # Ignore badly formatted policies (no space) 

776 if " " in policy: 

777 directive, value = policy.strip().split(" ", 1) 

778 items.append((directive.strip(), value.strip())) 

779 

780 return cls(items, on_update) 

781 

782 

783def parse_set_header( 

784 value: str | None, 

785 on_update: t.Callable[[ds.HeaderSet], None] | None = None, 

786) -> ds.HeaderSet: 

787 """Parse a set-like header and return a 

788 :class:`~werkzeug.datastructures.HeaderSet` object: 

789 

790 >>> hs = parse_set_header('token, "quoted value"') 

791 

792 The return value is an object that treats the items case-insensitively 

793 and keeps the order of the items: 

794 

795 >>> 'TOKEN' in hs 

796 True 

797 >>> hs.index('quoted value') 

798 1 

799 >>> hs 

800 HeaderSet(['token', 'quoted value']) 

801 

802 To create a header from the :class:`HeaderSet` again, use the 

803 :func:`dump_header` function. 

804 

805 :param value: a set header to be parsed. 

806 :param on_update: an optional callable that is called every time a 

807 value on the :class:`~werkzeug.datastructures.HeaderSet` 

808 object is changed. 

809 :return: a :class:`~werkzeug.datastructures.HeaderSet` 

810 """ 

811 if not value: 

812 return ds.HeaderSet(None, on_update) 

813 return ds.HeaderSet(parse_list_header(value), on_update) 

814 

815 

816def parse_authorization_header( 

817 value: str | None, 

818) -> ds.Authorization | None: 

819 """Parse an HTTP basic/digest authorization header transmitted by the web 

820 browser. The return value is either `None` if the header was invalid or 

821 not given, otherwise an :class:`~werkzeug.datastructures.Authorization` 

822 object. 

823 

824 :param value: the authorization header to parse. 

825 :return: a :class:`~werkzeug.datastructures.Authorization` object or `None`. 

826 

827 .. deprecated:: 2.3 

828 Will be removed in Werkzeug 3.0. Use :meth:`.Authorization.from_header` instead. 

829 """ 

830 from .datastructures import Authorization 

831 

832 warnings.warn( 

833 "'parse_authorization_header' is deprecated and will be removed in Werkzeug" 

834 " 2.4. Use 'Authorization.from_header' instead.", 

835 DeprecationWarning, 

836 stacklevel=2, 

837 ) 

838 return Authorization.from_header(value) 

839 

840 

841def parse_www_authenticate_header( 

842 value: str | None, 

843 on_update: t.Callable[[ds.WWWAuthenticate], None] | None = None, 

844) -> ds.WWWAuthenticate: 

845 """Parse an HTTP WWW-Authenticate header into a 

846 :class:`~werkzeug.datastructures.WWWAuthenticate` object. 

847 

848 :param value: a WWW-Authenticate header to parse. 

849 :param on_update: an optional callable that is called every time a value 

850 on the :class:`~werkzeug.datastructures.WWWAuthenticate` 

851 object is changed. 

852 :return: a :class:`~werkzeug.datastructures.WWWAuthenticate` object. 

853 

854 .. deprecated:: 2.3 

855 Will be removed in Werkzeug 3.0. Use :meth:`.WWWAuthenticate.from_header` 

856 instead. 

857 """ 

858 from .datastructures.auth import WWWAuthenticate 

859 

860 warnings.warn( 

861 "'parse_www_authenticate_header' is deprecated and will be removed in Werkzeug" 

862 " 2.4. Use 'WWWAuthenticate.from_header' instead.", 

863 DeprecationWarning, 

864 stacklevel=2, 

865 ) 

866 rv = WWWAuthenticate.from_header(value) 

867 

868 if rv is None: 

869 rv = WWWAuthenticate("basic") 

870 

871 rv._on_update = on_update 

872 return rv 

873 

874 

875def parse_if_range_header(value: str | None) -> ds.IfRange: 

876 """Parses an if-range header which can be an etag or a date. Returns 

877 a :class:`~werkzeug.datastructures.IfRange` object. 

878 

879 .. versionchanged:: 2.0 

880 If the value represents a datetime, it is timezone-aware. 

881 

882 .. versionadded:: 0.7 

883 """ 

884 if not value: 

885 return ds.IfRange() 

886 date = parse_date(value) 

887 if date is not None: 

888 return ds.IfRange(date=date) 

889 # drop weakness information 

890 return ds.IfRange(unquote_etag(value)[0]) 

891 

892 

893def parse_range_header( 

894 value: str | None, make_inclusive: bool = True 

895) -> ds.Range | None: 

896 """Parses a range header into a :class:`~werkzeug.datastructures.Range` 

897 object. If the header is missing or malformed `None` is returned. 

898 `ranges` is a list of ``(start, stop)`` tuples where the ranges are 

899 non-inclusive. 

900 

901 .. versionadded:: 0.7 

902 """ 

903 if not value or "=" not in value: 

904 return None 

905 

906 ranges = [] 

907 last_end = 0 

908 units, rng = value.split("=", 1) 

909 units = units.strip().lower() 

910 

911 for item in rng.split(","): 

912 item = item.strip() 

913 if "-" not in item: 

914 return None 

915 if item.startswith("-"): 

916 if last_end < 0: 

917 return None 

918 try: 

919 begin = _plain_int(item) 

920 except ValueError: 

921 return None 

922 end = None 

923 last_end = -1 

924 elif "-" in item: 

925 begin_str, end_str = item.split("-", 1) 

926 begin_str = begin_str.strip() 

927 end_str = end_str.strip() 

928 

929 try: 

930 begin = _plain_int(begin_str) 

931 except ValueError: 

932 return None 

933 

934 if begin < last_end or last_end < 0: 

935 return None 

936 if end_str: 

937 try: 

938 end = _plain_int(end_str) + 1 

939 except ValueError: 

940 return None 

941 

942 if begin >= end: 

943 return None 

944 else: 

945 end = None 

946 last_end = end if end is not None else -1 

947 ranges.append((begin, end)) 

948 

949 return ds.Range(units, ranges) 

950 

951 

952def parse_content_range_header( 

953 value: str | None, 

954 on_update: t.Callable[[ds.ContentRange], None] | None = None, 

955) -> ds.ContentRange | None: 

956 """Parses a range header into a 

957 :class:`~werkzeug.datastructures.ContentRange` object or `None` if 

958 parsing is not possible. 

959 

960 .. versionadded:: 0.7 

961 

962 :param value: a content range header to be parsed. 

963 :param on_update: an optional callable that is called every time a value 

964 on the :class:`~werkzeug.datastructures.ContentRange` 

965 object is changed. 

966 """ 

967 if value is None: 

968 return None 

969 try: 

970 units, rangedef = (value or "").strip().split(None, 1) 

971 except ValueError: 

972 return None 

973 

974 if "/" not in rangedef: 

975 return None 

976 rng, length_str = rangedef.split("/", 1) 

977 if length_str == "*": 

978 length = None 

979 else: 

980 try: 

981 length = _plain_int(length_str) 

982 except ValueError: 

983 return None 

984 

985 if rng == "*": 

986 if not is_byte_range_valid(None, None, length): 

987 return None 

988 

989 return ds.ContentRange(units, None, None, length, on_update=on_update) 

990 elif "-" not in rng: 

991 return None 

992 

993 start_str, stop_str = rng.split("-", 1) 

994 try: 

995 start = _plain_int(start_str) 

996 stop = _plain_int(stop_str) + 1 

997 except ValueError: 

998 return None 

999 

1000 if is_byte_range_valid(start, stop, length): 

1001 return ds.ContentRange(units, start, stop, length, on_update=on_update) 

1002 

1003 return None 

1004 

1005 

1006def quote_etag(etag: str, weak: bool = False) -> str: 

1007 """Quote an etag. 

1008 

1009 :param etag: the etag to quote. 

1010 :param weak: set to `True` to tag it "weak". 

1011 """ 

1012 if '"' in etag: 

1013 raise ValueError("invalid etag") 

1014 etag = f'"{etag}"' 

1015 if weak: 

1016 etag = f"W/{etag}" 

1017 return etag 

1018 

1019 

1020def unquote_etag( 

1021 etag: str | None, 

1022) -> tuple[str, bool] | tuple[None, None]: 

1023 """Unquote a single etag: 

1024 

1025 >>> unquote_etag('W/"bar"') 

1026 ('bar', True) 

1027 >>> unquote_etag('"bar"') 

1028 ('bar', False) 

1029 

1030 :param etag: the etag identifier to unquote. 

1031 :return: a ``(etag, weak)`` tuple. 

1032 """ 

1033 if not etag: 

1034 return None, None 

1035 etag = etag.strip() 

1036 weak = False 

1037 if etag.startswith(("W/", "w/")): 

1038 weak = True 

1039 etag = etag[2:] 

1040 if etag[:1] == etag[-1:] == '"': 

1041 etag = etag[1:-1] 

1042 return etag, weak 

1043 

1044 

1045def parse_etags(value: str | None) -> ds.ETags: 

1046 """Parse an etag header. 

1047 

1048 :param value: the tag header to parse 

1049 :return: an :class:`~werkzeug.datastructures.ETags` object. 

1050 """ 

1051 if not value: 

1052 return ds.ETags() 

1053 strong = [] 

1054 weak = [] 

1055 end = len(value) 

1056 pos = 0 

1057 while pos < end: 

1058 match = _etag_re.match(value, pos) 

1059 if match is None: 

1060 break 

1061 is_weak, quoted, raw = match.groups() 

1062 if raw == "*": 

1063 return ds.ETags(star_tag=True) 

1064 elif quoted: 

1065 raw = quoted 

1066 if is_weak: 

1067 weak.append(raw) 

1068 else: 

1069 strong.append(raw) 

1070 pos = match.end() 

1071 return ds.ETags(strong, weak) 

1072 

1073 

1074def generate_etag(data: bytes) -> str: 

1075 """Generate an etag for some data. 

1076 

1077 .. versionchanged:: 2.0 

1078 Use SHA-1. MD5 may not be available in some environments. 

1079 """ 

1080 return sha1(data).hexdigest() 

1081 

1082 

1083def parse_date(value: str | None) -> datetime | None: 

1084 """Parse an :rfc:`2822` date into a timezone-aware 

1085 :class:`datetime.datetime` object, or ``None`` if parsing fails. 

1086 

1087 This is a wrapper for :func:`email.utils.parsedate_to_datetime`. It 

1088 returns ``None`` if parsing fails instead of raising an exception, 

1089 and always returns a timezone-aware datetime object. If the string 

1090 doesn't have timezone information, it is assumed to be UTC. 

1091 

1092 :param value: A string with a supported date format. 

1093 

1094 .. versionchanged:: 2.0 

1095 Return a timezone-aware datetime object. Use 

1096 ``email.utils.parsedate_to_datetime``. 

1097 """ 

1098 if value is None: 

1099 return None 

1100 

1101 try: 

1102 dt = email.utils.parsedate_to_datetime(value) 

1103 except (TypeError, ValueError): 

1104 return None 

1105 

1106 if dt.tzinfo is None: 

1107 return dt.replace(tzinfo=timezone.utc) 

1108 

1109 return dt 

1110 

1111 

1112def http_date( 

1113 timestamp: datetime | date | int | float | struct_time | None = None, 

1114) -> str: 

1115 """Format a datetime object or timestamp into an :rfc:`2822` date 

1116 string. 

1117 

1118 This is a wrapper for :func:`email.utils.format_datetime`. It 

1119 assumes naive datetime objects are in UTC instead of raising an 

1120 exception. 

1121 

1122 :param timestamp: The datetime or timestamp to format. Defaults to 

1123 the current time. 

1124 

1125 .. versionchanged:: 2.0 

1126 Use ``email.utils.format_datetime``. Accept ``date`` objects. 

1127 """ 

1128 if isinstance(timestamp, date): 

1129 if not isinstance(timestamp, datetime): 

1130 # Assume plain date is midnight UTC. 

1131 timestamp = datetime.combine(timestamp, time(), tzinfo=timezone.utc) 

1132 else: 

1133 # Ensure datetime is timezone-aware. 

1134 timestamp = _dt_as_utc(timestamp) 

1135 

1136 return email.utils.format_datetime(timestamp, usegmt=True) 

1137 

1138 if isinstance(timestamp, struct_time): 

1139 timestamp = mktime(timestamp) 

1140 

1141 return email.utils.formatdate(timestamp, usegmt=True) 

1142 

1143 

1144def parse_age(value: str | None = None) -> timedelta | None: 

1145 """Parses a base-10 integer count of seconds into a timedelta. 

1146 

1147 If parsing fails, the return value is `None`. 

1148 

1149 :param value: a string consisting of an integer represented in base-10 

1150 :return: a :class:`datetime.timedelta` object or `None`. 

1151 """ 

1152 if not value: 

1153 return None 

1154 try: 

1155 seconds = int(value) 

1156 except ValueError: 

1157 return None 

1158 if seconds < 0: 

1159 return None 

1160 try: 

1161 return timedelta(seconds=seconds) 

1162 except OverflowError: 

1163 return None 

1164 

1165 

1166def dump_age(age: timedelta | int | None = None) -> str | None: 

1167 """Formats the duration as a base-10 integer. 

1168 

1169 :param age: should be an integer number of seconds, 

1170 a :class:`datetime.timedelta` object, or, 

1171 if the age is unknown, `None` (default). 

1172 """ 

1173 if age is None: 

1174 return None 

1175 if isinstance(age, timedelta): 

1176 age = int(age.total_seconds()) 

1177 else: 

1178 age = int(age) 

1179 

1180 if age < 0: 

1181 raise ValueError("age cannot be negative") 

1182 

1183 return str(age) 

1184 

1185 

1186def is_resource_modified( 

1187 environ: WSGIEnvironment, 

1188 etag: str | None = None, 

1189 data: bytes | None = None, 

1190 last_modified: datetime | str | None = None, 

1191 ignore_if_range: bool = True, 

1192) -> bool: 

1193 """Convenience method for conditional requests. 

1194 

1195 :param environ: the WSGI environment of the request to be checked. 

1196 :param etag: the etag for the response for comparison. 

1197 :param data: or alternatively the data of the response to automatically 

1198 generate an etag using :func:`generate_etag`. 

1199 :param last_modified: an optional date of the last modification. 

1200 :param ignore_if_range: If `False`, `If-Range` header will be taken into 

1201 account. 

1202 :return: `True` if the resource was modified, otherwise `False`. 

1203 

1204 .. versionchanged:: 2.0 

1205 SHA-1 is used to generate an etag value for the data. MD5 may 

1206 not be available in some environments. 

1207 

1208 .. versionchanged:: 1.0.0 

1209 The check is run for methods other than ``GET`` and ``HEAD``. 

1210 """ 

1211 return _sansio_http.is_resource_modified( 

1212 http_range=environ.get("HTTP_RANGE"), 

1213 http_if_range=environ.get("HTTP_IF_RANGE"), 

1214 http_if_modified_since=environ.get("HTTP_IF_MODIFIED_SINCE"), 

1215 http_if_none_match=environ.get("HTTP_IF_NONE_MATCH"), 

1216 http_if_match=environ.get("HTTP_IF_MATCH"), 

1217 etag=etag, 

1218 data=data, 

1219 last_modified=last_modified, 

1220 ignore_if_range=ignore_if_range, 

1221 ) 

1222 

1223 

1224def remove_entity_headers( 

1225 headers: ds.Headers | list[tuple[str, str]], 

1226 allowed: t.Iterable[str] = ("expires", "content-location"), 

1227) -> None: 

1228 """Remove all entity headers from a list or :class:`Headers` object. This 

1229 operation works in-place. `Expires` and `Content-Location` headers are 

1230 by default not removed. The reason for this is :rfc:`2616` section 

1231 10.3.5 which specifies some entity headers that should be sent. 

1232 

1233 .. versionchanged:: 0.5 

1234 added `allowed` parameter. 

1235 

1236 :param headers: a list or :class:`Headers` object. 

1237 :param allowed: a list of headers that should still be allowed even though 

1238 they are entity headers. 

1239 """ 

1240 allowed = {x.lower() for x in allowed} 

1241 headers[:] = [ 

1242 (key, value) 

1243 for key, value in headers 

1244 if not is_entity_header(key) or key.lower() in allowed 

1245 ] 

1246 

1247 

1248def remove_hop_by_hop_headers(headers: ds.Headers | list[tuple[str, str]]) -> None: 

1249 """Remove all HTTP/1.1 "Hop-by-Hop" headers from a list or 

1250 :class:`Headers` object. This operation works in-place. 

1251 

1252 .. versionadded:: 0.5 

1253 

1254 :param headers: a list or :class:`Headers` object. 

1255 """ 

1256 headers[:] = [ 

1257 (key, value) for key, value in headers if not is_hop_by_hop_header(key) 

1258 ] 

1259 

1260 

1261def is_entity_header(header: str) -> bool: 

1262 """Check if a header is an entity header. 

1263 

1264 .. versionadded:: 0.5 

1265 

1266 :param header: the header to test. 

1267 :return: `True` if it's an entity header, `False` otherwise. 

1268 """ 

1269 return header.lower() in _entity_headers 

1270 

1271 

1272def is_hop_by_hop_header(header: str) -> bool: 

1273 """Check if a header is an HTTP/1.1 "Hop-by-Hop" header. 

1274 

1275 .. versionadded:: 0.5 

1276 

1277 :param header: the header to test. 

1278 :return: `True` if it's an HTTP/1.1 "Hop-by-Hop" header, `False` otherwise. 

1279 """ 

1280 return header.lower() in _hop_by_hop_headers 

1281 

1282 

1283def parse_cookie( 

1284 header: WSGIEnvironment | str | None, 

1285 charset: str | None = None, 

1286 errors: str | None = None, 

1287 cls: type[ds.MultiDict] | None = None, 

1288) -> ds.MultiDict[str, str]: 

1289 """Parse a cookie from a string or WSGI environ. 

1290 

1291 The same key can be provided multiple times, the values are stored 

1292 in-order. The default :class:`MultiDict` will have the first value 

1293 first, and all values can be retrieved with 

1294 :meth:`MultiDict.getlist`. 

1295 

1296 :param header: The cookie header as a string, or a WSGI environ dict 

1297 with a ``HTTP_COOKIE`` key. 

1298 :param cls: A dict-like class to store the parsed cookies in. 

1299 Defaults to :class:`MultiDict`. 

1300 

1301 .. versionchanged:: 2.3 

1302 Passing bytes, and the ``charset`` and ``errors`` parameters, are deprecated and 

1303 will be removed in Werkzeug 3.0. 

1304 

1305 .. versionchanged:: 1.0 

1306 Returns a :class:`MultiDict` instead of a ``TypeConversionDict``. 

1307 

1308 .. versionchanged:: 0.5 

1309 Returns a :class:`TypeConversionDict` instead of a regular dict. The ``cls`` 

1310 parameter was added. 

1311 """ 

1312 if isinstance(header, dict): 

1313 cookie = header.get("HTTP_COOKIE") 

1314 elif isinstance(header, bytes): 

1315 warnings.warn( 

1316 "Passing bytes is deprecated and will not be supported in Werkzeug 3.0.", 

1317 DeprecationWarning, 

1318 stacklevel=2, 

1319 ) 

1320 cookie = header.decode() 

1321 else: 

1322 cookie = header 

1323 

1324 if cookie: 

1325 cookie = cookie.encode("latin1").decode() 

1326 

1327 return _sansio_http.parse_cookie( 

1328 cookie=cookie, charset=charset, errors=errors, cls=cls 

1329 ) 

1330 

1331 

1332_cookie_no_quote_re = re.compile(r"[\w!#$%&'()*+\-./:<=>?@\[\]^`{|}~]*", re.A) 

1333_cookie_slash_re = re.compile(rb"[\x00-\x19\",;\\\x7f-\xff]", re.A) 

1334_cookie_slash_map = {b'"': b'\\"', b"\\": b"\\\\"} 

1335_cookie_slash_map.update( 

1336 (v.to_bytes(1, "big"), b"\\%03o" % v) 

1337 for v in [*range(0x20), *b",;", *range(0x7F, 256)] 

1338) 

1339 

1340 

1341def dump_cookie( 

1342 key: str, 

1343 value: str = "", 

1344 max_age: timedelta | int | None = None, 

1345 expires: str | datetime | int | float | None = None, 

1346 path: str | None = "/", 

1347 domain: str | None = None, 

1348 secure: bool = False, 

1349 httponly: bool = False, 

1350 charset: str | None = None, 

1351 sync_expires: bool = True, 

1352 max_size: int = 4093, 

1353 samesite: str | None = None, 

1354) -> str: 

1355 """Create a Set-Cookie header without the ``Set-Cookie`` prefix. 

1356 

1357 The return value is usually restricted to ascii as the vast majority 

1358 of values are properly escaped, but that is no guarantee. It's 

1359 tunneled through latin1 as required by :pep:`3333`. 

1360 

1361 The return value is not ASCII safe if the key contains unicode 

1362 characters. This is technically against the specification but 

1363 happens in the wild. It's strongly recommended to not use 

1364 non-ASCII values for the keys. 

1365 

1366 :param max_age: should be a number of seconds, or `None` (default) if 

1367 the cookie should last only as long as the client's 

1368 browser session. Additionally `timedelta` objects 

1369 are accepted, too. 

1370 :param expires: should be a `datetime` object or unix timestamp. 

1371 :param path: limits the cookie to a given path, per default it will 

1372 span the whole domain. 

1373 :param domain: Use this if you want to set a cross-domain cookie. For 

1374 example, ``domain=".example.com"`` will set a cookie 

1375 that is readable by the domain ``www.example.com``, 

1376 ``foo.example.com`` etc. Otherwise, a cookie will only 

1377 be readable by the domain that set it. 

1378 :param secure: The cookie will only be available via HTTPS 

1379 :param httponly: disallow JavaScript to access the cookie. This is an 

1380 extension to the cookie standard and probably not 

1381 supported by all browsers. 

1382 :param charset: the encoding for string values. 

1383 :param sync_expires: automatically set expires if max_age is defined 

1384 but expires not. 

1385 :param max_size: Warn if the final header value exceeds this size. The 

1386 default, 4093, should be safely `supported by most browsers 

1387 <cookie_>`_. Set to 0 to disable this check. 

1388 :param samesite: Limits the scope of the cookie such that it will 

1389 only be attached to requests if those requests are same-site. 

1390 

1391 .. _`cookie`: http://browsercookielimits.squawky.net/ 

1392 

1393 .. versionchanged:: 2.3.3 

1394 The ``path`` parameter is ``/`` by default. 

1395 

1396 .. versionchanged:: 2.3.1 

1397 The value allows more characters without quoting. 

1398 

1399 .. versionchanged:: 2.3 

1400 ``localhost`` and other names without a dot are allowed for the domain. A 

1401 leading dot is ignored. 

1402 

1403 .. versionchanged:: 2.3 

1404 The ``path`` parameter is ``None`` by default. 

1405 

1406 .. versionchanged:: 2.3 

1407 Passing bytes, and the ``charset`` parameter, are deprecated and will be removed 

1408 in Werkzeug 3.0. 

1409 

1410 .. versionchanged:: 1.0.0 

1411 The string ``'None'`` is accepted for ``samesite``. 

1412 """ 

1413 if charset is not None: 

1414 warnings.warn( 

1415 "The 'charset' parameter is deprecated and will be removed" 

1416 " in Werkzeug 3.0.", 

1417 DeprecationWarning, 

1418 stacklevel=2, 

1419 ) 

1420 else: 

1421 charset = "utf-8" 

1422 

1423 if isinstance(key, bytes): 

1424 warnings.warn( 

1425 "The 'key' parameter must be a string. Bytes are deprecated" 

1426 " and will not be supported in Werkzeug 3.0.", 

1427 DeprecationWarning, 

1428 stacklevel=2, 

1429 ) 

1430 key = key.decode() 

1431 

1432 if isinstance(value, bytes): 

1433 warnings.warn( 

1434 "The 'value' parameter must be a string. Bytes are" 

1435 " deprecated and will not be supported in Werkzeug 3.0.", 

1436 DeprecationWarning, 

1437 stacklevel=2, 

1438 ) 

1439 value = value.decode() 

1440 

1441 if path is not None: 

1442 # safe = https://url.spec.whatwg.org/#url-path-segment-string 

1443 # as well as percent for things that are already quoted 

1444 # excluding semicolon since it's part of the header syntax 

1445 path = quote(path, safe="%!$&'()*+,/:=@", encoding=charset) 

1446 

1447 if domain: 

1448 domain = domain.partition(":")[0].lstrip(".").encode("idna").decode("ascii") 

1449 

1450 if isinstance(max_age, timedelta): 

1451 max_age = int(max_age.total_seconds()) 

1452 

1453 if expires is not None: 

1454 if not isinstance(expires, str): 

1455 expires = http_date(expires) 

1456 elif max_age is not None and sync_expires: 

1457 expires = http_date(datetime.now(tz=timezone.utc).timestamp() + max_age) 

1458 

1459 if samesite is not None: 

1460 samesite = samesite.title() 

1461 

1462 if samesite not in {"Strict", "Lax", "None"}: 

1463 raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.") 

1464 

1465 # Quote value if it contains characters not allowed by RFC 6265. Slash-escape with 

1466 # three octal digits, which matches http.cookies, although the RFC suggests base64. 

1467 if not _cookie_no_quote_re.fullmatch(value): 

1468 # Work with bytes here, since a UTF-8 character could be multiple bytes. 

1469 value = _cookie_slash_re.sub( 

1470 lambda m: _cookie_slash_map[m.group()], value.encode(charset) 

1471 ).decode("ascii") 

1472 value = f'"{value}"' 

1473 

1474 # Send a non-ASCII key as mojibake. Everything else should already be ASCII. 

1475 # TODO Remove encoding dance, it seems like clients accept UTF-8 keys 

1476 buf = [f"{key.encode().decode('latin1')}={value}"] 

1477 

1478 for k, v in ( 

1479 ("Domain", domain), 

1480 ("Expires", expires), 

1481 ("Max-Age", max_age), 

1482 ("Secure", secure), 

1483 ("HttpOnly", httponly), 

1484 ("Path", path), 

1485 ("SameSite", samesite), 

1486 ): 

1487 if v is None or v is False: 

1488 continue 

1489 

1490 if v is True: 

1491 buf.append(k) 

1492 continue 

1493 

1494 buf.append(f"{k}={v}") 

1495 

1496 rv = "; ".join(buf) 

1497 

1498 # Warn if the final value of the cookie is larger than the limit. If the cookie is 

1499 # too large, then it may be silently ignored by the browser, which can be quite hard 

1500 # to debug. 

1501 cookie_size = len(rv) 

1502 

1503 if max_size and cookie_size > max_size: 

1504 value_size = len(value) 

1505 warnings.warn( 

1506 f"The '{key}' cookie is too large: the value was {value_size} bytes but the" 

1507 f" header required {cookie_size - value_size} extra bytes. The final size" 

1508 f" was {cookie_size} bytes but the limit is {max_size} bytes. Browsers may" 

1509 " silently ignore cookies larger than this.", 

1510 stacklevel=2, 

1511 ) 

1512 

1513 return rv 

1514 

1515 

1516def is_byte_range_valid( 

1517 start: int | None, stop: int | None, length: int | None 

1518) -> bool: 

1519 """Checks if a given byte content range is valid for the given length. 

1520 

1521 .. versionadded:: 0.7 

1522 """ 

1523 if (start is None) != (stop is None): 

1524 return False 

1525 elif start is None: 

1526 return length is None or length >= 0 

1527 elif length is None: 

1528 return 0 <= start < stop # type: ignore 

1529 elif start >= stop: # type: ignore 

1530 return False 

1531 return 0 <= start < length 

1532 

1533 

1534# circular dependencies 

1535from . import datastructures as ds 

1536from .sansio import http as _sansio_http