Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/sansio/response.py: 53%

276 statements  

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

1from __future__ import annotations 

2 

3import typing as t 

4import warnings 

5from datetime import datetime 

6from datetime import timedelta 

7from datetime import timezone 

8from http import HTTPStatus 

9 

10from ..datastructures import Headers 

11from ..datastructures import HeaderSet 

12from ..http import dump_cookie 

13from ..http import HTTP_STATUS_CODES 

14from ..utils import get_content_type 

15from werkzeug.datastructures import CallbackDict 

16from werkzeug.datastructures import ContentRange 

17from werkzeug.datastructures import ContentSecurityPolicy 

18from werkzeug.datastructures import ResponseCacheControl 

19from werkzeug.datastructures import WWWAuthenticate 

20from werkzeug.http import COEP 

21from werkzeug.http import COOP 

22from werkzeug.http import dump_age 

23from werkzeug.http import dump_header 

24from werkzeug.http import dump_options_header 

25from werkzeug.http import http_date 

26from werkzeug.http import parse_age 

27from werkzeug.http import parse_cache_control_header 

28from werkzeug.http import parse_content_range_header 

29from werkzeug.http import parse_csp_header 

30from werkzeug.http import parse_date 

31from werkzeug.http import parse_options_header 

32from werkzeug.http import parse_set_header 

33from werkzeug.http import quote_etag 

34from werkzeug.http import unquote_etag 

35from werkzeug.utils import header_property 

36 

37 

38def _set_property(name: str, doc: str | None = None) -> property: 

39 def fget(self: Response) -> HeaderSet: 

40 def on_update(header_set: HeaderSet) -> None: 

41 if not header_set and name in self.headers: 

42 del self.headers[name] 

43 elif header_set: 

44 self.headers[name] = header_set.to_header() 

45 

46 return parse_set_header(self.headers.get(name), on_update) 

47 

48 def fset( 

49 self: Response, 

50 value: None | (str | dict[str, str | int] | t.Iterable[str]), 

51 ) -> None: 

52 if not value: 

53 del self.headers[name] 

54 elif isinstance(value, str): 

55 self.headers[name] = value 

56 else: 

57 self.headers[name] = dump_header(value) 

58 

59 return property(fget, fset, doc=doc) 

60 

61 

62class Response: 

63 """Represents the non-IO parts of an HTTP response, specifically the 

64 status and headers but not the body. 

65 

66 This class is not meant for general use. It should only be used when 

67 implementing WSGI, ASGI, or another HTTP application spec. Werkzeug 

68 provides a WSGI implementation at :cls:`werkzeug.wrappers.Response`. 

69 

70 :param status: The status code for the response. Either an int, in 

71 which case the default status message is added, or a string in 

72 the form ``{code} {message}``, like ``404 Not Found``. Defaults 

73 to 200. 

74 :param headers: A :class:`~werkzeug.datastructures.Headers` object, 

75 or a list of ``(key, value)`` tuples that will be converted to a 

76 ``Headers`` object. 

77 :param mimetype: The mime type (content type without charset or 

78 other parameters) of the response. If the value starts with 

79 ``text/`` (or matches some other special cases), the charset 

80 will be added to create the ``content_type``. 

81 :param content_type: The full content type of the response. 

82 Overrides building the value from ``mimetype``. 

83 

84 .. versionadded:: 2.0 

85 """ 

86 

87 _charset: str 

88 

89 @property 

90 def charset(self) -> str: 

91 """The charset used to encode body and cookie data. Defaults to UTF-8. 

92 

93 .. deprecated:: 2.3 

94 Will be removed in Werkzeug 3.0. Response data must always be UTF-8. 

95 """ 

96 warnings.warn( 

97 "The 'charset' attribute is deprecated and will not be used in Werkzeug" 

98 " 2.4. Text in body and cookie data will always use UTF-8.", 

99 DeprecationWarning, 

100 stacklevel=2, 

101 ) 

102 return self._charset 

103 

104 @charset.setter 

105 def charset(self, value: str) -> None: 

