Description: fix unexpected code execution using reverse()
Origin: backport, https://github.com/django/django/commit/c1a8c420fe4b27fb2caf5e46d23b5712fc0ac535
Bug-Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-django/+bug/1309779
Forwarded: not-needed
Last-Update: 2014-05-09

--- a/django/core/urlresolvers.py
+++ b/django/core/urlresolvers.py
@@ -151,6 +151,10 @@
         self._reverse_dict = None
         self._namespace_dict = None
         self._app_dict = None
+        # set of dotted paths to all functions and classes that are used in
+        # urlpatterns
+        self._callback_strs = set()
+        self._populated = False
 
     def __repr__(self):
         return '<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern)
@@ -160,6 +164,15 @@
         namespaces = {}
         apps = {}
         for pattern in reversed(self.url_patterns):
+            if hasattr(pattern, '_callback_str'):
+                self._callback_strs.add(pattern._callback_str)
+            elif hasattr(pattern, '_callback'):
+                callback = pattern._callback
+                if not hasattr(callback, '__name__'):
+                    lookup_str = callback.__module__ + "." + callback.__class__.__name__
+                else:
+                    lookup_str = callback.__module__ + "." + callback.__name__
+                self._callback_strs.add(lookup_str)
             p_pattern = pattern.regex.pattern
             if p_pattern.startswith('^'):
                 p_pattern = p_pattern[1:]
@@ -180,6 +193,7 @@
                         namespaces[namespace] = (p_pattern + prefix, sub_pattern)
                     for app_name, namespace_list in pattern.app_dict.items():
                         apps.setdefault(app_name, []).extend(namespace_list)
+                    self._callback_strs.update(pattern._callback_strs)
             else:
                 bits = normalize(p_pattern)
                 lookups.appendlist(pattern.callback, (bits, p_pattern))
@@ -188,6 +202,7 @@
         self._reverse_dict = lookups
         self._namespace_dict = namespaces
         self._app_dict = apps
+        self._populated = True
 
     def _get_reverse_dict(self):
         if self._reverse_dict is None:
@@ -265,8 +280,13 @@
     def reverse(self, lookup_view, *args, **kwargs):
         if args and kwargs:
             raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
+
+        if not self._populated:
+            self._populate()
+
         try:
-            lookup_view = get_callable(lookup_view, True)
+            if lookup_view in self._callback_strs:
+                lookup_view = get_callable(lookup_view, True)
         except (ImportError, AttributeError), e:
             raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
         possibilities = self.reverse_dict.getlist(lookup_view)
--- /dev/null
+++ b/tests/regressiontests/urlpatterns_reverse/nonimported_module.py
@@ -0,0 +1,3 @@
+def view(request):
+    """Stub view"""
+    pass
--- a/tests/regressiontests/urlpatterns_reverse/tests.py
+++ b/tests/regressiontests/urlpatterns_reverse/tests.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 """
 Unit tests for reverse URL lookups.
 """
@@ -14,6 +15,8 @@
 ImproperlyConfigured: The included urlconf regressiontests.urlpatterns_reverse.no_urls doesn't have any patterns in it
 """}
 
+import sys
+
 import unittest
 
 from django.conf import settings
@@ -169,6 +172,25 @@
         self.assertEqual(res['Location'], '/foo/')
         res = redirect('http://example.com/')
         self.assertEqual(res['Location'], 'http://example.com/')
+        # Assert that we can redirect using UTF-8 strings
+        res = redirect('/æøå/abc/')
+        self.assertEqual(res['Location'], '/%C3%A6%C3%B8%C3%A5/abc/')
+        # Assert that no imports are attempted when dealing with a relative path
+        # (previously, the below would resolve in a UnicodeEncodeError from __import__ )
+        res = redirect('/æøå.abc/')
+        self.assertEqual(res['Location'], '/%C3%A6%C3%B8%C3%A5.abc/')
+        res = redirect('os.path')
+        self.assertEqual(res['Location'], 'os.path')
+
+    def test_no_illegal_imports(self):
+        # modules that are not listed in urlpatterns should not be importable
+        redirect("urlpatterns_reverse.nonimported_module.view")
+        self.failIf("urlpatterns_reverse.nonimported_module" in sys.modules)
+
+    def test_reverse_by_path_nested(self):
+        # Views that are added to urlpatterns using include() should be
+        # reversable by doted path.
+        self.assertEqual(reverse('regressiontests.urlpatterns_reverse.views.nested_view'), '/includes/nested_path/')
 
     def test_redirect_view_object(self):
         from views import absolute_kwargs_view
--- a/tests/regressiontests/urlpatterns_reverse/urls.py
+++ b/tests/regressiontests/urlpatterns_reverse/urls.py
@@ -3,6 +3,7 @@
 
 other_patterns = patterns('',
     url(r'non_path_include/$', empty_view, name='non_path_include'),
+    url(r'nested_path/$', 'regressiontests.urlpatterns_reverse.views.nested_view'),
 )
 
 urlpatterns = patterns('',
--- a/tests/regressiontests/urlpatterns_reverse/views.py
+++ b/tests/regressiontests/urlpatterns_reverse/views.py
@@ -6,3 +6,6 @@
 
 def absolute_kwargs_view(request, arg1=1, arg2=2):
     pass
+
+def nested_view(request):
+    pass
