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
This commit is contained in:
Malcolm Tredinnick 2008-07-21 07:57:10 +00:00
parent ca7ee4be17
commit bfcecbffd3
13 changed files with 170 additions and 27 deletions

View File

@ -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:

View File

@ -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''))

View File

@ -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:

View File

@ -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:

View File

@ -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'/')

View File

@ -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

View File

@ -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',

View File

@ -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"

View File

@ -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.

View File

@ -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.

View File

@ -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
</Location>
@ -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 ``<Location ...>`` 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 ``<Location>`` directive, not the ``<Directory>``
directive. The latter is used for pointing at places on your filesystem,
whereas ``<Location>`` 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"**
</Location>

View File

@ -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
------------------

View File

@ -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))