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

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

441 statements  

1from __future__ import annotations 

2 

3import collections.abc as cabc 

4import datetime as dt 

5import email.utils 

6import hashlib 

7import re 

8import typing as t 

9import warnings 

10import wsgiref.types as wt 

11from base64 import b64encode 

12from enum import Enum 

13from time import mktime 

14from time import struct_time 

15from urllib.parse import quote 

16from urllib.parse import unquote 

17 

18from ._internal import _dt_as_utc 

19from ._internal import _plain_int 

20 

21_token_chars = frozenset( 

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

23) 

24_HTTP_STATUS_CODES = { 

25 100: "Continue", 

26 101: "Switching Protocols", 

27 102: "Processing", 

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

29 200: "OK", 

30 201: "Created", 

31 202: "Accepted", 

32 203: "Non Authoritative Information", 

33 204: "No Content", 

34 205: "Reset Content", 

35 206: "Partial Content", 

36 207: "Multi Status", 

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

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

39 300: "Multiple Choices", 

40 301: "Moved Permanently", 

41 302: "Found", 

42 303: "See Other", 

43 304: "Not Modified", 

44 305: "Use Proxy", 

45 306: "Switch Proxy", # unused 

46 307: "Temporary Redirect", 

47 308: "Permanent Redirect", 

48 400: "Bad Request", 

49 401: "Unauthorized", 

50 402: "Payment Required", # unused 

51 403: "Forbidden", 

52 404: "Not Found", 

53 405: "Method Not Allowed", 

54 406: "Not Acceptable", 

55 407: "Proxy Authentication Required", 

56 408: "Request Timeout", 

57 409: "Conflict", 

58 410: "Gone", 

59 411: "Length Required", 

60 412: "Precondition Failed", 

61 413: "Request Entity Too Large", 

62 414: "Request URI Too Long", 

63 415: "Unsupported Media Type", 

64 416: "Requested Range Not Satisfiable", 

65 417: "Expectation Failed", 

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

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

68 422: "Unprocessable Entity", 

69 423: "Locked", 

70 424: "Failed Dependency", 

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

72 426: "Upgrade Required", 

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

74 429: "Too Many Requests", 

75 431: "Request Header Fields Too Large", 

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

77 451: "Unavailable For Legal Reasons", 

78 500: "Internal Server Error", 

79 501: "Not Implemented", 

80 502: "Bad Gateway", 

81 503: "Service Unavailable", 

82 504: "Gateway Timeout", 

83 505: "HTTP Version Not Supported", 

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

85 507: "Insufficient Storage", 

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

87 510: "Not Extended", 

88 511: "Network Authentication Failed", 

89} 

90 

91 

92class COEP(Enum): 

93 """``Cross-Origin-Embedder-Policy`` header values. Used by 

94 :attr:`.Response.cross_origin_embedder_policy`. 

95 

96 .. versionchanged:: 3.2 

97 Added the ``credentialless`` member. 

98 

99 .. versionadded:: 2.0 

100 """ 

101 

102 UNSAFE_NONE = "unsafe-none" 

103 REQUIRE_CORP = "require-corp" 

104 CREDENTIALLESS = "credentialless" 

105 

106 

107class COOP(Enum): 

108 """``Cross-Origin-Opener-Policy`` header values. Used by 

109 :attr:`.Response.cross_origin_opener_policy`. 

110 

111 .. versionchanged:: 3.2 

112 Added the ``noopener-allow-popups`` member. 

113 

114 .. versionadded:: 2.0 

115 """ 

116 

117 UNSAFE_NONE = "unsafe-none" 

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

119 SAME_ORIGIN = "same-origin" 

120 NOOPENER_ALLOW_POPUPS = "noopener-allow-popups" 

121 

122 

123class CORP(Enum): 

124 """``Cross-Origin-Resource-Policy`` header values. Used by 

125 :attr:`.Response.cross_origin_resource_policy`. 

126 

127 .. versionadded:: 3.2 

128 """ 

129 

130 SAME_SITE = "same-site" 

131 SAME_ORIGIN = "same-origin" 

132 CROSS_ORIGIN = "cross-origin" 

133 

134 

135class SecFetchSite(Enum): 

136 """``Sec-Fetch-Site`` header values. Used by :attr:`.Request.sec_fetch_site`. 

137 

138 .. versionadded:: 3.2 

139 """ 

140 

141 CROSS_SITE = "cross-site" 

142 SAME_ORIGIN = "same-origin" 

143 SAME_SITE = "same-site" 

144 NONE = "none" 

145 

146 

147class SecFetchMode(Enum): 

148 """``Sec-Fetch-Mode`` header values. Used by :attr:`.Request.sec_fetch_mode`. 

149 

150 .. versionadded:: 3.2 

151 """ 

152 

153 CORS = "cors" 

154 NAVIGATE = "navigate" 

155 NO_CORS = "no-cors" 

156 SAME_ORIGIN = "same-origin" 

157 WEBSOCKET = "websocket" 

158 

159 

160class SecFetchDest(Enum): 

161 """``Sec-Fetch-Dest`` header values. Used by :attr:`.Request.sec_fetch_dest`. 

162 

163 .. versionadded:: 3.2 

164 """ 

165 

166 AUDIO = "audio" 

167 AUDIOWORKLET = "audioworklet" 

168 DOCUMENT = "document" 

169 EMBED = "embed" 

170 EMPTY = "empty" 

171 FENCEDFRAME = "fencedframe" 

172 FONT = "font" 

173 FRAME = "frame" 

174 IFRAME = "iframe" 