106 warnings.warn( 

107 "The 'charset' attribute is deprecated and will not be used in Werkzeug" 

108 " 2.4. Text in body and cookie data will always use UTF-8.", 

109 DeprecationWarning, 

110 stacklevel=2, 

111 ) 

112 self._charset = value 

113 

114 #: the default status if none is provided. 

115 default_status = 200 

116 

117 #: the default mimetype if none is provided. 

118 default_mimetype: str | None = "text/plain" 

119 

120 #: Warn if a cookie header exceeds this size. The default, 4093, should be 

121 #: safely `supported by most browsers <cookie_>`_. A cookie larger than 

122 #: this size will still be sent, but it may be ignored or handled 

123 #: incorrectly by some browsers. Set to 0 to disable this check. 

124 #: 

125 #: .. versionadded:: 0.13 

126 #: 

127 #: .. _`cookie`: http://browsercookielimits.squawky.net/ 

128 max_cookie_size = 4093 

129 

130 # A :class:`Headers` object representing the response headers. 

131 headers: Headers 

132 

133 def __init__( 

134 self, 

135 status: int | str | HTTPStatus | None = None, 

136 headers: t.Mapping[str, str | t.Iterable[str]] 

137 | t.Iterable[tuple[str, str]] 

138 | None = None, 

139 mimetype: str | None = None, 

140 content_type: str | None = None, 

141 ) -> None: 

142 if not isinstance(type(self).charset, property): 

143 warnings.warn( 

144 "The 'charset' attribute is deprecated and will not be used in Werkzeug" 

145 " 2.4. Text in body and cookie data will always use UTF-8.", 

146 DeprecationWarning, 

147 stacklevel=2, 

148 ) 

149 self._charset = self.charset 

150 else: 

151 self._charset = "utf-8" 

152 

153 if isinstance(headers, Headers): 

154 self.headers = headers 

155 elif not headers: 

156 self.headers = Headers() 

157 else: 

158 self.headers = Headers(headers) 

159 

160 if content_type is None: 

161 if mimetype is None and "content-type" not in self.headers: 

162 mimetype = self.default_mimetype 

163 if mimetype is not None: 

164 mimetype = get_content_type(mimetype, self._charset) 

165 content_type = mimetype 

166 if content_type is not None: 

167 self.headers["Content-Type"] = content_type 

168 if status is None: 

169 status = self.default_status 

170 self.status = status # type: ignore 

171 

172 def __repr__(self) -> str: 

173 return f"<{type(self).__name__} [{self.status}]>" 

174 

175 @property 

176 def status_code(self) -> int: 

177 """The HTTP status code as a number.""" 

178 return self._status_code 

179 

180 @status_code.setter 

181 def status_code(self, code: int) -> None: 

182 self.status = code # type: ignore 

183 

184 @property 

185 def status(self) -> str: 

186 """The HTTP status code as a string.""" 

187 return self._status 

188 

189 @status.setter 

190 def status(self, value: str | int | HTTPStatus) -> None: 

191 self._status, self._status_code = self._clean_status(value) 

192 

193 def _clean_status(self, value: str | int | HTTPStatus) -> tuple[str, int]: 

194 if isinstance(value, (int, HTTPStatus)): 

195 status_code = int(value) 

196 else: 

197 value = value.strip() 

198 

199 if not value: 

200 raise ValueError("Empty status argument") 

201 

202 code_str, sep, _ = value.partition(" ") 

203 

204 try: 

205 status_code = int(code_str) 

206 except ValueError: 

207 # only message 

208 return f"0 {value}", 0 

209 

210 if sep: 

211 # code and message 

212 return value, status_code 

213 

214 # only code, look up message 

215 try: 

216 status = f"{status_code} {HTTP_STATUS_CODES[status_code].upper()}" 

217 except KeyError: 

218 status = f"{status_code} UNKNOWN" 

219 

220 return status, status_code 

221 

