Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions django/conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
DEFAULT_STORAGE_ALIAS = "default"
STATICFILES_STORAGE_ALIAS = "staticfiles"

# RemovedInDjango70Warning.
SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG = (
"The SIGNED_COOKIE_LEGACY_SALT_FALLBACK transitional setting is "
"deprecated. Remove it from your settings once legacy signed cookies "
"have expired. They will not be accepted in Django 7.0."
)
# RemovedInDjango70Warning.
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG = (
"The USE_BLANK_CHOICE_DASH setting is deprecated. If you wish to define "
Expand Down Expand Up @@ -149,6 +155,12 @@ def __setattr__(self, name, value):
self.__dict__.pop(name, None)

# RemovedInDjango70Warning.
if name == "SIGNED_COOKIE_LEGACY_SALT_FALLBACK":
_show_settings_deprecation_warning(
SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG,
RemovedInDjango70Warning,
)
# RemovedInDjango70Warning.
if name == "USE_BLANK_CHOICE_DASH":
_show_settings_deprecation_warning(
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG, RemovedInDjango70Warning
Expand Down Expand Up @@ -260,6 +272,13 @@ def __init__(self, settings_module):
self._explicit_settings.add(setting)

# RemovedInDjango70Warning.
if "SIGNED_COOKIE_LEGACY_SALT_FALLBACK" in self._explicit_settings:
warnings.warn(
SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG,
RemovedInDjango70Warning,
skip_file_prefixes=django_file_prefixes(),
)
# RemovedInDjango70Warning.
if "USE_BLANK_CHOICE_DASH" in self._explicit_settings:
warnings.warn(
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG,
Expand Down Expand Up @@ -319,6 +338,13 @@ def __getattr__(self, name):

def __setattr__(self, name, value):
self._deleted.discard(name)
# RemovedInDjango70Warning.
if name == "SIGNED_COOKIE_LEGACY_SALT_FALLBACK":
_show_settings_deprecation_warning(
SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG,
RemovedInDjango70Warning,
)
# RemovedInDjango70Warning.
if name == "USE_BLANK_CHOICE_DASH":
_show_settings_deprecation_warning(
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG, RemovedInDjango70Warning
Expand Down
1 change: 1 addition & 0 deletions django/conf/global_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ def gettext_noop(s):
# SIGNING #
###########

SIGNED_COOKIE_LEGACY_SALT_FALLBACK = False
SIGNING_BACKEND = "django.core.signing.TimestampSigner"

########
Expand Down
54 changes: 36 additions & 18 deletions django/core/mail/backends/smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(
super().__init__(**kwargs)
self.fail_silently = fail_silently
self.connection = None
self._partial_connection = None
self._lock = threading.RLock()

# RemovedInDjango70Warning.
Expand Down Expand Up @@ -120,6 +121,11 @@ def open(self):
# Nothing to do if the connection is already open.
return False

# If a connection was partially opened before, close it.
if self._partial_connection is not None:
self._close_connection(self._partial_connection)
self._partial_connection = None

# If local_hostname is not specified, socket.getfqdn() gets used.
# For performance, we use the cached FQDN for local_hostname.
connection_params = {"local_hostname": DNS_NAME.get_fqdn()}
Expand All @@ -128,39 +134,51 @@ def open(self):
if self.use_ssl:
connection_params["context"] = self.ssl_context
try:
self.connection = self.connection_class(
self._partial_connection = self.connection_class(
self.host, self.port, **connection_params
)

# TLS/SSL are mutually exclusive, so only attempt TLS over
# non-secure connections.
if not self.use_ssl and self.use_tls:
self.connection.starttls(context=self.ssl_context)
self._partial_connection.starttls(context=self.ssl_context)
if self.username and self.password:
self.connection.login(self.username, self.password)
self._partial_connection.login(self.username, self.password)

# Don't set connection until it's fully configured.
self.connection = self._partial_connection
self._partial_connection = None

return True
except OSError:
if not self.fail_silently:
raise

def _close_connection(self, connection):
try:
connection.quit()
except (ssl.SSLError, smtplib.SMTPServerDisconnected):
# This happens when calling quit() on a TLS connection
# sometimes, or when the connection was already disconnected
# by the server.
connection.close()
except smtplib.SMTPException:
if self.fail_silently:
return
raise

def close(self):
"""Close the connection to the email server."""
if self.connection is None:
return
try:
if self._partial_connection is not None:
try:
self.connection.quit()
except (ssl.SSLError, smtplib.SMTPServerDisconnected):
# This happens when calling quit() on a TLS connection
# sometimes, or when the connection was already disconnected
# by the server.
self.connection.close()
except smtplib.SMTPException:
if self.fail_silently:
return
raise
finally:
self.connection = None
self._close_connection(self._partial_connection)
finally:
self._partial_connection = None
if self.connection is not None:
try:
self._close_connection(self.connection)
finally:
self.connection = None

def send_messages(self, email_messages):
"""
Expand Down
27 changes: 27 additions & 0 deletions django/core/signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,33 @@ def _cookie_signer_key(key):
return b"django.http.cookies" + force_bytes(key)


def _cookie_signer_salt(cookie_name, salt=""):
# Prefix the salt length so (cookie_name, salt) pairs can't collide.
return f"django.http.cookies.v2:{len(salt)}:{salt}{cookie_name}"


# RemovedInDjango70Warning: When the deprecation ends, remove.
def _cookie_signer_legacy_salt(cookie_name, salt=""):
return cookie_name + salt


def _unsign_cookie(signed_value, *, cookie_name, salt="", max_age=None):
try:
# RemovedInDjango70Warning: When the deprecation ends, replace the
# whole function body with this single return statement.
return get_cookie_signer(salt=_cookie_signer_salt(cookie_name, salt)).unsign(
signed_value, max_age=max_age
)
except BadSignature as exc:
if settings.SIGNED_COOKIE_LEGACY_SALT_FALLBACK and not isinstance(
exc, SignatureExpired
):
return get_cookie_signer(
salt=_cookie_signer_legacy_salt(cookie_name, salt)
).unsign(signed_value, max_age=max_age)
raise


def get_cookie_signer(salt="django.core.signing.get_cookie_signer"):
Signer = import_string(settings.SIGNING_BACKEND)
return Signer(
Expand Down
4 changes: 2 additions & 2 deletions django/http/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,8 @@ def get_signed_cookie(self, key, default=RAISE_ERROR, salt="", max_age=None):
else:
raise
try:
value = signing.get_cookie_signer(salt=key + salt).unsign(
cookie_value, max_age=max_age
value = signing._unsign_cookie(
cookie_value, cookie_name=key, salt=salt, max_age=max_age
)
except signing.BadSignature:
if default is not RAISE_ERROR:
Expand Down
4 changes: 3 additions & 1 deletion django/http/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,9 @@ def setdefault(self, key, value):
self.headers.setdefault(key, value)

def set_signed_cookie(self, key, value, salt="", **kwargs):
value = signing.get_cookie_signer(salt=key + salt).sign(value)
value = signing.get_cookie_signer(
salt=signing._cookie_signer_salt(key, salt)
).sign(value)
return self.set_cookie(key, value, **kwargs)

def delete_cookie(self, key, path="/", domain=None, samesite=None):
Expand Down
13 changes: 11 additions & 2 deletions django/middleware/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
* This middleware also sets ETag, Last-Modified, Expires and Cache-Control
headers on the response object.

* If the request had an Authorization header and the response was not marked
"Cache-Control: public", the response will vary on Authorization.
"""

import time
Expand All @@ -53,6 +55,7 @@
has_vary_header,
learn_cache_key,
patch_response_headers,
patch_vary_headers,
)
from django.utils.deprecation import MiddlewareMixin
from django.utils.http import parse_http_date_safe
Expand Down Expand Up @@ -102,8 +105,8 @@ def process_response(self, request, response):

# Don't cache responses when the Cache-Control header is set to
# private, no-cache, or no-store.
cache_control = response.get("Cache-Control", ())
if any(
cache_control = response.get("Cache-Control", "").lower()
if cache_control and any(
directive in cache_control
for directive in (
"private",
Expand All @@ -130,6 +133,12 @@ def process_response(self, request, response):
# max-age was set to 0, don't cache.
return response
patch_response_headers(response, timeout)
# Make the response vary on Authorization if the request bears that
# header, unless allowed by "public" per RFC 9111, Section 3.5. No
# exceptions are made for "s-maxage" and "must-revalidate" since these
# are not currently implemented by Django.
if request.headers.get("Authorization") and "public" not in cache_control:
patch_vary_headers(response, ("Authorization",))
if timeout and response.status_code == 200:
cache_key = learn_cache_key(
request, response, timeout, self.key_prefix, cache=self.cache
Expand Down
4 changes: 2 additions & 2 deletions django/utils/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,8 @@ def has_vary_header(response, header_query):
if not response.has_header("Vary"):
return False
vary_headers = cc_delim_re.split(response.headers["Vary"])
existing_headers = {header.lower() for header in vary_headers}
return header_query.lower() in existing_headers
existing_headers = {header.lower().strip() for header in vary_headers}
return header_query.lower().strip() in existing_headers


def _i18n_cache_key_suffix(request, cache_key):
Expand Down
3 changes: 3 additions & 0 deletions docs/internals/deprecation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ details on these changes.

* The ``URLIZE_ASSUME_HTTPS`` transitional setting will be removed.

* The ``SIGNED_COOKIE_LEGACY_SALT_FALLBACK`` transitional setting will be
removed.

* Using a percent sign in a column alias or annotation will raise
``ValueError``.

Expand Down
64 changes: 60 additions & 4 deletions docs/ref/models/database-functions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,34 @@ Usage example:
>>> Experiment.objects.filter(start_datetime__year=Extract("end_datetime", "year")).count()
1

.. admonition:: ``Extract()`` uses the active timezone

When :setting:`USE_TZ` is ``True``, Django saves values in ``UTC`` (or the
connection's configured timezone). Since ``tzinfo`` defaults to
:setting:`TIME_ZONE` when not provided, you may need to provide an explicit
``tzinfo`` to compare extracted values. The following example demonstrates
what happens when "Europe/Berlin" is active and how to adjust for this:

.. code-block:: pycon

>>> from django.utils import timezone
>>> from datetime import UTC, datetime
>>> from django.db.models.functions import ExtractHour
>>> start = datetime(2015, 6, 15, 14, 30, 50, 321)
>>> start = timezone.make_aware(start)
>>> exp = Experiment.objects.create(start_datetime=start)
>>> find_this_exp = Experiment.objects.annotate(
... extract_hour_start=ExtractHour("start_datetime")
... ).filter(extract_hour_start=start.hour)
# Comparing local time to UTC finds no results.
>>> find_this_exp.count()
0
>>> find_this_exp_adjusted = Experiment.objects.annotate(
... extract_hour_start=ExtractHour("start_datetime", tzinfo=UTC)
... ).filter(extract_hour_start=start.hour)
>>> find_this_exp_adjusted.count()
1

``DateField`` extracts
~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -584,16 +612,16 @@ The timezone offset for Melbourne in the example date above is +10:00. The
values returned when this timezone is active will be:

* "year": 2015-01-01 00:00:00+11:00
* "quarter": 2015-04-01 00:00:00+10:00
* "quarter": 2015-04-01 00:00:00+11:00
* "month": 2015-06-01 00:00:00+10:00
* "week": 2015-06-16 00:00:00+10:00
* "week": 2015-06-15 00:00:00+10:00
* "day": 2015-06-16 00:00:00+10:00
* "hour": 2015-06-16 00:00:00+10:00
* "minute": 2015-06-16 00:30:00+10:00
* "second": 2015-06-16 00:30:50+10:00

The year has an offset of +11:00 because the result transitioned into daylight
saving time.
The year and quarter have an offset of +11:00 because each result falls before
the daylight saving time transition.

Each ``kind`` above has a corresponding ``Trunc`` subclass (listed below) that
should typically be used instead of the more verbose equivalent,
Expand Down Expand Up @@ -634,6 +662,34 @@ Usage example:
2015-06-15 14:30:50.000321
2015-06-15 14:40:02.000123

.. admonition:: ``Trunc()`` produces naive datetimes

When :setting:`USE_TZ` is ``True``, Django saves values in ``UTC`` (or the
connection's configured timezone). Since ``tzinfo`` defaults to
:setting:`TIME_ZONE` when not provided, you may need to provide an explicit
``tzinfo`` to compare truncated values. The following example demonstrates
what happens when "Europe/Berlin" is active and how to adjust for this:

.. code-block:: pycon

>>> from django.utils import timezone
>>> from datetime import UTC, datetime
>>> from django.db.models.functions import TruncSecond
>>> start = datetime(2015, 6, 15, 14, 30, 50, 321)
>>> start = timezone.make_aware(start)
>>> exp = Experiment.objects.create(start_datetime=start)
>>> find_this_exp = Experiment.objects.annotate(
... trunc_start=TruncSecond("start_datetime")
... ).filter(trunc_start__lte=start)
# Comparing local time to UTC finds no results.
>>> find_this_exp.count()
0
>>> find_this_exp_adjusted = Experiment.objects.annotate(
... trunc_start=TruncSecond("start_datetime", tzinfo=UTC)
... ).filter(trunc_start__lte=start)
>>> find_this_exp_adjusted.count()
1

``DateField`` truncation
~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
Loading
Loading