diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 08953509a7..d7aee0c80a 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -10,7 +10,7 @@ jobs: hash: ${{ steps.hash.outputs.hash }} steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 + - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c with: python-version: '3.x' cache: pip @@ -23,7 +23,7 @@ jobs: - name: generate hash id: hash run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 + - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 with: path: ./dist provenance: @@ -44,7 +44,7 @@ jobs: permissions: contents: write steps: - - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a + - uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 - name: create release run: > gh release create --draft --repo ${{ github.repository }} @@ -61,11 +61,11 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a - - uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf + - uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 + - uses: pypa/gh-action-pypi-publish@e53eb8b103ffcb59469888563dc324e3c8ba6f06 with: repository-url: https://test.pypi.org/legacy/ packages-dir: artifact/ - - uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf + - uses: pypa/gh-action-pypi-publish@e53eb8b103ffcb59469888563dc324e3c8ba6f06 with: packages-dir: artifact/ diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 9e0ef4583c..142732b15c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -32,13 +32,13 @@ jobs: - {name: Typing, python: '3.12', os: ubuntu-latest, tox: typing} steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 + - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c with: python-version: ${{ matrix.python }} cache: 'pip' cache-dependency-path: requirements*/*.txt - name: cache mypy - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 with: path: ./.mypy_cache key: mypy|${{ matrix.python }}|${{ hashFiles('pyproject.toml') }} diff --git a/CHANGES.rst b/CHANGES.rst index 186e8f580a..ca85717131 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,13 @@ .. currentmodule:: werkzeug +Version 3.1.0 +------------- + +Unreleased + +- Support Cookie CHIPS (Partitioned Cookies). :issue:`2797` + + Version 3.0.1 ------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 0f3714e6e0..7424f0d6b0 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -141,7 +141,7 @@ the quality, the best item being the first: 'text/html' >>> 'application/xhtml+xml' in request.accept_mimetypes True ->>> print request.accept_mimetypes["application/json"] +>>> print(request.accept_mimetypes["application/json"]) 0.8 The same works for languages: diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 943787a7ce..9cb5aef471 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -123,7 +123,7 @@ if they are not used right away, to keep it from being confusing:: import os import redis - from werkzeug.urls import url_parse + from urllib.parse import urlparse from werkzeug.wrappers import Request, Response from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, NotFound @@ -308,7 +308,7 @@ we need to write a function and a helper method. For URL validation this is good enough:: def is_valid_url(url): - parts = url_parse(url) + parts = urlparse(url) return parts.scheme in ('http', 'https') For inserting the URL, all we need is this little method on our class:: diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py index 8280f51fa2..4ead0165a0 100644 --- a/src/werkzeug/http.py +++ b/src/werkzeug/http.py @@ -1216,6 +1216,7 @@ def dump_cookie( sync_expires: bool = True, max_size: int = 4093, samesite: str | None = None, + partitioned: bool = False, ) -> str: """Create a Set-Cookie header without the ``Set-Cookie`` prefix. @@ -1252,9 +1253,14 @@ def dump_cookie( `_. Set to 0 to disable this check. :param samesite: Limits the scope of the cookie such that it will only be attached to requests if those requests are same-site. + :param partitioned: Opts the cookie into partitioned storage. This + will also set secure to True .. _`cookie`: http://browsercookielimits.squawky.net/ + .. versionchanged:: 3.1 + The ``partitioned`` parameter was added. + .. versionchanged:: 3.0 Passing bytes, and the ``charset`` parameter, were removed. @@ -1298,6 +1304,9 @@ def dump_cookie( if samesite not in {"Strict", "Lax", "None"}: raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.") + if partitioned: + secure = True + # Quote value if it contains characters not allowed by RFC 6265. Slash-escape with # three octal digits, which matches http.cookies, although the RFC suggests base64. if not _cookie_no_quote_re.fullmatch(value): @@ -1319,6 +1328,7 @@ def dump_cookie( ("HttpOnly", httponly), ("Path", path), ("SameSite", samesite), + ("Partitioned", partitioned), ): if v is None or v is False: continue diff --git a/src/werkzeug/sansio/response.py b/src/werkzeug/sansio/response.py index 271974ecf0..cfad0994ba 100644 --- a/src/werkzeug/sansio/response.py +++ b/src/werkzeug/sansio/response.py @@ -194,6 +194,7 @@ def set_cookie( secure: bool = False, httponly: bool = False, samesite: str | None = None, + partitioned: bool = False, ) -> None: """Sets a cookie. @@ -218,6 +219,7 @@ def set_cookie( :param httponly: Disallow JavaScript access to the cookie. :param samesite: Limit the scope of the cookie to only be attached to requests that are "same-site". + :param partitioned: If ``True``, the cookie will be partitioned. """ self.headers.add( "Set-Cookie", @@ -232,6 +234,7 @@ def set_cookie( httponly=httponly, max_size=self.max_cookie_size, samesite=samesite, + partitioned=partitioned, ), ) @@ -243,6 +246,7 @@ def delete_cookie( secure: bool = False, httponly: bool = False, samesite: str | None = None, + partitioned: bool = False, ) -> None: """Delete a cookie. Fails silently if key doesn't exist. @@ -256,6 +260,7 @@ def delete_cookie( :param httponly: Disallow JavaScript access to the cookie. :param samesite: Limit the scope of the cookie to only be attached to requests that are "same-site". + :param partitioned: If ``True``, the cookie will be partitioned. """ self.set_cookie( key, @@ -266,6 +271,7 @@ def delete_cookie( secure=secure, httponly=httponly, samesite=samesite, + partitioned=partitioned, ) @property diff --git a/tests/test_http.py b/tests/test_http.py index bbd51ba335..1cf1613da7 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -576,6 +576,14 @@ def test_cookie_samesite_invalid(self): with pytest.raises(ValueError): http.dump_cookie("foo", "bar", samesite="invalid") + def test_cookie_partitioned(self): + value = http.dump_cookie("foo", "bar", partitioned=True, secure=True) + assert value == "foo=bar; Secure; Path=/; Partitioned" + + def test_cookie_partitioned_sets_secure(self): + value = http.dump_cookie("foo", "bar", partitioned=True, secure=False) + assert value == "foo=bar; Secure; Path=/; Partitioned" + class TestRange: def test_if_range_parsing(self):