222 def set_cookie( 

223 self, 

224 key: str, 

225 value: str = "", 

226 max_age: timedelta | int | None = None, 

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

228 path: str | None = "/", 

229 domain: str | None = None, 

230 secure: bool = False, 

231 httponly: bool = False, 

232 samesite: str | None = None, 

233 ) -> None: 

234 """Sets a cookie. 

235 

236 A warning is raised if the size of the cookie header exceeds 

237 :attr:`max_cookie_size`, but the header will still be set. 

238 

239 :param key: the key (name) of the cookie to be set. 

240 :param value: the value of the cookie. 

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

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

243 browser session. 

244 :param expires: should be a `datetime` object or UNIX timestamp. 

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

246 span the whole domain. 

247 :param domain: if you want to set a cross-domain cookie. For example, 

248 ``domain=".example.com"`` will set a cookie that is 

249 readable by the domain ``www.example.com``, 

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

251 be readable by the domain that set it. 

252 :param secure: If ``True``, the cookie will only be available 

253 via HTTPS. 

254 :param httponly: Disallow JavaScript access to the cookie. 

255 :param samesite: Limit the scope of the cookie to only be 

256 attached to requests that are "same-site". 

257 """ 

258 charset = self._charset if self._charset != "utf-8" else None 

259 self.headers.add( 

260 "Set-Cookie", 

261 dump_cookie( 

262 key, 

263 value=value, 

264 max_age=max_age, 

265 expires=expires, 

266 path=path, 

267 domain=domain, 

268 secure=secure, 

269 httponly=httponly, 

270 charset=charset, 

271 max_size=self.max_cookie_size, 

272 samesite=samesite, 

273 ), 

274 ) 

275 

276 def delete_cookie( 

277 self, 

278 key: str, 

279 path: str | None = "/", 

280 domain: str | None = None, 

281 secure: bool = False, 

282 httponly: bool = False, 

283 samesite: str | None = None, 

284 ) -> None: 

285 """Delete a cookie. Fails silently if key doesn't exist. 

286 

287 :param key: the key (name) of the cookie to be deleted. 

288 :param path: if the cookie that should be deleted was limited to a 

289 path, the path has to be defined here. 

290 :param domain: if the cookie that should be deleted was limited to a 

291 domain, that domain has to be defined here. 

292 :param secure: If ``True``, the cookie will only be available 

293 via HTTPS. 

294 :param httponly: Disallow JavaScript access to the cookie. 

295 :param samesite: Limit the scope of the cookie to only be 

296 attached to requests that are "same-site". 

297 """ 

298 self.set_cookie( 

299 key, 

300 expires=0, 

301 max_age=0, 

302 path=path, 

303 domain=domain, 

304 secure=secure, 

305 httponly=httponly, 

306 samesite=samesite, 

307 ) 

308 

309 @property 

310 def is_json(self) -> bool: 

311 """Check if the mimetype indicates JSON data, either 

312 :mimetype:`application/json` or :mimetype:`application/*+json`. 

313 """ 

314 mt = self.mimetype 

315 return mt is not None and ( 

316 mt == "application/json" 

317 or mt.startswith("application/") 

318 and mt.endswith("+json") 

319 ) 

320 

321 # Common Descriptors 

322 

323 @property 

324 def mimetype(self) -> str | None: 

325 """The mimetype (content type without charset etc.)""" 

326 ct = self.headers.get("content-type") 

327 

328 if ct: 

329 return ct.split(";")[0].strip() 

330 else: 

331 return None 

332 

333 @mimetype.setter 

334 def mimetype(self, value: str) -> None: 

335 self.headers["Content-Type"] = get_content_type(value, self._charset) 

336 

337 @property 

338 def mimetype_params(self) -> dict[str, str]: 

339 """The mimetype parameters as dict. For example if the 

340 content type is ``text/html; charset=utf-8`` the params would be 

341 ``{'charset': 'utf-8'}``. 

342 

343 .. versionadded:: 0.5 

344 """ 

345 

346 def on_update(d: CallbackDict) -> None: 

347 self.headers["Content-Type"] = dump_options_header(self.mimetype, d) 

348 

349 d = parse_options_header(self.headers.get("content-type", ""))[1] 

