Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refs #23919 -- Deprecated django.utils.http urllib equivalents #7906

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion django/contrib/admin/options.py
Expand Up @@ -3,6 +3,7 @@
import operator
from collections import OrderedDict
from functools import partial, reduce, update_wrapper
from urllib.parse import quote as urlquote

from django import forms
from django.conf import settings
Expand Down Expand Up @@ -39,7 +40,7 @@
from django.utils.decorators import method_decorator
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.http import urlencode, urlquote
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.utils.text import capfirst, format_lazy, get_text_list
from django.utils.translation import ugettext as _, ungettext
Expand Down
4 changes: 2 additions & 2 deletions django/core/cache/utils.py
@@ -1,14 +1,14 @@
import hashlib
from urllib.parse import quote

from django.utils.encoding import force_bytes
from django.utils.http import urlquote

TEMPLATE_FRAGMENT_KEY_TEMPLATE = 'template.cache.%s.%s'


def make_template_fragment_key(fragment_name, vary_on=None):
if vary_on is None:
vary_on = ()
key = ':'.join(urlquote(var) for var in vary_on)
key = ':'.join(quote(str(var)) for var in vary_on)
args = hashlib.md5(force_bytes(key))
return TEMPLATE_FRAGMENT_KEY_TEMPLATE % (fragment_name, args.hexdigest())
6 changes: 3 additions & 3 deletions django/template/defaultfilters.py
Expand Up @@ -5,6 +5,7 @@
from functools import wraps
from operator import itemgetter
from pprint import pformat
from urllib.parse import quote

