From bfcecbffd37a68b5fffe81954c17aedeacb1ba22 Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Mon, 21 Jul 2008 07:57:10 +0000 Subject: [PATCH] Changed/fixed the way Django handles SCRIPT_NAME and PATH_INFO (or equivalents). Basically, URL resolving will only use the PATH_INFO and the SCRIPT_NAME will be prepended by reverse() automatically. Allows for more portable development and installation. Also exposes SCRIPT_NAME in the HttpRequest instance. There are a number of cases where things don't work completely transparently, so mod_python and fastcgi users should read the relevant docs. Fixed #285, #1516, #3414. git-svn-id: http://code.djangoproject.com/svn/django/trunk@8015 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 3 +++ django/core/handlers/base.py | 28 ++++++++++++++++++- django/core/handlers/modpython.py | 20 ++++++++++++-- django/core/handlers/wsgi.py | 13 ++++++--- django/core/urlresolvers.py | 33 ++++++++++++++++++++--- django/http/__init__.py | 2 ++ django/test/client.py | 2 +- django/utils/thread_support.py | 12 +++++++++ django/utils/translation/trans_real.py | 13 +-------- docs/fastcgi.txt | 36 ++++++++++++++++++++++--- docs/modpython.txt | 20 ++++++++++++++ docs/settings.txt | 10 +++++++ tests/regressiontests/requests/tests.py | 5 ++-- 13 files changed, 170 insertions(+), 27 deletions(-) create mode 100644 django/utils/thread_support.py diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 2c9720da10..797841d297 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -188,6 +188,9 @@ APPEND_SLASH = True # Whether to prepend the "www." subdomain to URLs that don't have it. PREPEND_WWW = False +# Override the server-derived value of SCRIPT_NAME +FORCE_SCRIPT_NAME = None + # List of compiled regular expression objects representing User-Agent strings # that are not allowed to visit any page, systemwide. Use this for bad # robots/crawlers. Here are a few examples: diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index f63b28dc66..214032e318 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -3,6 +3,7 @@ import sys from django import http from django.core import signals from django.dispatch import dispatcher +from django.utils.encoding import force_unicode class BaseHandler(object): # Changes that are always applied to a response (in this order). @@ -73,7 +74,8 @@ class BaseHandler(object): resolver = urlresolvers.RegexURLResolver(r'^/', urlconf) try: - callback, callback_args, callback_kwargs = resolver.resolve(request.path) + callback, callback_args, callback_kwargs = resolver.resolve( + request.path_info) # Apply view middleware for middleware_method in self._view_middleware: @@ -170,3 +172,27 @@ class BaseHandler(object): response = func(request, response) return response +def get_script_name(environ): + """ + Returns the equivalent of the HTTP request's SCRIPT_NAME environment + variable. If Apache mod_rewrite has been used, returns what would have been + the script name prior to any rewriting (so it's the script name as seen + from the client's perspective), unless DJANGO_USE_POST_REWRITE is set (to + anything). + """ + from django.conf import settings + if settings.FORCE_SCRIPT_NAME is not None: + return force_unicode(settings.FORCE_SCRIPT_NAME) + + # If Apache's mod_rewrite had a whack at the URL, Apache set either + # SCRIPT_URL or REDIRECT_URL to the full resource URL before applying any + # rewrites. Unfortunately not every webserver (lighttpd!) passes this + # information through all the time, so FORCE_SCRIPT_NAME, above, is still + # needed. + script_url = environ.get('SCRIPT_URL', u'') + if not script_url: + script_url = environ.get('REDIRECT_URL', u'') + if script_url: + return force_unicode(script_url[:-len(environ.get('PATH_INFO', ''))]) + return force_unicode(environ.get('SCRIPT_NAME', u'')) + diff --git a/django/core/handlers/modpython.py b/django/core/handlers/modpython.py index 332df6f54c..d407476acf 100644 --- a/django/core/handlers/modpython.py +++ b/django/core/handlers/modpython.py @@ -4,6 +4,7 @@ from pprint import pformat from django import http from django.core import signals from django.core.handlers.base import BaseHandler +from django.core.urlresolvers import set_script_prefix from django.dispatch import dispatcher from django.utils import datastructures from django.utils.encoding import force_unicode, smart_str @@ -15,7 +16,21 @@ from django.utils.encoding import force_unicode, smart_str class ModPythonRequest(http.HttpRequest): def __init__(self, req): self._req = req + # FIXME: This isn't ideal. The request URI may be encoded (it's + # non-normalized) slightly differently to the "real" SCRIPT_NAME + # and PATH_INFO values. This causes problems when we compute path_info, + # below. For now, don't use script names that will be subject to + # encoding/decoding. self.path = force_unicode(req.uri) + root = req.get_options().get('django.root', '') + self.django_root = root + # req.path_info isn't necessarily computed correctly in all + # circumstances (it's out of mod_python's control a bit), so we use + # req.uri and some string manipulations to get the right value. + if root and req.uri.startswith(root): + self.path_info = force_unicode(req.uri[len(root):]) + else: + self.path_info = self.path def __repr__(self): # Since this is called as part of error handling, we need to be very @@ -100,7 +115,7 @@ class ModPythonRequest(http.HttpRequest): 'CONTENT_LENGTH': self._req.clength, # This may be wrong 'CONTENT_TYPE': self._req.content_type, # This may be wrong 'GATEWAY_INTERFACE': 'CGI/1.1', - 'PATH_INFO': self._req.path_info, + 'PATH_INFO': self.path_info, 'PATH_TRANSLATED': None, # Not supported 'QUERY_STRING': self._req.args, 'REMOTE_ADDR': self._req.connection.remote_ip, @@ -108,7 +123,7 @@ class ModPythonRequest(http.HttpRequest): 'REMOTE_IDENT': self._req.connection.remote_logname, 'REMOTE_USER': self._req.user, 'REQUEST_METHOD': self._req.method, - 'SCRIPT_NAME': None, # Not supported + 'SCRIPT_NAME': self.django_root, 'SERVER_NAME': self._req.server.server_hostname, 'SERVER_PORT': self._req.server.port, 'SERVER_PROTOCOL': self._req.protocol, @@ -153,6 +168,7 @@ class ModPythonHandler(BaseHandler): if self._request_middleware is None: self.load_middleware() + set_script_prefix(req.get_options().get('django.root', '')) dispatcher.send(signal=signals.request_started) try: try: diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 795f139042..ca8dd4565f 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -7,7 +7,8 @@ except ImportError: from django import http from django.core import signals -from django.core.handlers.base import BaseHandler +from django.core.handlers import base +from django.core.urlresolvers import set_script_prefix from django.dispatch import dispatcher from django.utils import datastructures from django.utils.encoding import force_unicode @@ -74,9 +75,14 @@ def safe_copyfileobj(fsrc, fdst, length=16*1024, size=0): class WSGIRequest(http.HttpRequest): def __init__(self, environ): + script_name = base.get_script_name(environ) + path_info = force_unicode(environ.get('PATH_INFO', '/')) self.environ = environ - self.path = force_unicode(environ['PATH_INFO']) + self.path_info = path_info + self.path = '%s%s' % (script_name, path_info) self.META = environ + self.META['PATH_INFO'] = path_info + self.META['SCRIPT_NAME'] = script_name self.method = environ['REQUEST_METHOD'].upper() def __repr__(self): @@ -178,7 +184,7 @@ class WSGIRequest(http.HttpRequest): REQUEST = property(_get_request) raw_post_data = property(_get_raw_post_data) -class WSGIHandler(BaseHandler): +class WSGIHandler(base.BaseHandler): initLock = Lock() request_class = WSGIRequest @@ -194,6 +200,7 @@ class WSGIHandler(BaseHandler): self.load_middleware() self.initLock.release() + set_script_prefix(base.get_script_name(environ)) dispatcher.send(signal=signals.request_started) try: try: diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index ff0bcbcfea..4ec725cb38 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -7,11 +7,13 @@ a string) and returns a tuple in this format: (view_function, function_args, function_kwargs) """ +import re + from django.http import Http404 from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.utils.encoding import iri_to_uri, force_unicode, smart_str from django.utils.functional import memoize -import re +from django.utils.thread_support import currentThread try: reversed @@ -21,6 +23,11 @@ except NameError: _resolver_cache = {} # Maps urlconf modules to RegexURLResolver instances. _callable_cache = {} # Maps view and url pattern names to their view functions. +# SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for +# the current thread (which is the only one we ever access), it is assumed to +# be empty. +_prefixes = {} + class Resolver404(Http404): pass @@ -291,13 +298,33 @@ class RegexURLResolver(object): def resolve(path, urlconf=None): return get_resolver(urlconf).resolve(path) -def reverse(viewname, urlconf=None, args=None, kwargs=None): +def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None): args = args or [] kwargs = kwargs or {} - return iri_to_uri(u'/' + get_resolver(urlconf).reverse(viewname, *args, **kwargs)) + if prefix is None: + prefix = get_script_prefix() + return iri_to_uri(u'%s%s' % (prefix, get_resolver(urlconf).reverse(viewname, + *args, **kwargs))) def clear_url_caches(): global _resolver_cache global _callable_cache _resolver_cache.clear() _callable_cache.clear() + +def set_script_prefix(prefix): + """ + Sets the script prefix for the current thread. + """ + if not prefix.endswith('/'): + prefix += '/' + _prefixes[currentThread()] = prefix + +def get_script_prefix(): + """ + Returns the currently active script prefix. Useful for client code that + wishes to construct their own URLs manually (although accessing the request + instance is normally going to be a lot cleaner). + """ + return _prefixes.get(currentThread(), u'/') + diff --git a/django/http/__init__.py b/django/http/__init__.py index ef15479983..fe0b93edcf 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -31,6 +31,7 @@ class HttpRequest(object): def __init__(self): self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {} self.path = '' + self.path_info = '' self.method = None def __repr__(self): @@ -442,3 +443,4 @@ def str_to_unicode(s, encoding): return unicode(s, encoding, 'replace') else: return s + diff --git a/django/test/client.py b/django/test/client.py index 47c12a4ca1..7d621449ea 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -190,7 +190,7 @@ class Client: 'PATH_INFO': '/', 'QUERY_STRING': '', 'REQUEST_METHOD': 'GET', - 'SCRIPT_NAME': None, + 'SCRIPT_NAME': '', 'SERVER_NAME': 'testserver', 'SERVER_PORT': 80, 'SERVER_PROTOCOL': 'HTTP/1.1', diff --git a/django/utils/thread_support.py b/django/utils/thread_support.py new file mode 100644 index 0000000000..2b97d165ca --- /dev/null +++ b/django/utils/thread_support.py @@ -0,0 +1,12 @@ +""" +Code used in a couple of places to work with the current thread's environment. +Current users include i18n and request prefix handling. +""" + +try: + import threading + currentThread = threading.currentThread +except ImportError: + def currentThread(): + return "no threading" + diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 451420c7d8..93bb3c9bf9 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -8,18 +8,7 @@ import gettext as gettext_module from cStringIO import StringIO from django.utils.safestring import mark_safe, SafeData - -try: - import threading - hasThreads = True -except ImportError: - hasThreads = False - -if hasThreads: - currentThread = threading.currentThread -else: - def currentThread(): - return 'no threading' +from django.utils.thread_support import currentThread # Translations are cached in a dictionary for every language+app tuple. # The active translations are stored by threadid to make them thread local. diff --git a/docs/fastcgi.txt b/docs/fastcgi.txt index 78ee9d408c..b0490171b6 100644 --- a/docs/fastcgi.txt +++ b/docs/fastcgi.txt @@ -79,9 +79,9 @@ your ``manage.py`` is), and then run ``manage.py`` with the ``runfcgi`` option:: If you specify ``help`` as the only option after ``runfcgi``, it'll display a list of all the available options. -You'll need to specify either a ``socket``, ``protocol`` or both ``host`` and ``port``. -Then, when you set up your Web server, you'll just need to point it at the host/port -or socket you specified when starting the FastCGI server. +You'll need to specify either a ``socket``, ``protocol`` or both ``host`` and +``port``. Then, when you set up your Web server, you'll just need to point it +at the host/port or socket you specified when starting the FastCGI server. Protocols --------- @@ -209,6 +209,9 @@ This is probably the most common case, if you're using Django's admin site:: .. _mod_rewrite: http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html +Django will automatically use the pre-rewrite version of the URL when +constructing URLs with the ``{% url %}`` template tag (and similar methods). + lighttpd setup ============== @@ -336,3 +339,30 @@ detailed above. .. _modpython: ../modpython/#serving-the-admin-files +Forcing the URL prefix to a particular value +============================================ + +Because many of these fastcgi-based solutions require rewriting the URL at +some point inside the webserver, the path information that Django sees may not +resemble the original URL that was passed in. This is a problem if the Django +application is being served from under a particular prefix and you want your +URLs from the ``{% url %}`` tag to look like the prefix, rather than the +rewritten version, which might contain, for example, ``mysite.fcgi``. + +Django makes a good attempt to work out what the real script name prefix +should be. In particular, if the webserver sets the ``SCRIPT_URL`` (specific +to Apache's mod_rewrite), or ``REDIRECT_URL`` (set by a few servers, including +Apache + mod_rewrite in some situations), Django will work out the original +prefix automatically. + +In the cases where Django cannot work out the prefix correctly and where you +wan the original value to be used in URLs, you can set the +``FORCE_SCRIPT_NAME`` setting in your main ``settings`` file. This sets the +script name uniformly for every URL served via that settings file. Thus you'll +need to use different settings files is you want different sets of URLs to +have different script names in this case, but that is a rare situation. + +As an example of how to use it, if your Django configuration is serving all of +the URLs under ``'/'`` and you wanted to use this setting, you would set +``FORCE_SCRIPT_NAME = ''`` in your settings file. + diff --git a/docs/modpython.txt b/docs/modpython.txt index aa1acf5864..44de0e1bd2 100644 --- a/docs/modpython.txt +++ b/docs/modpython.txt @@ -35,6 +35,7 @@ Then edit your ``httpd.conf`` file and add the following:: SetHandler python-program PythonHandler django.core.handlers.modpython SetEnv DJANGO_SETTINGS_MODULE mysite.settings + PythonOption django.root /mysite PythonDebug On @@ -45,6 +46,24 @@ This tells Apache: "Use mod_python for any URL at or under '/mysite/', using the Django mod_python handler." It passes the value of ``DJANGO_SETTINGS_MODULE`` so mod_python knows which settings to use. +**New in Django development version:** Because mod_python does not know we are +serving this site from underneath the ``/mysite/`` prefix, this value needs to +be passed through to the mod_python handler in Django, via the ``PythonOption +django.root ...`` line. The value set on that line (the last item) should +match the string given in the ```` directive. The effect of this +is that Django will automatically strip the ``/mysite`` string from the front +of any URLs before matching them against your ``URLConf`` patterns. If you +later move your site to live under ``/mysite2``, you will not have to change +anything except the ``django.root`` option in the config file. + +When using ``django.root`` you should make sure that what's left, after the +prefix has been removed, begins with a slash. Your URLConf patterns that are +expecting an initial slash will then work correctly. In the above example, +since we want to send things like ``/mysite/admin/`` to ``/admin/``, we need +to remove the string ``/mysite`` from the beginning, so that is the +``django.root`` value. It would be an error to use ``/mysite/`` (with a +trailing slash) in this case. + Note that we're using the ```` directive, not the ```` directive. The latter is used for pointing at places on your filesystem, whereas ```` points at places in the URL structure of a Web site. @@ -59,6 +78,7 @@ computer, you'll have to tell mod_python where your project can be found: SetHandler python-program PythonHandler django.core.handlers.modpython SetEnv DJANGO_SETTINGS_MODULE mysite.settings + PythonOption django.root /mysite PythonDebug On **PythonPath "['/path/to/project'] + sys.path"** diff --git a/docs/settings.txt b/docs/settings.txt index fbe23b5e88..2c9e56abd4 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -578,6 +578,16 @@ these paths should use Unix-style forward slashes, even on Windows. See .. _Testing Django Applications: ../testing/ +FORCE_SCRIPT_NAME +------------------ + +Default: ``None`` + +If not ``None``, this will be used as the value of the ``SCRIPT_NAME`` +environment variable in any HTTP request. This setting can be used to override +the server-provided value of ``SCRIPT_NAME``, which may be a rewritten version +of the preferred value or not supplied at all. + IGNORABLE_404_ENDS ------------------ diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py index aaaef1d8b0..a091113515 100644 --- a/tests/regressiontests/requests/tests.py +++ b/tests/regressiontests/requests/tests.py @@ -20,8 +20,9 @@ META:{...}> ... def __init__(self, *args, **kwargs): ... super(FakeModPythonRequest, self).__init__(*args, **kwargs) ... self._get = self._post = self._meta = self._cookies = {} ->>> class Dummy: pass -... +>>> class Dummy: +... def get_options(self): +... return {} >>> req = Dummy() >>> req.uri = 'bogus' >>> print repr(FakeModPythonRequest(req))