350 return CallbackDict(d, on_update) 

351 

352 location = header_property[str]( 

353 "Location", 

354 doc="""The Location response-header field is used to redirect 

355 the recipient to a location other than the Request-URI for 

356 completion of the request or identification of a new 

357 resource.""", 

358 ) 

359 age = header_property( 

360 "Age", 

361 None, 

362 parse_age, 

363 dump_age, # type: ignore 

364 doc="""The Age response-header field conveys the sender's 

365 estimate of the amount of time since the response (or its 

366 revalidation) was generated at the origin server. 

367 

368 Age values are non-negative decimal integers, representing time 

369 in seconds.""", 

370 ) 

371 content_type = header_property[str]( 

372 "Content-Type", 

373 doc="""The Content-Type entity-header field indicates the media 

374 type of the entity-body sent to the recipient or, in the case of 

375 the HEAD method, the media type that would have been sent had 

376 the request been a GET.""", 

377 ) 

378 content_length = header_property( 

379 "Content-Length", 

380 None, 

381 int, 

382 str, 

383 doc="""The Content-Length entity-header field indicates the size 

384 of the entity-body, in decimal number of OCTETs, sent to the 

385 recipient or, in the case of the HEAD method, the size of the 

386 entity-body that would have been sent had the request been a 

387 GET.""", 

388 ) 

389 content_location = header_property[str]( 

390 "Content-Location", 

391 doc="""The Content-Location entity-header field MAY be used to 

392 supply the resource location for the entity enclosed in the 

393 message when that entity is accessible from a location separate 

394 from the requested resource's URI.""", 

395 ) 

396 content_encoding = header_property[str]( 

397 "Content-Encoding", 

398 doc="""The Content-Encoding entity-header field is used as a 

399 modifier to the media-type. When present, its value indicates 

400 what additional content codings have been applied to the 

401 entity-body, and thus what decoding mechanisms must be applied 

402 in order to obtain the media-type referenced by the Content-Type 

403 header field.""", 

404 ) 

405 content_md5 = header_property[str]( 

406 "Content-MD5", 

407 doc="""The Content-MD5 entity-header field, as defined in 

408 RFC 1864, is an MD5 digest of the entity-body for the purpose of 

409 providing an end-to-end message integrity check (MIC) of the 

410 entity-body. (Note: a MIC is good for detecting accidental 

411 modification of the entity-body in transit, but is not proof 

412 against malicious attacks.)""", 

413 ) 

414 date = header_property( 

415 "Date", 

416 None, 

417 parse_date, 

418 http_date, 

419 doc="""The Date general-header field represents the date and 

420 time at which the message was originated, having the same 

421 semantics as orig-date in RFC 822. 

422 

423 .. versionchanged:: 2.0 

424 The datetime object is timezone-aware. 

425 """, 

426 ) 

427 expires = header_property( 

428 "Expires", 

429 None, 

430 parse_date, 

431 http_date, 

432 doc="""The Expires entity-header field gives the date/time after 

433 which the response is considered stale. A stale cache entry may 

434 not normally be returned by a cache. 

435 

436 .. versionchanged:: 2.0 

437 The datetime object is timezone-aware. 

438 """, 

439 ) 

440 last_modified = header_property( 

441 "Last-Modified", 

442 None, 

443 parse_date, 

444 http_date, 

445 doc="""The Last-Modified entity-header field indicates the date 

446 and time at which the origin server believes the variant was 

447 last modified. 

448 

449 .. versionchanged:: 2.0 

450 The datetime object is timezone-aware. 

451 """, 

452 ) 

453 

454 @property 

455 def retry_after(self) -> datetime | None: 

456 """The Retry-After response-header field can be used with a 

457 503 (Service Unavailable) response to indicate how long the 

458 service is expected to be unavailable to the requesting client. 

459 

460 Time in seconds until expiration or date. 

461 

462 .. versionchanged:: 2.0 

463 The datetime object is timezone-aware. 

464 """ 

465 value = self.headers.get("retry-after") 

466 if value is None: 

467 return None 

