diff options
Diffstat (limited to 'rest_framework/compat.py')
| -rw-r--r-- | rest_framework/compat.py | 615 |
1 files changed, 155 insertions, 460 deletions
diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 6f7447ad..c6a4a869 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -5,34 +5,58 @@ versions of django/python, and compatibility wrappers around optional packages. # flake8: noqa from __future__ import unicode_literals - -import django from django.core.exceptions import ImproperlyConfigured from django.conf import settings - -# Try to import six from Django, fallback to included `six`. +from django.utils.encoding import force_text +from django.utils.six.moves.urllib.parse import urlparse as _urlparse +from django.utils import six +import django +import inspect try: - from django.utils import six + import importlib except ImportError: - from rest_framework import six + from django.utils import importlib -# location of patterns, url, include changes in 1.4 onwards -try: - from django.conf.urls import patterns, url, include -except ImportError: - from django.conf.urls.defaults import patterns, url, include +def unicode_repr(instance): + # Get the repr of an instance, but ensure it is a unicode string + # on both python 3 (already the case) and 2 (not the case). + if six.PY2: + return repr(instance).decode('utf-8') + return repr(instance) -# Handle django.utils.encoding rename: -# smart_unicode -> smart_text -# force_unicode -> force_text -try: - from django.utils.encoding import smart_text -except ImportError: - from django.utils.encoding import smart_unicode as smart_text + +def unicode_to_repr(value): + # Coerce a unicode string to the correct repr return type, depending on + # the Python version. We wrap all our `__repr__` implementations with + # this and then use unicode throughout internally. + if six.PY2: + return value.encode('utf-8') + return value + + +def unicode_http_header(value): + # Coerce HTTP header value to unicode. + if isinstance(value, six.binary_type): + return value.decode('iso-8859-1') + return value + + +def total_seconds(timedelta): + # TimeDelta.total_seconds() is only available in Python 2.7 + if hasattr(timedelta, 'total_seconds'): + return timedelta.total_seconds() + else: + return (timedelta.days * 86400.0) + float(timedelta.seconds) + (timedelta.microseconds / 1000000.0) + + +# OrderedDict only available in Python 2.7. +# This will always be the case in Django 1.7 and above, as these versions +# no longer support Python 2.6. +# For Django <= 1.6 and Python 2.6 fall back to SortedDict. try: - from django.utils.encoding import force_text + from collections import OrderedDict except ImportError: - from django.utils.encoding import force_unicode as force_text + from django.utils.datastructures import SortedDict as OrderedDict # HttpResponseBase only exists from 1.5 onwards @@ -41,437 +65,163 @@ try: except ImportError: from django.http import HttpResponse as HttpResponseBase -# django-filter is optional -try: - import django_filters -except ImportError: - django_filters = None - -# cStringIO only if it's available, otherwise StringIO +# contrib.postgres only supported from 1.8 onwards. try: - import cStringIO.StringIO as StringIO + from django.contrib.postgres import fields as postgres_fields except ImportError: - StringIO = six.StringIO - -BytesIO = six.BytesIO + postgres_fields = None -# urlparse compat import (Required because it changed in python 3.x) -try: - from urllib import parse as urlparse -except ImportError: - import urlparse +# request only provides `resolver_match` from 1.5 onwards. +def get_resolver_match(request): + try: + return request.resolver_match + except AttributeError: + # Django < 1.5 + from django.core.urlresolvers import resolve + return resolve(request.path_info) -# Try to import PIL in either of the two ways it can end up installed. +# django-filter is optional try: - from PIL import Image + import django_filters except ImportError: + django_filters = None + +if django.VERSION >= (1, 6): + def clean_manytomany_helptext(text): + return text +else: + # Up to version 1.5 many to many fields automatically suffix + # the `help_text` attribute with hardcoded text. + def clean_manytomany_helptext(text): + if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'): + text = text[:-69] + return text + +# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS +# Fixes (#1712). We keep the try/except for the test suite. +guardian = None +if 'guardian' in settings.INSTALLED_APPS: try: - import Image + import guardian + import guardian.shortcuts # Fixes #1624 except ImportError: - Image = None + pass -def get_concrete_model(model_cls): +def get_model_name(model_cls): try: - return model_cls._meta.concrete_model + return model_cls._meta.model_name except AttributeError: - # 1.3 does not include concrete model - return model_cls - - -# Django 1.5 add support for custom auth user model -if django.VERSION >= (1, 5): - AUTH_USER_MODEL = settings.AUTH_USER_MODEL -else: - AUTH_USER_MODEL = 'auth.User' + # < 1.6 used module_name instead of model_name + return model_cls._meta.module_name +# View._allowed_methods only present from 1.5 onwards if django.VERSION >= (1, 5): from django.views.generic import View else: - from django.views.generic import View as _View - from django.utils.decorators import classonlymethod - from django.utils.functional import update_wrapper - - class View(_View): - # 1.3 does not include head method in base View class - # See: https://code.djangoproject.com/ticket/15668 - @classonlymethod - def as_view(cls, **initkwargs): - """ - Main entry point for a request-response process. - """ - # sanitize keyword arguments - for key in initkwargs: - if key in cls.http_method_names: - raise TypeError("You tried to pass in the %s method name as a " - "keyword argument to %s(). Don't do that." - % (key, cls.__name__)) - if not hasattr(cls, key): - raise TypeError("%s() received an invalid keyword %r" % ( - cls.__name__, key)) - - def view(request, *args, **kwargs): - self = cls(**initkwargs) - if hasattr(self, 'get') and not hasattr(self, 'head'): - self.head = self.get - return self.dispatch(request, *args, **kwargs) - - # take name and docstring from class - update_wrapper(view, cls, updated=()) - - # and possible attributes set by decorators - # like csrf_exempt from dispatch - update_wrapper(view, cls.dispatch, assigned=()) - return view - - # _allowed_methods only present from 1.5 onwards + from django.views.generic import View as DjangoView + + class View(DjangoView): def _allowed_methods(self): return [m.upper() for m in self.http_method_names if hasattr(self, m)] -# PATCH method is not implemented by Django -if 'patch' not in View.http_method_names: - View.http_method_names = View.http_method_names + ['patch'] - - -# PUT, DELETE do not require CSRF until 1.4. They should. Make it better. -if django.VERSION >= (1, 4): - from django.middleware.csrf import CsrfViewMiddleware +# MinValueValidator, MaxValueValidator et al. only accept `message` in 1.8+ +if django.VERSION >= (1, 8): + from django.core.validators import MinValueValidator, MaxValueValidator + from django.core.validators import MinLengthValidator, MaxLengthValidator else: - import hashlib - import re - import random - import logging - - from django.conf import settings - from django.core.urlresolvers import get_callable - - try: - from logging import NullHandler - except ImportError: - class NullHandler(logging.Handler): - def emit(self, record): - pass - - logger = logging.getLogger('django.request') - - if not logger.handlers: - logger.addHandler(NullHandler()) - - def same_origin(url1, url2): - """ - Checks if two URLs are 'same-origin' - """ - p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2) - return p1[0:2] == p2[0:2] - - def constant_time_compare(val1, val2): - """ - Returns True if the two strings are equal, False otherwise. - - The time taken is independent of the number of characters that match. - """ - if len(val1) != len(val2): - return False - result = 0 - for x, y in zip(val1, val2): - result |= ord(x) ^ ord(y) - return result == 0 - - # Use the system (hardware-based) random number generator if it exists. - if hasattr(random, 'SystemRandom'): - randrange = random.SystemRandom().randrange - else: - randrange = random.randrange - - _MAX_CSRF_KEY = 18446744073709551616 # 2 << 63 - - REASON_NO_REFERER = "Referer checking failed - no Referer." - REASON_BAD_REFERER = "Referer checking failed - %s does not match %s." - REASON_NO_CSRF_COOKIE = "CSRF cookie not set." - REASON_BAD_TOKEN = "CSRF token missing or incorrect." - - def _get_failure_view(): - """ - Returns the view to be used for CSRF rejections - """ - return get_callable(settings.CSRF_FAILURE_VIEW) - - def _get_new_csrf_key(): - return hashlib.md5("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest() - - def get_token(request): - """ - Returns the the CSRF token required for a POST form. The token is an - alphanumeric value. - - A side effect of calling this function is to make the the csrf_protect - decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie' - header to the outgoing response. For this reason, you may need to use this - function lazily, as is done by the csrf context processor. - """ - request.META["CSRF_COOKIE_USED"] = True - return request.META.get("CSRF_COOKIE", None) - - def _sanitize_token(token): - # Allow only alphanum, and ensure we return a 'str' for the sake of the post - # processing middleware. - token = re.sub('[^a-zA-Z0-9]', '', str(token.decode('ascii', 'ignore'))) - if token == "": - # In case the cookie has been truncated to nothing at some point. - return _get_new_csrf_key() - else: - return token - - class CsrfViewMiddleware(object): - """ - Middleware that requires a present and correct csrfmiddlewaretoken - for POST requests that have a CSRF cookie, and sets an outgoing - CSRF cookie. + from django.core.validators import MinValueValidator as DjangoMinValueValidator + from django.core.validators import MaxValueValidator as DjangoMaxValueValidator + from django.core.validators import MinLengthValidator as DjangoMinLengthValidator + from django.core.validators import MaxLengthValidator as DjangoMaxLengthValidator + + class MinValueValidator(DjangoMinValueValidator): + def __init__(self, *args, **kwargs): + self.message = kwargs.pop('message', self.message) + super(MinValueValidator, self).__init__(*args, **kwargs) + + class MaxValueValidator(DjangoMaxValueValidator): + def __init__(self, *args, **kwargs): + self.message = kwargs.pop('message', self.message) + super(MaxValueValidator, self).__init__(*args, **kwargs) + + class MinLengthValidator(DjangoMinLengthValidator): + def __init__(self, *args, **kwargs): + self.message = kwargs.pop('message', self.message) + super(MinLengthValidator, self).__init__(*args, **kwargs) + + class MaxLengthValidator(DjangoMaxLengthValidator): + def __init__(self, *args, **kwargs): + self.message = kwargs.pop('message', self.message) + super(MaxLengthValidator, self).__init__(*args, **kwargs) + + +# URLValidator only accepts `message` in 1.6+ +if django.VERSION >= (1, 6): + from django.core.validators import URLValidator +else: + from django.core.validators import URLValidator as DjangoURLValidator - This middleware should be used in conjunction with the csrf_token template - tag. - """ - # The _accept and _reject methods currently only exist for the sake of the - # requires_csrf_token decorator. - def _accept(self, request): - # Avoid checking the request twice by adding a custom attribute to - # request. This will be relevant when both decorator and middleware - # are used. - request.csrf_processing_done = True - return None - - def _reject(self, request, reason): - return _get_failure_view()(request, reason=reason) - - def process_view(self, request, callback, callback_args, callback_kwargs): - - if getattr(request, 'csrf_processing_done', False): - return None - - try: - csrf_token = _sanitize_token(request.COOKIES[settings.CSRF_COOKIE_NAME]) - # Use same token next time - request.META['CSRF_COOKIE'] = csrf_token - except KeyError: - csrf_token = None - # Generate token and store it in the request, so it's available to the view. - request.META["CSRF_COOKIE"] = _get_new_csrf_key() - - # Wait until request.META["CSRF_COOKIE"] has been manipulated before - # bailing out, so that get_token still works - if getattr(callback, 'csrf_exempt', False): - return None - - # Assume that anything not defined as 'safe' by RC2616 needs protection. - if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'): - if getattr(request, '_dont_enforce_csrf_checks', False): - # Mechanism to turn off CSRF checks for test suite. It comes after - # the creation of CSRF cookies, so that everything else continues to - # work exactly the same (e.g. cookies are sent etc), but before the - # any branches that call reject() - return self._accept(request) - - if request.is_secure(): - # Suppose user visits http://example.com/ - # An active network attacker,(man-in-the-middle, MITM) sends a - # POST form which targets https://example.com/detonate-bomb/ and - # submits it via javascript. - # - # The attacker will need to provide a CSRF cookie and token, but - # that is no problem for a MITM and the session independent - # nonce we are using. So the MITM can circumvent the CSRF - # protection. This is true for any HTTP connection, but anyone - # using HTTPS expects better! For this reason, for - # https://example.com/ we need additional protection that treats - # http://example.com/ as completely untrusted. Under HTTPS, - # Barth et al. found that the Referer header is missing for - # same-domain requests in only about 0.2% of cases or less, so - # we can use strict Referer checking. - referer = request.META.get('HTTP_REFERER') - if referer is None: - logger.warning('Forbidden (%s): %s' % (REASON_NO_REFERER, request.path), - extra={ - 'status_code': 403, - 'request': request, - } - ) - return self._reject(request, REASON_NO_REFERER) - - # Note that request.get_host() includes the port - good_referer = 'https://%s/' % request.get_host() - if not same_origin(referer, good_referer): - reason = REASON_BAD_REFERER % (referer, good_referer) - logger.warning('Forbidden (%s): %s' % (reason, request.path), - extra={ - 'status_code': 403, - 'request': request, - } - ) - return self._reject(request, reason) - - if csrf_token is None: - # No CSRF cookie. For POST requests, we insist on a CSRF cookie, - # and in this way we can avoid all CSRF attacks, including login - # CSRF. - logger.warning('Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path), - extra={ - 'status_code': 403, - 'request': request, - } - ) - return self._reject(request, REASON_NO_CSRF_COOKIE) - - # check non-cookie token for match - request_csrf_token = "" - if request.method == "POST": - request_csrf_token = request.POST.get('csrfmiddlewaretoken', '') - - if request_csrf_token == "": - # Fall back to X-CSRFToken, to make things easier for AJAX, - # and possible for PUT/DELETE - request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '') - - if not constant_time_compare(request_csrf_token, csrf_token): - logger.warning('Forbidden (%s): %s' % (REASON_BAD_TOKEN, request.path), - extra={ - 'status_code': 403, - 'request': request, - } - ) - return self._reject(request, REASON_BAD_TOKEN) - - return self._accept(request) - -# timezone support is new in Django 1.4 -try: - from django.utils import timezone -except ImportError: - timezone = None + class URLValidator(DjangoURLValidator): + def __init__(self, *args, **kwargs): + self.message = kwargs.pop('message', self.message) + super(URLValidator, self).__init__(*args, **kwargs) -# dateparse is ALSO new in Django 1.4 -try: - from django.utils.dateparse import parse_date, parse_datetime, parse_time -except ImportError: - import datetime - import re - - date_re = re.compile( - r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$' - ) - - datetime_re = re.compile( - r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})' - r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})' - r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?' - r'(?P<tzinfo>Z|[+-]\d{1,2}:\d{1,2})?$' - ) - - time_re = re.compile( - r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})' - r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?' - ) - - def parse_date(value): - match = date_re.match(value) - if match: - kw = dict((k, int(v)) for k, v in match.groupdict().iteritems()) - return datetime.date(**kw) - - def parse_time(value): - match = time_re.match(value) - if match: - kw = match.groupdict() - if kw['microsecond']: - kw['microsecond'] = kw['microsecond'].ljust(6, '0') - kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None) - return datetime.time(**kw) - - def parse_datetime(value): - """Parse datetime, but w/o the timezone awareness in 1.4""" - match = datetime_re.match(value) - if match: - kw = match.groupdict() - if kw['microsecond']: - kw['microsecond'] = kw['microsecond'].ljust(6, '0') - kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None) - return datetime.datetime(**kw) - - -# smart_urlquote is new on Django 1.4 -try: - from django.utils.html import smart_urlquote -except ImportError: - import re - from django.utils.encoding import smart_str - try: - from urllib.parse import quote, urlsplit, urlunsplit - except ImportError: # Python 2 - from urllib import quote - from urlparse import urlsplit, urlunsplit - - unquoted_percents_re = re.compile(r'%(?![0-9A-Fa-f]{2})') - def smart_urlquote(url): - "Quotes a URL if it isn't already quoted." - # Handle IDN before quoting. - scheme, netloc, path, query, fragment = urlsplit(url) - try: - netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE - except UnicodeError: # invalid domain part - pass - else: - url = urlunsplit((scheme, netloc, path, query, fragment)) +# EmailValidator requires explicit regex prior to 1.6+ +if django.VERSION >= (1, 6): + from django.core.validators import EmailValidator +else: + from django.core.validators import EmailValidator as DjangoEmailValidator + from django.core.validators import email_re - # An URL is considered unquoted if it contains no % characters or - # contains a % not followed by two hexadecimal digits. See #9655. - if '%' not in url or unquoted_percents_re.search(url): - # See http://bugs.python.org/issue2637 - url = quote(smart_str(url), safe=b'!*\'();:@&=+$,/?#[]~') + class EmailValidator(DjangoEmailValidator): + def __init__(self, *args, **kwargs): + super(EmailValidator, self).__init__(email_re, *args, **kwargs) - return force_text(url) +# PATCH method is not implemented by Django +if 'patch' not in View.http_method_names: + View.http_method_names = View.http_method_names + ['patch'] -# RequestFactory only provide `generic` from 1.5 onwards +# RequestFactory only provides `generic` from 1.5 onwards from django.test.client import RequestFactory as DjangoRequestFactory from django.test.client import FakePayload + try: # In 1.5 the test client uses force_bytes - from django.utils.encoding import force_bytes_or_smart_bytes + from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes except ImportError: - # In 1.3 and 1.4 the test client just uses smart_str + # In 1.4 the test client just uses smart_str from django.utils.encoding import smart_str as force_bytes_or_smart_bytes class RequestFactory(DjangoRequestFactory): def generic(self, method, path, data='', content_type='application/octet-stream', **extra): - parsed = urlparse.urlparse(path) + parsed = _urlparse(path) data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET) r = { - 'PATH_INFO': self._get_path(parsed), - 'QUERY_STRING': force_text(parsed[4]), - 'REQUEST_METHOD': str(method), + 'PATH_INFO': self._get_path(parsed), + 'QUERY_STRING': force_text(parsed[4]), + 'REQUEST_METHOD': six.text_type(method), } if data: r.update({ 'CONTENT_LENGTH': len(data), - 'CONTENT_TYPE': str(content_type), - 'wsgi.input': FakePayload(data), - }) - elif django.VERSION <= (1, 4): - # For 1.3 we need an empty WSGI payload - r.update({ - 'wsgi.input': FakePayload('') + 'CONTENT_TYPE': six.text_type(content_type), + 'wsgi.input': FakePayload(data), }) r.update(extra) return self.request(**r) + # Markdown is optional try: import markdown @@ -486,72 +236,17 @@ try: safe_mode = False md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode) return md.convert(text) - except ImportError: apply_markdown = None -# Yaml is optional -try: - import yaml -except ImportError: - yaml = None - - -# XML is optional -try: - import defusedxml.ElementTree as etree -except ImportError: - etree = None - -# OAuth is optional -try: - # Note: The `oauth2` package actually provides oauth1.0a support. Urg. - import oauth2 as oauth -except ImportError: - oauth = None - -# OAuth is optional -try: - import oauth_provider - from oauth_provider.store import store as oauth_provider_store -except (ImportError, ImproperlyConfigured): - oauth_provider = None - oauth_provider_store = None - -# OAuth 2 support is optional -try: - import provider.oauth2 as oauth2_provider - from provider.oauth2 import models as oauth2_provider_models - from provider.oauth2 import forms as oauth2_provider_forms - from provider import scope as oauth2_provider_scope - from provider import constants as oauth2_constants - from provider import __version__ as provider_version - if provider_version in ('0.2.3', '0.2.4'): - # 0.2.3 and 0.2.4 are supported version that do not support - # timezone aware datetimes - import datetime - provider_now = datetime.datetime.now - else: - # Any other supported version does use timezone aware datetimes - from django.utils.timezone import now as provider_now -except ImportError: - oauth2_provider = None - oauth2_provider_models = None - oauth2_provider_forms = None - oauth2_provider_scope = None - oauth2_constants = None - provider_now = None - -# Handle lazy strings -from django.utils.functional import Promise - +# `separators` argument to `json.dumps()` differs between 2.x and 3.x +# See: http://bugs.python.org/issue22767 if six.PY3: - def is_non_str_iterable(obj): - if (isinstance(obj, str) or - (isinstance(obj, Promise) and obj._delegate_text)): - return False - return hasattr(obj, '__iter__') + SHORT_SEPARATORS = (',', ':') + LONG_SEPARATORS = (', ', ': ') + INDENT_SEPARATORS = (',', ': ') else: - def is_non_str_iterable(obj): - return hasattr(obj, '__iter__') + SHORT_SEPARATORS = (b',', b':') + LONG_SEPARATORS = (b', ', b': ') + INDENT_SEPARATORS = (b',', b': ') |