175 IMAGE = "image" 

176 JSON = "json" 

177 MANIFEST = "manifest" 

178 OBJECT = "object" 

179 PAINTWORKLET = "paintworklet" 

180 REPORT = "report" 

181 SCRIPT = "script" 

182 SERVICEWORKER = "serviceworker" 

183 SHAREDWORKER = "sharedworker" 

184 STYLE = "style" 

185 TRACK = "track" 

186 VIDEO = "video" 

187 WEBIDENTITY = "webidentity" 

188 WORKER = "worker" 

189 XSLT = "xslt" 

190 

191 

192def quote_header_value(value: t.Any, allow_token: bool = True) -> str: 

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

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

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

196 

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

198 

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

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

201 

202 .. versionchanged:: 3.0 

203 Passing bytes is not supported. 

204 

205 .. versionchanged:: 3.0 

206 The ``extra_chars`` parameter is removed. 

207 

208 .. versionchanged:: 2.3 

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

210 

211 .. versionadded:: 0.5 

212 """ 

213 value_str = str(value) 

214 

215 if not value_str: 

216 return '""' 

217 

218 if allow_token: 

219 token_chars = _token_chars 

220 

221 if token_chars.issuperset(value_str): 

222 return value_str 

223 

224 value_str = value_str.replace("\\", "\\\\").replace('"', '\\"') 

225 return f'"{value_str}"' 

226 

227 

228_unslash_re = re.compile(r"\\(.)", re.A) 

229 

230 

231def unquote_header_value(value: str) -> str: 

232 """Remove double quotes and backslash escapes from a header value. 

233 

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

235 

236 :param value: The header value to unquote. 

237 

238 .. versionchanged:: 3.2 

239 Removes escape preceding any character. 

240 

241 .. versionchanged:: 3.0 

242 The ``is_filename`` parameter is removed. 

243 """ 

244 if len(value) >= 2 and value.startswith('"') and value.endswith('"'): 

245 return _unslash_re.sub(r"\g<1>", value[1:-1]) 

246 

247 return value 

248 

249 

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

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

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

253 

254 .. code-block:: python 

255 

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

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

258 

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

260 

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

262 

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

264 

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

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

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

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

269 

270 :param header: The primary header value. 

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

272 

273 .. versionchanged:: 2.3 

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

275 

276 .. versionchanged:: 2.2.3 

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

278 """ 

279 segments = [] 

280 

281 if header is not None: 

282 segments.append(header) 

283 

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

285 if value is None: 

286 continue 

287 

288 if key.endswith("*"): 

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

290 else: 

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

292 

293 return "; ".join(segments) 

294 

295 

296def dump_header(iterable: cabc.Mapping[str, t.Any] | cabc.Iterable[t.Any]) -> str: 

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

298 commas ``,``. 

299 

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

301 :func:`parse_set_header`. 

302 

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

304 

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

306 

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

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

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

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

311 

312 .. code-block:: python 

313 

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

315 'foo, "bar baz"' 

316 

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

318 'foo="bar baz"' 

319 

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

321 

322 .. versionchanged:: 3.0 

323 The ``allow_token`` parameter is removed. 

324 

325 .. versionchanged:: 2.2.3 

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

327 """ 

328 if isinstance(iterable, cabc.Mapping): 

329 items = [] 

330 

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

332 if value is None: 

333 items.append(key) 

334 elif key.endswith("*"): 

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

336 else: 

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

338 else: 

339 items = [quote_header_value(x) for x in iterable] 

340 

341 return ", ".join(items) 

342 

343 

344def _dump_csp_header(header: ds.ContentSecurityPolicy) -> str: 

345 """Dump a Content Security Policy header. 

346 

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

348 script-src 'self'". 

349 

350 .. deprecated:: 3.2 

351 Will be removed in Werkzeug 4.0. Use the 

352 ``ContentSecurityPolicy.to_header`` method instead. 

353 

354 .. versionadded:: 1.0.0 

355 Support for Content Security Policy headers was added. 

356 

357 """ 

358 return header.to_header() 

359 

360 

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

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

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

364 

365 Surrounding quotes are removed from items, but internal quotes are left for 

366 future parsing. Empty values are discarded. 

367 

368 .. code-block:: python 

369 

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

371 ['token', 'quoted value'] 

372 

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

374 

375 :param value: The header value to parse. 

376 

377 .. versionchanged:: 3.2 

378 Quotes and escapes are kept if only part of an item is quoted. Empty 

379 values are omitted. An empty list is returned if the value contains an 

380 unclosed quoted string. 

381 """ 

382 items = [] 

383 item = "" 

384 escape = False 

385 quote = False 

386 

387 for char in value: 

388 if escape: 

389 escape = False 

390 item += char 

391 continue 

392 

393 if quote: 

394 if char == "\\": 

395 escape = True 

396 elif char == '"': 

397 quote = False 

398 

399 item += char 

400 continue 

401 

402 if char == ",": 

403 items.append(item) 

404 item = "" 

405 continue 

406 

407 if char == '"': 

408 quote = True 

409 

410 item += char 

411 

412 if quote: 

413 # invalid, unclosed quoted string 

414 return [] 

415 

416 items.append(item) 

417 return [ 

418 unquote_header_value(item) 

419 for item in (item.strip(" \t") for item in items) 

420 if item 

421 ] 

422 

423 

424def parse_dict_header(value: str) -> dict[str, str | None]: 

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

426 ``key=value`` pair. 

427 

428 .. code-block:: python 

429 

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

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

432 

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

434 

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

436 

437 This handles charsets for values as described in 

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

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