from django.utils import formats
from django.utils.dateformat import format, time_format
Expand All @@ -13,7 +14,6 @@
avoid_wrapping, conditional_escape, escape, escapejs, linebreaks,
strip_tags, urlize as _urlize,
)
from django.utils.http import urlquote
from django.utils.safestring import SafeData, mark_safe
from django.utils.text import (
Truncator, normalize_newlines, phone2numeric, slugify as _slugify, wrap,
Expand Down Expand Up @@ -318,14 +318,14 @@ def urlencode(value, safe=None):
Escapes a value for use in a URL.

Takes an optional ``safe`` parameter used to determine the characters which
should not be escaped by Django's ``urlquote`` method. If not provided, the
should not be escaped by Python's quote() function. If not provided, the
default safe characters will be used (but an empty string can be provided
when *all* characters should be escaped).
"""
kwargs = {}
if safe is not None:
kwargs['safe'] = safe
return urlquote(value, **kwargs)
return quote(value, **kwargs)


@register.filter(is_safe=True, needs_autoescape=True)
Expand Down
5 changes: 3 additions & 2 deletions django/urls/resolvers.py
Expand Up @@ -9,6 +9,7 @@
import re
import threading
from importlib import import_module
from urllib.parse import quote

from django.conf import settings
from django.core.checks import Warning
Expand All @@ -17,7 +18,7 @@
from django.utils.datastructures import MultiValueDict
from django.utils.encoding import force_text
from django.utils.functional import cached_property
from django.utils.http import RFC3986_SUBDELIMS, urlquote
from django.utils.http import RFC3986_SUBDELIMS
from django.utils.regex_helper import normalize
from django.utils.translation import get_language

Expand Down Expand Up @@ -455,7 +456,7 @@ def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
candidate_pat = _prefix.replace('%', '%%') + result
if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % candidate_subs):
# safe characters from `pchar` definition of RFC 3986
url = urlquote(candidate_pat % candidate_subs, safe=RFC3986_SUBDELIMS + str('/~:@'))
url = quote(candidate_pat % candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
# Don't allow construction of scheme relative urls.
if url.startswith('//'):
url = '/%%2F%s' % url[2:]
Expand Down
69 changes: 43 additions & 26 deletions django/utils/http.py
Expand Up @@ -13,8 +13,10 @@

from django.core.exceptions import TooManyFieldsSent
from django.utils.datastructures import MultiValueDict
from django.utils.deprecation import RemovedInDjango21Warning
from django.utils.encoding import force_bytes, force_str, force_text
from django.utils.deprecation import (
RemovedInDjango21Warning, RemovedInDjango30Warning,
)
from django.utils.encoding import force_bytes
from django.utils.functional import keep_lazy_text

# based on RFC 7232, Appendix C
Expand Down Expand Up @@ -47,58 +49,73 @@
@keep_lazy_text
def urlquote(url, safe='/'):
"""
A version of Python's urllib.quote() function that can operate on unicode
strings. The url is first UTF-8 encoded before quoting. The returned string
can safely be used as part of an argument to a subsequent iri_to_uri() call
without double-quoting occurring.
A legacy compatibility wrapper to Python's urllib.parse.quote() function.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might add something like "(did something different for Python 2)" similar to django.utils._os.

(was used for unicode handling on Python 2)
"""
return force_text(quote(force_str(url), force_str(safe)))
warnings.warn(
"django.utils.http.urlquote() is deprecated, use urllib.parse.quote() instead.",
RemovedInDjango30Warning,
stacklevel=2,
)
return quote(url, safe)


@keep_lazy_text
def urlquote_plus(url, safe=''):
"""
A version of Python's urllib.quote_plus() function that can operate on
unicode strings. The url is first UTF-8 encoded before quoting. The
returned string can safely be used as part of an argument to a subsequent
iri_to_uri() call without double-quoting occurring.
A legacy compatibility wrapper to Python's urllib.parse.quote_plus()
function. (was used for unicode handling on Python 2)
"""
return force_text(quote_plus(force_str(url), force_str(safe)))
warnings.warn(
"django.utils.http.urlquote_plus() is deprecated, use urllib.parse.quote_plus() instead.",
RemovedInDjango30Warning,
stacklevel=2,
)
return quote_plus(url, safe)


@keep_lazy_text
def urlunquote(quoted_url):
"""
A wrapper for Python's urllib.unquote() function that can operate on
the result of django.utils.http.urlquote().
A legacy compatibility wrapper to Python's urllib.parse.unquote() function.
(was used for unicode handling on Python 2)
"""
return force_text(unquote(force_str(quoted_url)))
warnings.warn(
"django.utils.http.urlunquote() is deprecated, use urllib.parse.unquote() instead.",
RemovedInDjango30Warning,
stacklevel=2,
)
return unquote(quoted_url)


@keep_lazy_text
def urlunquote_plus(quoted_url):
"""
A wrapper for Python's urllib.unquote_plus() function that can operate on
the result of django.utils.http.urlquote_plus().
A legacy compatibility wrapper to Python's urllib.parse.unquote_plus()
function. (was used for unicode handling on Python 2)
"""
return force_text(unquote_plus(force_str(quoted_url)))
warnings.warn(
"django.utils.http.urlunquote_plus() is deprecated, use urllib.parse.unquote_plus() instead.",
RemovedInDjango30Warning,
stacklevel=2,
)
return unquote_plus(quoted_url)


def urlencode(query, doseq=0):
def urlencode(query, doseq=False):
"""
A version of Python's urllib.urlencode() function that can operate on
unicode strings. The parameters are first cast to UTF-8 encoded strings and
then encoded as per normal.
A version of Python's urllib.parse.urlencode() function that can operate on
MultiValueDict and non-string values.
"""
if isinstance(query, MultiValueDict):
query = query.lists()
elif hasattr(query, 'items'):
query = query.items()
return original_urlencode(
[(force_str(k),
[force_str(i) for i in v] if isinstance(v, (list, tuple)) else force_str(v))
for k, v in query],
doseq)
[(k, [str(i) for i in v] if isinstance(v, (list, tuple)) else str(v))
for k, v in query],
doseq
)


def cookie_date(epoch_seconds=None):
Expand Down
5 changes: 3 additions & 2 deletions django/views/i18n.py
@@ -1,6 +1,7 @@
import itertools
import json
import os
from urllib.parse import unquote

from django import http
from django.apps import apps
Expand All @@ -9,7 +10,7 @@
from django.urls import translate_url
from django.utils.encoding import force_text
from django.utils.formats import get_format
from django.utils.http import is_safe_url, urlunquote
from django.utils.http import is_safe_url
from django.utils.translation import (
LANGUAGE_SESSION_KEY, check_for_language, get_language,
)
Expand All @@ -35,7 +36,7 @@ def set_language(request):
not is_safe_url(url=next, allowed_hosts={request.get_host()}, require_https=request.is_secure())):
next = request.META.get('HTTP_REFERER')
if next:
next = urlunquote(next) # HTTP_REFERER may be encoded.
next = unquote(next) # HTTP_REFERER may be encoded.
if not is_safe_url(url=next, allowed_hosts={request.get_host()}, require_https=request.is_secure()):
next = '/'
response = http.HttpResponseRedirect(next) if next else http.HttpResponse(status=204)
Expand Down
4 changes: 4 additions & 0 deletions docs/internals/deprecation.txt
Expand Up @@ -17,6 +17,10 @@ details on these changes.

* The ``django.db.backends.postgresql_psycopg2`` module will be removed.

* ``django.utils.http.urlquote()``, ``django.utils.http.urlquote_plus()``,
``django.utils.http.urlunquote()`` and ``django.utils.http.urlunquote_plus()``
will be removed.

.. _deprecation-removed-in-2.1:

2.1
Expand Down
5 changes: 2 additions & 3 deletions docs/ref/urlresolvers.txt
Expand Up @@ -70,9 +70,8 @@ use for reversing. By default, the root URLconf for the current thread is used.
>>> reverse('cities', args=['Orléans'])
'.../Orl%C3%A9ans/'

Applying further encoding (such as :meth:`~django.utils.http.urlquote` or
``urllib.quote``) to the output of ``reverse()`` may produce undesirable
results.
Applying further encoding (such as :func:`urllib.parse.quote`) to the output
of ``reverse()`` may produce undesirable results.

``reverse_lazy()``
==================
Expand Down
21 changes: 2 additions & 19 deletions docs/ref/utils.txt
Expand Up @@ -684,27 +684,10 @@ escaping HTML.
.. module:: django.utils.http
:synopsis: HTTP helper functions. (URL encoding, cookie handling, ...)

.. function:: urlquote(url, safe='/')

A version of Python's ``urllib.quote()`` function that can operate on
unicode strings. The url is first UTF-8 encoded before quoting. The
returned string can safely be used as part of an argument to a subsequent
``iri_to_uri()`` call without double-quoting occurring. Employs lazy
execution.

.. function:: urlquote_plus(url, safe='')

A version of Python's urllib.quote_plus() function that can operate on
unicode strings. The url is first UTF-8 encoded before quoting. The
returned string can safely be used as part of an argument to a subsequent
``iri_to_uri()`` call without double-quoting occurring. Employs lazy
execution.

.. function:: urlencode(query, doseq=0)

A version of Python's urllib.urlencode() function that can operate on
unicode strings. The parameters are first cast to UTF-8 encoded strings
and then encoded as per normal.
A version of Python's :func:`urllib.parse.urlencode()` function that can
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually the () are omitted since sphinx adds those to the rendered version.

operate on ``MultiValueDict`` and non-string values.

.. function:: cookie_date(epoch_seconds=None)

Expand Down
21 changes: 10 additions & 11 deletions tests/auth_tests/test_views.py
Expand Up @@ -3,7 +3,7 @@
import os
import re
from importlib import import_module
from urllib.parse import ParseResult, urlparse
from urllib.parse import ParseResult, quote, urlparse

from django.apps import apps
from django.conf import settings
Expand All @@ -28,7 +28,6 @@
from django.urls import NoReverseMatch, reverse, reverse_lazy
from django.utils.deprecation import RemovedInDjango21Warning
from django.utils.encoding import force_text
from django.utils.http import urlquote
from django.utils.translation import LANGUAGE_SESSION_KEY

from .client import PasswordResetConfirmClient
Expand Down Expand Up @@ -546,7 +545,7 @@ def test_security_check(self):
nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
'url': login_url,
'next': REDIRECT_FIELD_NAME,
'bad_url': urlquote(bad_url),
'bad_url': quote(bad_url),
}
response = self.client.post(nasty_url, {
'username': 'testclient',
Expand All @@ -568,7 +567,7 @@ def test_security_check(self):
safe_url = '%(url)s?%(next)s=%(good_url)s' % {
'url': login_url,
'next': REDIRECT_FIELD_NAME,
'good_url': urlquote(good_url),
'good_url': quote(good_url),
}
response = self.client.post(safe_url, {
'username': 'testclient',
Expand All @@ -583,7 +582,7 @@ def test_security_check_https(self):
not_secured_url = '%(url)s?%(next)s=%(next_url)s' % {
'url': login_url,
'next': REDIRECT_FIELD_NAME,
'next_url': urlquote(non_https_next_url),
'next_url': quote(non_https_next_url),
}
post_data = {
'username': 'testclient',
Expand Down Expand Up @@ -701,13 +700,13 @@ def test_named_login_url(self):

@override_settings(LOGIN_URL='http://remote.example.com/login')
def test_remote_login_url(self):
quoted_next = urlquote('http://testserver/login_required/')
quoted_next = quote('http://testserver/login_required/')
expected = 'http://remote.example.com/login?next=%s' % quoted_next
self.assertLoginURLEquals(expected)

@override_settings(LOGIN_URL='https:///login/')
def test_https_login_url(self):
quoted_next = urlquote('http://testserver/login_required/')
quoted_next = quote('http://testserver/login_required/')
expected = 'https:///login/?next=%s' % quoted_next
self.assertLoginURLEquals(expected)

Expand All @@ -717,7 +716,7 @@ def test_login_url_with_querystring(self):

@override_settings(LOGIN_URL='http://remote.example.com/login/?next=/default/')
def test_remote_login_url_with_next_querystring(self):
quoted_next = urlquote('http://testserver/login_required/')
quoted_next = quote('http://testserver/login_required/')
expected = 'http://remote.example.com/login/?next=%s' % quoted_next
self.assertLoginURLEquals(expected)

Expand Down Expand Up @@ -973,7 +972,7 @@ def test_security_check(self):
nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
'url': logout_url,
'next': REDIRECT_FIELD_NAME,
'bad_url': urlquote(bad_url),
'bad_url': quote(bad_url),
}
self.login()
response = self.client.get(nasty_url)
Expand All @@ -994,7 +993,7 @@ def test_security_check(self):
safe_url = '%(url)s?%(next)s=%(good_url)s' % {
'url': logout_url,
'next': REDIRECT_FIELD_NAME,
'good_url': urlquote(good_url),
'good_url': quote(good_url),
}
self.login()
response = self.client.get(safe_url)
Expand All @@ -1008,7 +1007,7 @@ def test_security_check_https(self):
url = '%(url)s?%(next)s=%(next_url)s' % {
'url': logout_url,
'next': REDIRECT_FIELD_NAME,
'next_url': urlquote(non_https_next_url),
'next_url': quote(non_https_next_url),
}
self.login()
response = self.client.get(url, secure=True)
Expand Down