468 

469 try: 

470 seconds = int(value) 

471 except ValueError: 

472 return parse_date(value) 

473 

474 return datetime.now(timezone.utc) + timedelta(seconds=seconds) 

475 

476 @retry_after.setter 

477 def retry_after(self, value: datetime | int | str | None) -> None: 

478 if value is None: 

479 if "retry-after" in self.headers: 

480 del self.headers["retry-after"] 

481 return 

482 elif isinstance(value, datetime): 

483 value = http_date(value) 

484 else: 

485 value = str(value) 

486 self.headers["Retry-After"] = value 

487 

488 vary = _set_property( 

489 "Vary", 

490 doc="""The Vary field value indicates the set of request-header 

491 fields that fully determines, while the response is fresh, 

492 whether a cache is permitted to use the response to reply to a 

493 subsequent request without revalidation.""", 

494 ) 

495 content_language = _set_property( 

496 "Content-Language", 

497 doc="""The Content-Language entity-header field describes the 

498 natural language(s) of the intended audience for the enclosed 

499 entity. Note that this might not be equivalent to all the 

500 languages used within the entity-body.""", 

501 ) 

502 allow = _set_property( 

503 "Allow", 

504 doc="""The Allow entity-header field lists the set of methods 

505 supported by the resource identified by the Request-URI. The 

506 purpose of this field is strictly to inform the recipient of 

507 valid methods associated with the resource. An Allow header 

508 field MUST be present in a 405 (Method Not Allowed) 

509 response.""", 

510 ) 

511 

512 # ETag 

513 

514 @property 

515 def cache_control(self) -> ResponseCacheControl: 

516 """The Cache-Control general-header field is used to specify 

517 directives that MUST be obeyed by all caching mechanisms along the 

518 request/response chain. 

519 """ 

520 

521 def on_update(cache_control: ResponseCacheControl) -> None: 

522 if not cache_control and "cache-control" in self.headers: 

523 del self.headers["cache-control"] 

524 elif cache_control: 

525 self.headers["Cache-Control"] = cache_control.to_header() 

526 

527 return parse_cache_control_header( 

528 self.headers.get("cache-control"), on_update, ResponseCacheControl 

529 ) 

530 

531 def set_etag(self, etag: str, weak: bool = False) -> None: 

532 """Set the etag, and override the old one if there was one.""" 

533 self.headers["ETag"] = quote_etag(etag, weak) 

534 

535 def get_etag(self) -> tuple[str, bool] | tuple[None, None]: 

536 """Return a tuple in the form ``(etag, is_weak)``. If there is no 

537 ETag the return value is ``(None, None)``. 

538 """ 

539 return unquote_etag(self.headers.get("ETag")) 

540 

541 accept_ranges = header_property[str]( 

542 "Accept-Ranges", 

543 doc="""The `Accept-Ranges` header. Even though the name would 

544 indicate that multiple values are supported, it must be one 

545 string token only. 

546 

547 The values ``'bytes'`` and ``'none'`` are common. 

548 

549 .. versionadded:: 0.7""", 

550 ) 

551 

552 @property 

553 def content_range(self) -> ContentRange: 

554 """The ``Content-Range`` header as a 

555 :class:`~werkzeug.datastructures.ContentRange` object. Available 

556 even if the header is not set. 

557 

558 .. versionadded:: 0.7 

559 """ 

560 

561 def on_update(rng: ContentRange) -> None: 

562 if not rng: 

563 del self.headers["content-range"] 

564 else: 

565 self.headers["Content-Range"] = rng.to_header() 

566 

567 rv = parse_content_range_header(self.headers.get("content-range"), on_update) 

568 # always provide a content range object to make the descriptor 

569 # more user friendly. It provides an unset() method that can be 

570 # used to remove the header quickly. 

571 if rv is None: 

572 rv = ContentRange(None, None, None, on_update=on_update) 

573 return rv 

574 

575 @content_range.setter 

576 def content_range(self, value: ContentRange | str | None) -> None: 

577 if not value: 

578 del self.headers["content-range"] 