440 

441 :param value: The header value to parse. 

442 

443 .. versionchanged:: 3.2 

444 An empty dict is returned if the value contains an unclosed quoted 

445 string. 

446 

447 .. versionchanged:: 3.0 

448 Passing bytes is not supported. 

449 

450 .. versionchanged:: 3.0 

451 The ``cls`` argument is removed. 

452 

453 .. versionchanged:: 2.3 

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

455 

456 .. versionchanged:: 0.9 

457 The ``cls`` argument was added. 

458 """ 

459 result: dict[str, str | None] = {} 

460 

461 for item in parse_list_header(value): 

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

463 key = key.strip(" \t") 

464 

465 if not key: 

466 # =value is not valid 

467 continue 

468 

469 if not has_value: 

470 result[key] = None 

471 continue 

472 

473 value = value.strip(" \t") 

474 encoding: str | None = None 

475 

476 if key.endswith("*"): 

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

478 # adapted from parse_options_header, without the continuation handling 

479 key = key[:-1] 

480 

481 if (m_charset := _charset_value_re.match(value)) is not None: 

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

483 encoding, value = m_charset.groups() 

484 encoding = encoding.lower() 

485 

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

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

488 # value quoted. 

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

490 # invalid bytes are replaced during unquoting 

491 value = unquote(value, encoding=encoding) 

492 

493 result[key] = unquote_header_value(value) 

494 

495 return result 

496 

497 

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

499_parameter_key_re = re.compile(r"([\w!#$%&'*+\-.^`|~]+)=", flags=re.ASCII) 

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

501_parameter_mark_re = re.compile(r"(?:\*(\d+))?(\*)?$", flags=re.ASCII) 

502_parameter_token_value_re = re.compile(r"[\w!#$%&'*+\-.^`|~]+", flags=re.ASCII) 

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

504_charset_value_re = re.compile( 

505 r""" 

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

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

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

509 """, 

510 re.ASCII | re.VERBOSE, 

511) 

512_parameter_delimiter_re = re.compile(r";[ \t]*") 

513 

514 

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

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

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

518 

519 .. code-block:: python 

520 

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

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

523 

524 parse_options_header("") 

525 ("", {}) 

526 

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

528 

529 This parses valid parameter parts as described in 

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

531 skipped. 

532 

533 This handles continuations and charsets as described in 

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

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

536 otherwise the value remains quoted. 

537 

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

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

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

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

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

543 

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

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

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

547 

548 :param value: The header value to parse. 

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

550 

551 .. versionchanged:: 2.3 

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

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

554 

555 .. versionchanged:: 2.3 

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

557 

558 .. versionchanged:: 2.3 

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

560 

561 .. versionchanged:: 2.2 

562 Option names are always converted to lowercase. 

563 

564 .. versionchanged:: 2.2 

565 The ``multiple`` parameter was removed. 

566 

567 .. versionchanged:: 0.15 

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

569 

570 .. versionadded:: 0.5 

571 """ 

572 if value is None: 

573 return "", {} 

574 

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

576 value = value.strip(" \t") 

577 rest = rest.strip(" \t") 

578 

579 if not value or not rest: 

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

581 return value, {} 

582 

583 # Collect all valid key=value parts without processing the value. 

584 parts: list[tuple[str, str]] = [] 

585 scan_pos = 0 

586 length = len(rest) 

587 

588 while scan_pos < length: 

589 if (m_key := _parameter_key_re.match(rest, scan_pos)) is not None: 

590 pk = m_key.group(1).lower() 

591 scan_pos = m_key.end() 

592 

593 # Value may be a token. 

594 if (m_value := _parameter_token_value_re.match(rest, scan_pos)) is not None: 

595 parts.append((pk, m_value.group())) 

596 

597 # Value may be a quoted string, find the closing quote. 

598 elif rest.startswith('"', scan_pos): 

599 quote_pos = scan_pos + 1 

600 

601 while quote_pos < length: 

602 if rest.startswith(("\\\\", '\\"'), quote_pos): 

603 # Consume escaped slashes and quotes. 

604 quote_pos += 2 

605 elif rest.startswith('"', quote_pos): 

606 # Stop at an unescaped quote. 

607 quote_pos += 1 

608 parts.append((pk, rest[scan_pos:quote_pos])) 

609 scan_pos = quote_pos 

610 break 

611 else: 

612 # Consume any other character. 

613 quote_pos += 1 

614 else: 

615 # Scanned all without finding closing quote. 

616 break 

617 

618 # Continue to the next delimited part, skipping spaces. 

619 if m_delim := _parameter_delimiter_re.search(rest, scan_pos): 

620 scan_pos = m_delim.end() 

621 else: 

622 break 

623 

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

625 continuations: dict[str, list[str]] = {} 

626 continuation_encodings: dict[str, str] = {} 

627 

628 # For each collected part, process optional charset and continuation, 

629 # unquote quoted values. 

630 for pk, pv in parts: 

631 encoding: str | None = None 

632 has_continuation = has_charset = False 

633 

634 # Check for and remove markers at end of key. 

635 if ( 

636 m_mark := _parameter_mark_re.search(pk) 

637 ) is not None and m_mark.end() - m_mark.start() > 0: 

638 has_continuation = m_mark.group(1) is not None 

639 has_charset = m_mark.group(2) is not None 

640 pk = pk[: m_mark.start()] 

641 

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

643 if has_charset: 

644 if (m_charset := _charset_value_re.match(pv)) is not None: 

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

646 encoding, pv = m_charset.groups() 

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

648 encoding = encoding.lower() 

