diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index ffca8163ec..ec99b67ddf 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -3,6 +3,7 @@ import json 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 @@ -39,7 +40,7 @@ from django.urls import reverse 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 diff --git a/django/core/cache/utils.py b/django/core/cache/utils.py index 84676b9237..e9e144275f 100644 --- a/django/core/cache/utils.py +++ b/django/core/cache/utils.py @@ -1,7 +1,7 @@ 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' @@ -9,6 +9,6 @@ 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()) diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index c5080e6024..5a2413d22d 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -5,6 +5,7 @@ from decimal import ROUND_HALF_UP, Context, Decimal, InvalidOperation 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 @@ -13,7 +14,6 @@ from django.utils.html import ( 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, @@ -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) diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py index d838e19ceb..e71744ead7 100644 --- a/django/urls/resolvers.py +++ b/django/urls/resolvers.py @@ -9,6 +9,7 @@ import functools 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 @@ -17,7 +18,7 @@ from django.core.exceptions import ImproperlyConfigured 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 @@ -455,7 +456,7 @@ class RegexURLResolver(LocaleRegexProvider): 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:] diff --git a/django/utils/http.py b/django/utils/http.py index afa7368ee8..7e7b7ab321 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -14,7 +14,7 @@ from urllib.parse import ( 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.encoding import force_bytes from django.utils.functional import keep_lazy_text # based on RFC 7232, Appendix C @@ -47,58 +47,53 @@ FIELDS_MATCH = re.compile('[&;]') @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. + (was used for unicode handling on Python 2) """ - return force_text(quote(force_str(url), force_str(safe))) + 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))) + 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))) + 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))) + 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): diff --git a/django/views/i18n.py b/django/views/i18n.py index ada2624795..93a67d2a7e 100644 --- a/django/views/i18n.py +++ b/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 @@ -9,7 +10,7 @@ from django.template import Context, Engine 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, ) @@ -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) diff --git a/docs/ref/unicode.txt b/docs/ref/unicode.txt index eb71b63682..b0a888e2f8 100644 --- a/docs/ref/unicode.txt +++ b/docs/ref/unicode.txt @@ -156,8 +156,8 @@ Django provides some assistance. * The function :func:`django.utils.encoding.iri_to_uri()` implements the conversion from IRI to URI as required by the specification (:rfc:`3987#section-3.1`). -* The functions :func:`django.utils.http.urlquote()` and - :func:`django.utils.http.urlquote_plus()` are versions of Python's standard +* The functions ``django.utils.http.urlquote()`` and + ``django.utils.http.urlquote_plus()`` are versions of Python's standard ``urllib.quote()`` and ``urllib.quote_plus()`` that work with non-ASCII characters. (The data is converted to UTF-8 prior to encoding.) diff --git a/docs/ref/urlresolvers.txt b/docs/ref/urlresolvers.txt index 9d462637ef..2e6bc463dd 100644 --- a/docs/ref/urlresolvers.txt +++ b/docs/ref/urlresolvers.txt @@ -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()`` ================== diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index cc6cf74478..7acf057e08 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -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 + operate on ``MultiValueDict`` and non-string values. .. function:: cookie_date(epoch_seconds=None) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 0ddd1e05dd..c3dd9acf4b 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -585,7 +585,7 @@ be at the end of a line. If they are not, the comments are ignored and Quoting in ``reverse()`` ------------------------ -When reversing URLs, Django didn't apply :func:`~django.utils.http.urlquote` +When reversing URLs, Django didn't apply ``django.utils.http.urlquote`` to arguments before interpolating them in URL patterns. This bug is fixed in Django 1.6. If you worked around this bug by applying URL quoting before passing arguments to ``reverse()``, this may result in double-quoting. If this diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py index 76d167fa40..cee3383f05 100644 --- a/tests/auth_tests/test_views.py +++ b/tests/auth_tests/test_views.py @@ -3,7 +3,7 @@ import itertools 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 @@ -28,7 +28,6 @@ from django.test.utils import patch_logger 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 @@ -546,7 +545,7 @@ class LoginTest(AuthViewsTestCase): 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', @@ -568,7 +567,7 @@ class LoginTest(AuthViewsTestCase): 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', @@ -583,7 +582,7 @@ class LoginTest(AuthViewsTestCase): 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', @@ -701,13 +700,13 @@ class LoginURLSettings(AuthViewsTestCase): @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) @@ -717,7 +716,7 @@ class LoginURLSettings(AuthViewsTestCase): @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) @@ -973,7 +972,7 @@ class LogoutTest(AuthViewsTestCase): 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) @@ -994,7 +993,7 @@ class LogoutTest(AuthViewsTestCase): 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) @@ -1008,7 +1007,7 @@ class LogoutTest(AuthViewsTestCase): 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) diff --git a/tests/contenttypes_tests/models.py b/tests/contenttypes_tests/models.py index 0aa8bbf0bc..5475c4aade 100644 --- a/tests/contenttypes_tests/models.py +++ b/tests/contenttypes_tests/models.py @@ -1,10 +1,11 @@ +from urllib.parse import quote + from django.contrib.contenttypes.fields import ( GenericForeignKey, GenericRelation, ) from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import SiteManager from django.db import models -from django.utils.http import urlquote class Site(models.Model): @@ -72,7 +73,7 @@ class FooWithUrl(FooWithoutUrl): """ def get_absolute_url(self): - return "/users/%s/" % urlquote(self.name) + return "/users/%s/" % quote(self.name) class FooWithBrokenAbsoluteUrl(FooWithoutUrl): @@ -126,4 +127,4 @@ class ModelWithNullFKToSite(models.Model): return self.title def get_absolute_url(self): - return '/title/%s/' % urlquote(self.title) + return '/title/%s/' % quote(self.title) diff --git a/tests/file_uploads/tests.py b/tests/file_uploads/tests.py index 44a7272753..51e6937598 100644 --- a/tests/file_uploads/tests.py +++ b/tests/file_uploads/tests.py @@ -7,13 +7,13 @@ import sys import tempfile as sys_tempfile import unittest from io import BytesIO, StringIO +from urllib.parse import quote from django.core.files import temp as tempfile from django.core.files.uploadedfile import SimpleUploadedFile from django.http.multipartparser import MultiPartParser, parse_header from django.test import SimpleTestCase, TestCase, client, override_settings from django.utils.encoding import force_bytes -from django.utils.http import urlquote from . import uploadhandler from .models import FileModel @@ -127,7 +127,7 @@ class FileUploadTests(TestCase): payload = client.FakePayload() payload.write('\r\n'.join([ '--' + client.BOUNDARY, - 'Content-Disposition: form-data; name="file_unicode"; filename*=UTF-8\'\'%s' % urlquote(UNICODE_FILENAME), + 'Content-Disposition: form-data; name="file_unicode"; filename*=UTF-8\'\'%s' % quote(UNICODE_FILENAME), 'Content-Type: application/octet-stream', '', 'You got pwnd.\r\n', @@ -153,7 +153,7 @@ class FileUploadTests(TestCase): payload.write( '\r\n'.join([ '--' + client.BOUNDARY, - 'Content-Disposition: form-data; name*=UTF-8\'\'file_unicode; filename*=UTF-8\'\'%s' % urlquote( + 'Content-Disposition: form-data; name*=UTF-8\'\'file_unicode; filename*=UTF-8\'\'%s' % quote( UNICODE_FILENAME ), 'Content-Type: application/octet-stream', diff --git a/tests/requests/tests.py b/tests/requests/tests.py index ac331232aa..932078fb0e 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta from http import cookies from io import BytesIO from itertools import chain -from urllib.parse import urlencode as original_urlencode +from urllib.parse import urlencode from django.core.exceptions import SuspiciousOperation from django.core.handlers.wsgi import LimitedStream, WSGIRequest @@ -14,7 +14,7 @@ from django.http.request import split_domain_port from django.test import RequestFactory, SimpleTestCase, override_settings from django.test.client import FakePayload from django.test.utils import freeze_time -from django.utils.http import cookie_date, urlencode +from django.utils.http import cookie_date from django.utils.timezone import utc @@ -379,7 +379,7 @@ class RequestsTests(SimpleTestCase): """ Test a POST with non-utf-8 payload encoding. """ - payload = FakePayload(original_urlencode({'key': 'España'.encode('latin-1')})) + payload = FakePayload(urlencode({'key': 'España'.encode('latin-1')})) request = WSGIRequest({ 'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), diff --git a/tests/servers/tests.py b/tests/servers/tests.py index 27f7f9994f..5052f57908 100644 --- a/tests/servers/tests.py +++ b/tests/servers/tests.py @@ -5,10 +5,10 @@ import errno import os import socket from urllib.error import HTTPError +from urllib.parse import urlencode from urllib.request import urlopen from django.test import LiveServerTestCase, override_settings -from django.utils.http import urlencode from .models import Person diff --git a/tests/utils_tests/test_encoding.py b/tests/utils_tests/test_encoding.py index 2efdd24ff8..ea3b97ddf5 100644 --- a/tests/utils_tests/test_encoding.py +++ b/tests/utils_tests/test_encoding.py @@ -1,12 +1,12 @@ import datetime import unittest +from urllib.parse import quote_plus from django.utils.encoding import ( escape_uri_path, filepath_to_uri, force_bytes, force_text, iri_to_uri, smart_text, uri_to_iri, ) from django.utils.functional import SimpleLazyObject -from django.utils.http import urlquote_plus class TestEncodingUtils(unittest.TestCase): @@ -72,7 +72,7 @@ class TestRFC3987IEncodingUtils(unittest.TestCase): # Valid UTF-8 sequences are encoded. ('red%09rosé#red', 'red%09ros%C3%A9#red'), ('/blog/for/Jürgen Münster/', '/blog/for/J%C3%BCrgen%20M%C3%BCnster/'), - ('locations/%s' % urlquote_plus('Paris & Orléans'), 'locations/Paris+%26+Orl%C3%A9ans'), + ('locations/%s' % quote_plus('Paris & Orléans'), 'locations/Paris+%26+Orl%C3%A9ans'), # Reserved chars remain unescaped. ('%&', '%&'),