579 elif isinstance(value, str): 

580 self.headers["Content-Range"] = value 

581 else: 

582 self.headers["Content-Range"] = value.to_header() 

583 

584 # Authorization 

585 

586 @property 

587 def www_authenticate(self) -> WWWAuthenticate: 

588 """The ``WWW-Authenticate`` header parsed into a :class:`.WWWAuthenticate` 

589 object. Modifying the object will modify the header value. 

590 

591 This header is not set by default. To set this header, assign an instance of 

592 :class:`.WWWAuthenticate` to this attribute. 

593 

594 .. code-block:: python 

595 

596 response.www_authenticate = WWWAuthenticate( 

597 "basic", {"realm": "Authentication Required"} 

598 ) 

599 

600 Multiple values for this header can be sent to give the client multiple options. 

601 Assign a list to set multiple headers. However, modifying the items in the list 

602 will not automatically update the header values, and accessing this attribute 

603 will only ever return the first value. 

604 

605 To unset this header, assign ``None`` or use ``del``. 

606 

607 .. versionchanged:: 2.3 

608 This attribute can be assigned to to set the header. A list can be assigned 

609 to set multiple header values. Use ``del`` to unset the header. 

610 

611 .. versionchanged:: 2.3 

612 :class:`WWWAuthenticate` is no longer a ``dict``. The ``token`` attribute 

613 was added for auth challenges that use a token instead of parameters. 

614 """ 

615 value = WWWAuthenticate.from_header(self.headers.get("WWW-Authenticate")) 

616 

617 if value is None: 

618 value = WWWAuthenticate("basic") 

619 

620 def on_update(value: WWWAuthenticate) -> None: 

621 self.www_authenticate = value 

622 

623 value._on_update = on_update 

624 return value 

625 

626 @www_authenticate.setter 

627 def www_authenticate( 

628 self, value: WWWAuthenticate | list[WWWAuthenticate] | None 

629 ) -> None: 

630 if not value: # None or empty list 

631 del self.www_authenticate 

632 elif isinstance(value, list): 

633 # Clear any existing header by setting the first item. 

634 self.headers.set("WWW-Authenticate", value[0].to_header()) 

635 

636 for item in value[1:]: 

637 # Add additional header lines for additional items. 

638 self.headers.add("WWW-Authenticate", item.to_header()) 

639 else: 

640 self.headers.set("WWW-Authenticate", value.to_header()) 

641 

642 def on_update(value: WWWAuthenticate) -> None: 

643 self.www_authenticate = value 

644 

645 # When setting a single value, allow updating it directly. 

646 value._on_update = on_update 

647 

648 @www_authenticate.deleter 

649 def www_authenticate(self) -> None: 

650 if "WWW-Authenticate" in self.headers: 

651 del self.headers["WWW-Authenticate"] 

652 

653 # CSP 

654 

655 @property 

656 def content_security_policy(self) -> ContentSecurityPolicy: 

657 """The ``Content-Security-Policy`` header as a 

658 :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available 

659 even if the header is not set. 

660 

661 The Content-Security-Policy header adds an additional layer of 

662 security to help detect and mitigate certain types of attacks. 

663 """ 

664 

665 def on_update(csp: ContentSecurityPolicy) -> None: 

666 if not csp: 

667 del self.headers["content-security-policy"] 

668 else: 

669 self.headers["Content-Security-Policy"] = csp.to_header() 

670 

671 rv = parse_csp_header(self.headers.get("content-security-policy"), on_update) 

672 if rv is None: 

673 rv = ContentSecurityPolicy(None, on_update=on_update) 

674 return rv 

675 

676 @content_security_policy.setter 

677 def content_security_policy( 

678 self, value: ContentSecurityPolicy | str | None 

679 ) -> None: 

680 if not value: 

681 del self.headers["content-security-policy"] 

682 elif isinstance(value, str): 

683 self.headers["Content-Security-Policy"] = value 

684 else: 

685 self.headers["Content-Security-Policy"] = value.to_header() 

686 

687 @property 