649 

650 # Use the prior continuation encoding if not set on this part. 

651 if not encoding and has_continuation: 

652 encoding = continuation_encodings.get(pk) 

653 

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

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

656 # value quoted. 

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

658 # invalid bytes are replaced during unquoting 

659 pv = unquote(pv, encoding=encoding) 

660 else: 

661 # Remove quotes, replace header slash escapes, replace multipart 

662 # percent-encoded quotes. 

663 pv = unquote_header_value(pv).replace("%22", '"') 

664 

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

666 if has_continuation: 

667 # Remove prior normal option. 

668 if pk in options: 

669 del options[pk] 

670 

671 # For simplicity, this uses the scan order rather than enforcing 

672 # sequential numbering. 

673 if pk in continuations: 

674 continuations[pk].append(pv) 

675 else: 

676 continuations[pk] = [pv] 

677 

678 # Further parts don't require their own charset marker. 

679 if encoding: 

680 continuation_encodings[pk] = encoding 

681 else: 

682 # Remove prior continuation option. 

683 if pk in continuations: 

684 del continuations[pk] 

685 continuation_encodings.pop(pk, None) 

686 

687 options[pk] = pv 

688 

689 options.update({pk: "".join(pv) for pk, pv in continuations.items()}) 

690 return value, options 

691 

692 

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

694 

695 

696@t.overload 

697def _parse_accept_header(value: str | None) -> ds.Accept: ... 

698 

699 

700@t.overload 

701def _parse_accept_header(value: str | None, cls: type[_TAnyAccept]) -> _TAnyAccept: ... 

702 

703 

704def _parse_accept_header( 

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

706) -> _TAnyAccept: 

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

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

709 

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

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

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

713 

714 :param value: The header value to parse. 

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

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

717 

718 .. deprecated:: 3.2 

719 Will be removed in Werkzeug 4.0. Use the ``Accept.from_header`` method 

720 instead. 

721 

722 .. versionchanged:: 2.3 

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

724 """ 

725 if cls is None: 

726 cls = t.cast(type[_TAnyAccept], ds.Accept) 

727 

728 return cls.from_header(value) 

729 

730 

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

732 

733 

734@t.overload 

735def _parse_cache_control_header( 

736 value: str | None, 

737 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None, 

738) -> ds.RequestCacheControl: ... 

739 

740 

741@t.overload 

742def _parse_cache_control_header( 

743 value: str | None, 

744 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None, 

745 cls: type[_TAnyCC] = ..., 

746) -> _TAnyCC: ... 

747 

748 

749def _parse_cache_control_header( 

750 value: str | None, 

751 on_update: t.Callable[[ds.cache_control._CacheControl], None] | None = None, 

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

753) -> _TAnyCC: 

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

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

756 to not use the wrong control statements. 

757 

758 .. deprecated:: 3.2 

759 Will be removed in Werkzeug 4.0. Use the ``CacheControl.from_header`` 

760 method instead. 

761 

762 .. versionadded:: 0.5 

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

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

765 

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

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

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

769 object is changed. 

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

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

772 :return: a `cls` object. 

773 """ 

774 if cls is None: 

775 cls = t.cast(type[_TAnyCC], ds.RequestCacheControl) 

776 

777 obj = cls.from_header(value) 

778 

779 if hasattr(obj, "_on_update"): 

780 obj._on_update = on_update 

781 

782 return obj 

783 

784 

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

786 

787 

788@t.overload 

789def _parse_csp_header( 

790 value: str | None, 

791 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None, 

792) -> ds.ContentSecurityPolicy: ... 

793 

794 

795@t.overload 

796def _parse_csp_header( 

797 value: str | None, 

798 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None, 

799 cls: type[_TAnyCSP] = ..., 

800) -> _TAnyCSP: ... 

801 

802 

803def _parse_csp_header( 

804 value: str | None, 

805 on_update: t.Callable[[ds.ContentSecurityPolicy], None] | None = None, 

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

807) -> _TAnyCSP: 

808 """Parse a Content Security Policy header. 

809 

810 .. deprecated:: 3.2 

811 Will be removed in Werkzeug 4.0. Use the 

812 ``ContentSecurityPolicy.from_header`` method instead. 

813 

814 .. versionadded:: 1.0 

815 Support for Content Security Policy headers was added. 

816 

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

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

819 on the object is changed. 

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

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

822 :return: a `cls` object. 

823 """ 

824 if cls is None: 

825 cls = t.cast(type[_TAnyCSP], ds.ContentSecurityPolicy) 

826 

827 obj = cls.from_header(value) 

828 obj.on_update = on_update 

829 return obj 

830 

831 

832def _parse_set_header( 

833 value: str | None, 

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

835) -> ds.HeaderSet: 

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

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

838 

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

840 

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

842 and keeps the order of the items: 

843 

844 >>> 'TOKEN' in hs 

845 True 

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

847 1 

848 >>> hs 

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

850 

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

852 :func:`dump_header` function. 

853 

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

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

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

857 object is changed. 

858 

859 .. deprecated:: 3.2 

860 Will be removed in Werkzeug 4.0. Use the ``HeaderSet.from_header`` 

861 method instead. 

862 """ 

863 obj = ds.HeaderSet.from_header(value) 

864 obj._on_update = on_update 

865 return obj 

866 

867 

868def _parse_if_range_header(value: str | None) -> ds.IfRange: 

869 """Parses an if-range header which can be an ETag or a date. Returns 

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

871 

872 .. deprecated:: 3.2 

873 Will be removed in Werkzeug 4.0. Use the ``IfRange.from_header`` 

