Commits

Anonymous committed e9c55b0

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.

  • Participants
  • Parent commits 1aab383

Comments (0)

Files changed (13)

django/conf/global_settings.py

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

django/core/handlers/base.py

 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).
 
         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:
             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''))
+

django/core/handlers/modpython.py

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

django/core/handlers/wsgi.py

 
 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
 
 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):
     REQUEST = property(_get_request)
     raw_post_data = property(_get_raw_post_data)
 
-class WSGIHandler(BaseHandler):
+class WSGIHandler(base.BaseHandler):
     initLock = Lock()
     request_class = WSGIRequest
 
                 self.load_middleware()
             self.initLock.release()
 
+        set_script_prefix(base.get_script_name(environ))
         dispatcher.send(signal=signals.request_started)
         try:
             try:

django/core/urlresolvers.py

     (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
 _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
 
 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'/')
+

django/http/__init__.py

     def __init__(self):
         self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {}
         self.path = ''
+        self.path_info = ''
         self.method = None
 
     def __repr__(self):
         return unicode(s, encoding, 'replace')
     else:
         return s
+

django/test/client.py

             'PATH_INFO':         '/',
             'QUERY_STRING':      '',
             'REQUEST_METHOD':    'GET',
-            'SCRIPT_NAME':       None,
+            'SCRIPT_NAME':       '',
             'SERVER_NAME':       'testserver',
             'SERVER_PORT':       80,
             'SERVER_PROTOCOL':   'HTTP/1.1',

django/utils/thread_support.py

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

django/utils/translation/trans_real.py

 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.
 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
 ---------
 
 .. _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
 ==============
 
 
 .. _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.
+

docs/modpython.txt

         SetHandler python-program
         PythonHandler django.core.handlers.modpython
         SetEnv DJANGO_SETTINGS_MODULE mysite.settings
+        PythonOption django.root /mysite
         PythonDebug On
     </Location>
 
 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.
         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>

docs/settings.txt

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

tests/regressiontests/requests/tests.py

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