688 def content_security_policy_report_only(self) -> ContentSecurityPolicy: 

689 """The ``Content-Security-policy-report-only`` header as a 

690 :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available 

691 even if the header is not set. 

692 

693 The Content-Security-Policy-Report-Only header adds a csp policy 

694 that is not enforced but is reported thereby helping detect 

695 certain types of attacks. 

696 """ 

697 

698 def on_update(csp: ContentSecurityPolicy) -> None: 

699 if not csp: 

700 del self.headers["content-security-policy-report-only"] 

701 else: 

702 self.headers["Content-Security-policy-report-only"] = csp.to_header() 

703 

704 rv = parse_csp_header( 

705 self.headers.get("content-security-policy-report-only"), on_update 

706 ) 

707 if rv is None: 

708 rv = ContentSecurityPolicy(None, on_update=on_update) 

709 return rv 

710 

711 @content_security_policy_report_only.setter 

712 def content_security_policy_report_only( 

713 self, value: ContentSecurityPolicy | str | None 

714 ) -> None: 

715 if not value: 

716 del self.headers["content-security-policy-report-only"] 

717 elif isinstance(value, str): 

718 self.headers["Content-Security-policy-report-only"] = value 

719 else: 

720 self.headers["Content-Security-policy-report-only"] = value.to_header() 

721 

722 # CORS 

723 

724 @property 

725 def access_control_allow_credentials(self) -> bool: 

726 """Whether credentials can be shared by the browser to 

727 JavaScript code. As part of the preflight request it indicates 

728 whether credentials can be used on the cross origin request. 

729 """ 

730 return "Access-Control-Allow-Credentials" in self.headers 

731 

732 @access_control_allow_credentials.setter 

733 def access_control_allow_credentials(self, value: bool | None) -> None: 

734 if value is True: 

735 self.headers["Access-Control-Allow-Credentials"] = "true" 

736 else: 

737 self.headers.pop("Access-Control-Allow-Credentials", None) 

738 

739 access_control_allow_headers = header_property( 

740 "Access-Control-Allow-Headers", 

741 load_func=parse_set_header, 

742 dump_func=dump_header, 

743 doc="Which headers can be sent with the cross origin request.", 

744 ) 

745 

746 access_control_allow_methods = header_property( 

747 "Access-Control-Allow-Methods", 

748 load_func=parse_set_header, 

749 dump_func=dump_header, 

750 doc="Which methods can be used for the cross origin request.", 

751 ) 

752 

753 access_control_allow_origin = header_property[str]( 

754 "Access-Control-Allow-Origin", 

755 doc="The origin or '*' for any origin that may make cross origin requests.", 

756 ) 

757 

758 access_control_expose_headers = header_property( 

759 "Access-Control-Expose-Headers", 

760 load_func=parse_set_header, 

761 dump_func=dump_header, 

762 doc="Which headers can be shared by the browser to JavaScript code.", 

763 ) 

764 

765 access_control_max_age = header_property( 

766 "Access-Control-Max-Age", 

767 load_func=int, 

768 dump_func=str, 

769 doc="The maximum age in seconds the access control settings can be cached for.", 

770 ) 

771 

772 cross_origin_opener_policy = header_property[COOP]( 

773 "Cross-Origin-Opener-Policy", 

774 load_func=lambda value: COOP(value), 

775 dump_func=lambda value: value.value, 

776 default=COOP.UNSAFE_NONE, 

777 doc="""Allows control over sharing of browsing context group with cross-origin 

778 documents. Values must be a member of the :class:`werkzeug.http.COOP` enum.""", 

779 ) 

780 

781 cross_origin_embedder_policy = header_property[COEP]( 

782 "Cross-Origin-Embedder-Policy", 

783 load_func=lambda value: COEP(value), 

784 dump_func=lambda value: value.value, 

785 default=COEP.UNSAFE_NONE, 

786 doc="""Prevents a document from loading any cross-origin resources that do not 

787 explicitly grant the document permission. Values must be a member of the 

788 :class:`werkzeug.http.COEP` enum.""", 

789 )