874 method instead. 

875 

876 .. versionchanged:: 2.0 

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

878 

879 .. versionadded:: 0.7 

880 """ 

881 return ds.IfRange.from_header(value) 

882 

883 

884def _parse_range_header( 

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

886) -> ds.Range | None: 

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

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

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

890 non-inclusive. 

891 

892 .. deprecated:: 3.2 

893 Will be removed in Werkzeug 4.0. Use the ``Range.from_header`` 

894 method instead. 

895 

896 .. versionadded:: 0.7 

897 """ 

898 return ds.Range.from_header(value) 

899 

900 

901def _parse_content_range_header( 

902 value: str | None, 

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

904) -> ds.ContentRange | None: 

905 """Parses a range header into a 

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

907 parsing is not possible. 

908 

909 .. deprecated:: 3.2 

910 Will be removed in Werkzeug 4.0. Use the ``ContentRange.from_header`` 

911 method instead. 

912 

913 .. versionadded:: 0.7 

914 

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

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

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

918 object is changed. 

919 """ 

920 if not (obj := ds.ContentRange.from_header(value)): 

921 return None 

922 

923 obj._on_update = on_update 

924 return obj 

925 

926 

927def _quote_etag(etag: str, weak: bool = False) -> str: 

928 """Quote an ETag value. 

929 

930 :param etag: The ETag to quote. 

931 :param weak: Add a weak marker to the quoted value. 

932 

933 .. deprecated:: 3.2 

934 Will be removed in Werkzeug 4.0. Use ``ETag.to_header`` instead. 

935 """ 

936 return ds.ETag(etag, weak).to_header() 

937 

938 

939def _unquote_etag(etag: str | None) -> tuple[str, bool] | tuple[None, None]: 

940 """Parse a valid single ETag. A valid ETag must be quoted and may have an 

941 optional weak ``W/`` prefix. 

942 

943 .. code-block:: pycon 

944 

945 >>> unquote_etag('"strong"') 

946 ('strong', False) 

947 >>> unquote_etag('W/"weak"') 

948 ('weak', True) 

949 >>> unquote_etag('no_quotes') 

950 (None, None) 

951 

952 Use :meth:`.ETags.from_header` to parse a list of ETag values. 

953 

954 :param etag: A valid ETag to unquote. 

955 :return: A tuple ``(value, weak)``, or ``(None, None)`` if the 

956 value is empty or invalid. 

957 

958 .. deprecated:: 3.2 

959 Will be removed in Werkzeug 4.0. Use ``ETag.from_header`` instead. 

960 

961 .. versionchanged:: 3.2 

962 Does not accept invalid unquoted values. 

963 """ 

964 if (result := ds.ETag.from_header(etag)) is None: 

965 return None, None 

966 

967 return result.value, result.weak 

968 

969 

970def _parse_etags(value: str | None) -> ds.ETagSet: 

971 """Parse an ETag header. 

972 

973 :param value: the tag header to parse 

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

975 

976 .. deprecated:: 3.2 

977 Will be removed in Werkzeug 4.0. Use the 'ETagSet.from_header' method instead. 

978 

979 .. versionchanged:: 3.2 

980 Does not accept invalid unquoted values. 

981 """ 

982 return ds.ETagSet.from_header(value) 

983 

984 

985def _generate_etag(data: bytes) -> str: 

986 """Generate a strong ETag value by hashing the given data. 

987 

988 .. deprecated:: 3.2 

989 Will be removed in Werkzeug 4.0. Use ``Response.add_etag`` instead. 

990 

991 .. versionchanged:: 3.2 

992 Use SHA3-256. SHA-1 is not allowed in FIPS-enabled systems. Use base64 

993 instead of hex digest. This increases the length from 40 to 43 

994 characters. 

995 

996 .. versionchanged:: 2.0 

997 Use SHA-1. MD5 is not allowed in FIPS-enabled systems. This increases 

998 the length from 32 to 40 characters. 

999 """ 

1000 digest = hashlib.sha3_256(data, usedforsecurity=False).digest() 

1001 return b64encode(digest).decode().rstrip("=") 

1002 

1003 

1004def parse_date(value: str | None) -> dt.datetime | None: 

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

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

1007 

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

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

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

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

1012 

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

1014 

1015 .. versionchanged:: 2.0 

1016 Return a timezone-aware datetime object. Use 

1017 ``email.utils.parsedate_to_datetime``. 

1018 """ 

1019 if value is None: 

1020 return None 

1021 

1022 try: 

1023 parsed = email.utils.parsedate_to_datetime(value) 

1024 except (TypeError, ValueError): 

1025 return None 

1026 

1027 if parsed.tzinfo is None: 

1028 return parsed.replace(tzinfo=dt.UTC) 

1029 

1030 return parsed 

1031 

1032 

1033def http_date( 

1034 timestamp: dt.datetime | dt.date | int | float | struct_time | None = None, 

1035) -> str: 

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

1037 string. 

1038 

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

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

1041 exception. 

1042 

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

1044 the current time. 

1045 

1046 .. versionchanged:: 2.0 

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

1048 """ 

1049 if isinstance(timestamp, dt.date): 

1050 if not isinstance(timestamp, dt.datetime): 

1051 # Assume plain date is midnight UTC. 

1052 timestamp = dt.datetime.combine(timestamp, dt.time(), tzinfo=dt.UTC) 

1053 else: 

1054 # Ensure datetime is timezone-aware. 

1055 timestamp = _dt_as_utc(timestamp) 

1056 

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

1058 

1059 if isinstance(timestamp, struct_time): 

1060 timestamp = mktime(timestamp) 

1061 

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

1063 

1064 

1065def _load_age(value: str) -> dt.timedelta | None: 

1066 try: 

1067 seconds = _plain_int(value) 

1068 except ValueError: 

1069 return None 

1070 

1071 if seconds < 0: 

1072 return None 

1073 

1074 try: 

1075 return dt.timedelta(seconds=seconds) 

1076 except OverflowError: 

1077 return None 

1078 

1079 

1080def _dump_age(value: dt.timedelta | int) -> str: 

1081 if isinstance(value, dt.timedelta): 

1082 value = int(value.total_seconds()) 

1083 

1084 if value < 0: 

1085 raise ValueError("cannot be negative") 

1086 

1087 return str(value) 

1088 

1089 

1090def _load_retry_after(value: str) -> dt.datetime | None: 

1091 try: 

1092 seconds = _plain_int(value) 

1093 except ValueError: 

1094 return parse_date(value) 

1095 

1096 if seconds < 0: 

1097 return None 

1098 

1099 try: 

1100 delta = dt.timedelta(seconds) 

1101 except OverflowError: 

1102 return None 

1103 

1104 return dt.datetime.now(dt.UTC) + delta 

1105 

1106 

1107def _dump_retry_after(value: dt.datetime | int) -> str: 

1108 if isinstance(value, dt.datetime): 

1109 return http_date(value) 

1110 

1111 if value < 0: 

1112 raise ValueError("cannot be negative") 

1113 

1114 return str(value) 

1115 

1116 

1117def _is_resource_modified( 

1118 environ: wt.WSGIEnvironment, 

1119 etag: str | None = None, 

1120 data: bytes | None = None, 

1121 last_modified: dt.datetime | str | None = None, 

1122 ignore_if_range: bool = True, 

1123) -> bool: 

1124 """Convenience method for conditional requests. 

1125 

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

1127 :param etag: the ETag for the response for comparison. 

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

1129 generate an ETag value using :func:`generate_etag`. 

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

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

1132 account. 

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

1134 

1135 .. deprecated:: 3.2 

1136 Will be removed in Werkzeug 4.0. ``Response.apply_conditions`` and 

1137 ``apply_range`` perform their own checks. 

1138 

1139 .. versionchanged:: 1.0 

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

1141 """ 

1142 return _sansio_http._is_resource_modified( 

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

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

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

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

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

1148 etag=etag, 

1149 data=data, 

1150 last_modified=last_modified, 

1151 ignore_if_range=ignore_if_range, 

1152 ) 

1153 

1154 

1155def _remove_entity_headers( 

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

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

1158) -> None: 

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

1160 operation works in-place. 

1161 

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

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

1164 they are entity headers. 

1165 

1166 .. deprecated:: 3.2 

1167 Will be removed in Werkzeug 4.0. Use ``Response`` instead. 

1168 

1169 .. versionchanged:: 3.2 

1170 The list of headers is updated to RFC 9110. 

1171 

1172 .. versionchanged:: 0.5 

1173 The ``allowed`` parameter was added. 

1174 

1175 """ 

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

1177 headers[:] = [ 

1178 (key, value) 

1179 for key, value in headers 

1180 if not _is_entity_header(key) or key.lower() in allowed 

1181 ] 

1182 

1183 

1184def _remove_hop_by_hop_headers( 

1185 headers: ds.Headers | list[tuple[str, str]], connection: ds.HeaderSet | None = None 

1186) -> None: 

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

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

1189 

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

1191 :param connection: The parsed ``Connection`` header. 

1192 

1193 .. deprecated:: 3.2 

1194 Will be removed in Werkzeug 4.0. Use ``is_hop_by_hop_header`` to filter 

1195 while building a new ``Headers`` object. 

1196 

1197 .. versionchanged:: 3.2 

1198 Accepts a parsed ``Connection`` header to check against. The list of 

1199 headers to always remove is updated to RFC 9110. 

1200 

1201 .. versionadded:: 0.5 

1202 """ 

1203 headers[:] = [ 

1204 (key, value) 

1205 for key, value in headers 

1206 if not is_hop_by_hop_header(key, connection) 

1207 ] 

1208 

1209 

1210def _is_entity_header(header: str) -> bool: 

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

1212 

1213 This is defined in https://httpwg.org/specs/rfc9110.html#status.304 and 

1214 https://httpwg.org/specs/rfc9110.html#representation.metadata. 

1215 

1216 :param header: The header key to check. 

1217 

1218 .. deprecated:: 3.2 

1219 Will be removed in Werkzeug 4.0. Use ``Response`` instead. 

1220 

1221 .. versionchanged:: 3.2 

1222 The list of headers is updated to RFC 9110. 

1223 

1224 .. versionadded:: 0.5 

1225 """ 

1226 return header.lower() in { 

1227 "content-type", 

1228 "content-encoding", 

1229 "content-language", 

1230 "content-length", 

1231 } 

1232 

1233 

1234_hop_by_hop_headers = frozenset( 

1235 ( 

1236 "connection", 

1237 "proxy-connection", 

1238 "keep-alive", 

1239 "te", 

1240 "transfer-encoding", 

1241 "upgrade", 

1242 ) 

1243) 

1244 

1245 

1246def is_hop_by_hop_header(key: str, connection: ds.HeaderSet | None = None) -> bool: 

1247 """Check if a header should be removed by a proxy. 

1248 

1249 This is defined in https://httpwg.org/specs/rfc9110.html#field.connection. 

1250 All headers to remove should be listed in the ``Connection`` header. In case 

1251 it's not given, there is a set of headers that is always removed. 

1252 

1253 :param key: The header key to check. 

1254 :param connection: The parsed ``Connection`` header. 

1255 

1256 .. versionchanged:: 3.2 

1257 Accepts a parsed ``Connection`` header to check against. The list of 

1258 headers to always remove is updated to RFC 9110. 

1259 

1260 .. versionadded:: 0.5 

1261 """ 

1262 key = key.lower() 

1263 return key in _hop_by_hop_headers or (connection is not None and key in connection) 

1264 

1265 

1266def parse_cookie( 

1267 header: wt.WSGIEnvironment | str | None, **kwargs: t.Any 

1268) -> ds.ImmutableMultiDict[str, str]: 

1269 """Parse cookies from a ``Cookie`` header as an :class:`.ImmutableMultiDict`. 

1270 

1271 :param header: The ``Cookie`` header, or a WSGI environ with an 

1272 ``HTTP_COOKIE`` key. 

1273 

1274 .. versionchanged:: 3.2 

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

1276 It will always be ``ImmutableMultiDict``. 

1277 

1278 .. versionchanged:: 3.0 

1279 Passing bytes, and the ``charset`` and ``errors`` parameters, were removed. 

1280 

1281 .. versionchanged:: 1.0 

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

1283 

1284 .. versionchanged:: 0.5 

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

1286 parameter was added. 

1287 """ 

1288 if isinstance(header, dict): 

1289 cookie = header.get("HTTP_COOKIE") 

1290 else: 

1291 cookie = header 

1292 

1293 if cookie: 

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

1295 

1296 parse_kwargs: dict[str, t.Any] = {} 

1297 

1298 if "cls" in kwargs: 

1299 import warnings 

1300 

1301 warnings.warn( 

1302 "The 'cls' parameter is deprecated and will be removed in Werkzeug" 

1303 " 4.0. It will always be 'ImmutableMultiDict'.", 

1304 DeprecationWarning, 

1305 stacklevel=2, 

1306 ) 

1307 parse_kwargs["cls"] = kwargs["cls"] 

1308 

1309 return _sansio_http.parse_cookie(cookie=cookie, **kwargs) 

1310 

1311 

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

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

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

1315_cookie_slash_map.update( 

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

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

1318) 

1319 

1320 

1321def dump_cookie( 

1322 key: str, 

1323 value: str = "", 

1324 max_age: dt.timedelta | int | None = None, 

1325 expires: str | dt.datetime | int | float | None = None, 

1326 path: str | None = "/", 

1327 domain: str | None = None, 

1328 secure: bool = False, 

1329 httponly: bool = False, 

1330 sync_expires: bool = True, 

1331 max_size: int = 4093, 

1332 samesite: str | None = None, 

1333 partitioned: bool = False, 

1334) -> str: 

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

1336 

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

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

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

1340 

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

1342 characters. This is technically against the specification but 

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

1344 non-ASCII values for the keys. 

1345 

1346 :param key: The cookie key. This is assumed to be trusted and valid, it 

1347 must not come from untrusted user input. 

1348 :param value: The cookie value. It will be quoted if it contains characters 

1349 not allowed by RFC 6265; ``\123`` octal escapes are used to encode 

1350 non-ASCII bytes. 

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

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

1353 browser session. Additionally `timedelta` objects 

1354 are accepted, too. 

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

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

1357 span the whole domain. 

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

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

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

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

1362 be readable by the domain that set it. 

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

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

1365 extension to the cookie standard and probably not 

1366 supported by all browsers. 

1367 :param charset: the encoding for string values. 

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

1369 but expires not. 

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

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

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

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

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

1375 :param partitioned: Opts the cookie into partitioned storage. This 

1376 will also set secure to True 

1377 

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

1379 

1380 .. versionchanged:: 3.1 

1381 The ``partitioned`` parameter was added. 

1382 

1383 .. versionchanged:: 3.0 

1384 Passing bytes, and the ``charset`` parameter, were removed. 

1385 

1386 .. versionchanged:: 2.3.3 

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

1388 

1389 .. versionchanged:: 2.3.1 

1390 The value allows more characters without quoting. 

1391 

1392 .. versionchanged:: 2.3 

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

1394 leading dot is ignored. 

1395 

1396 .. versionchanged:: 2.3 

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

1398 

1399 .. versionchanged:: 1.0.0 

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

1401 """ 

1402 if path is not None: 

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

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

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

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

1407 

1408 if domain: 

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

1410 

1411 if isinstance(max_age, dt.timedelta): 

1412 max_age = int(max_age.total_seconds()) 

1413 

1414 if expires is not None: 

1415 if not isinstance(expires, str): 

1416 expires = http_date(expires) 

1417 elif max_age is not None and sync_expires: 

1418 expires = http_date(dt.datetime.now(tz=dt.UTC).timestamp() + max_age) 

1419 

1420 if samesite is not None: 

1421 samesite = samesite.title() 

1422 

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

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

1425 

1426 if partitioned: 

1427 secure = True 

1428 

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

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

1431 if not _cookie_no_quote_re.fullmatch(value): 

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

1433 value = _cookie_slash_re.sub( 

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

1435 ).decode("ascii") 

1436 value = f'"{value}"' 

1437 

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

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

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

1441 

1442 for k, v in ( 

1443 ("Domain", domain), 

1444 ("Expires", expires), 

1445 ("Max-Age", max_age), 

1446 ("Secure", secure), 

1447 ("HttpOnly", httponly), 

1448 ("Path", path), 

1449 ("SameSite", samesite), 

1450 ("Partitioned", partitioned), 

1451 ): 

1452 if v is None or v is False: 

1453 continue 

1454 

1455 if v is True: 

1456 buf.append(k) 

1457 continue 

1458 

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

1460 

1461 rv = "; ".join(buf) 

1462 

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

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

1465 # to debug. 

1466 cookie_size = len(rv) 

1467 

1468 if max_size and cookie_size > max_size: 

1469 value_size = len(value) 

1470 warnings.warn( 

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

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

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

1474 " silently ignore cookies larger than this.", 

1475 stacklevel=2, 

1476 ) 

1477 

1478 return rv 

1479 

1480 

1481def _is_byte_range_valid( 

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

1483) -> bool: 

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

1485 

1486 .. deprecated:: 3.2 

1487 Will be removed in Werkzeug 4.0. ``Range.from_header``, 

1488 ``Range.make_content_range``, and ``ContentRange.from_header`` validate 

1489 their values. 

1490 

1491 .. versionadded:: 0.7 

1492 """ 

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

1494 return False 

1495 elif start is None: 

1496 return length is None or length >= 0 

1497 elif length is None: 

1498 return 0 <= start < stop # type: ignore[operator] 

1499 elif start >= stop: # type: ignore[operator] 

1500 return False 

1501 return 0 <= start < length 

1502 

1503 

1504# circular dependencies 

1505from . import datastructures as ds # noqa: E402 

1506from .sansio import http as _sansio_http # noqa: E402 

1507 

1508if not t.TYPE_CHECKING: 

1509 

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

1511 import warnings 

1512 

1513 if name == "HTTP_STATUS_CODES": 

1514 warnings.warn( 

1515 "The 'HTTP_STATUS_CODES' data is deprecated and will be removed in" 

1516 " Werkzeug 4.0. Use Python's built-in 'http.HTTPStatus' instead.", 

1517 DeprecationWarning, 

1518 stacklevel=2, 

1519 ) 

1520 return _HTTP_STATUS_CODES 

1521 

1522 if name == "is_resource_modified": 

1523 warnings.warn( 

1524 "The 'is_resource_modified' function is deprecated and will be removed" 

1525 " in Werkzeug 4.0. 'Response.apply_conditions' and" 

1526 " 'Response.apply_range' perform their own checks.", 

1527 DeprecationWarning, 

1528 stacklevel=2, 

1529 ) 

1530 return _is_resource_modified 

1531 

1532 if name == "is_byte_range_valid": 

1533 warnings.warn( 

1534 "The 'is_byte_range_valid' function is deprecated and will be" 

1535 " removed in Werkzeug 4.0. 'Range.from_header'," 

1536 " 'Range.make_content_range', and 'ContentRange.from_header'" 

1537 " validate their values.", 

1538 DeprecationWarning, 

1539 stacklevel=2, 

1540 ) 

1541 return _is_byte_range_valid 

1542 

1543 if name == "generate_etag": 

1544 warnings.warn( 

1545 "The 'generate_etag' function is deprecated and will be removed in" 

1546 " Werkzeug 4.0. Use 'Response.add_etag' instead.", 

1547 DeprecationWarning, 

1548 stacklevel=2, 

1549 ) 

1550 return _generate_etag 

1551 

1552 if name == "remove_hop_by_hop_headers": 

1553 warnings.warn( 

1554 "The 'remove_hop_by_hop_headers' function is deprecated and will be" 

1555 " removed in Werkzeug 4.0. Use 'is_hop_by_hop_header' to filter while" 

1556 " building a new 'Headers' object.", 

1557 DeprecationWarning, 

1558 stacklevel=2, 

1559 ) 

1560 return _remove_hop_by_hop_headers 

1561 

1562 if name == "remove_entity_headers": 

1563 warnings.warn( 

1564 "The 'remove_entity_headers' function is deprecated and will be removed" 

1565 " in Werkzeug 4.0. Use 'Response' instead.", 

1566 DeprecationWarning, 

1567 stacklevel=2, 

1568 ) 

1569 return _remove_entity_headers 

1570 

1571 if name == "is_entity_header": 

1572 warnings.warn( 

1573 "The 'is_entity_header' function is deprecated and will be removed in" 

1574 " Werkzeug 4.0. Use 'Response' instead.", 

1575 DeprecationWarning, 

1576 stacklevel=2, 

1577 ) 

1578 return _is_entity_header 

1579 

1580 alts = { 

1581 "dump_csp_header": "ContentSecurityPolicy.to_header", 

1582 "parse_accept_header": "Accept.from_header", 

1583 "parse_cache_control_header": "CacheControl.from_header", 

1584 "parse_content_range_header": "ContentRange.from_header", 

1585 "parse_csp_header": "ContentSecurityPolicy.from_header", 

1586 "parse_etags": "ETagSet.from_header", 

1587 "parse_if_range_header": "IfRange.from_header", 

1588 "parse_range_header": "Range.from_header", 

1589 "parse_set_header": "HeaderSet.from_header", 

1590 "unquote_etag": "ETag.from_header", 

1591 "quote_etag": "ETag.to_header", 

1592 } 

1593 

1594 if name in alts: 

1595 warnings.warn( 

1596 f"The '{name}' function is deprecated and will be removed in" 

1597 f" Werkzeug 4.0. Use the '{alts[name]}' method instead.", 

1598 DeprecationWarning, 

1599 stacklevel=2, 

1600 ) 

1601 return globals()[f"_{name}"] 

1602 

1603 raise AttributeError(name)