From cbabc3d9c7a5b9e6bb6f160ad5c40e0ca01aaaeb Mon Sep 17 00:00:00 2001 From: Robert Rollins Date: Mon, 2 May 2016 13:58:57 -0700 Subject: [PATCH] Page.dummy_request() now takes an optional request object argument. When building a dummy request, you can now pass in the original request object to add additional information to the dummy. Currently, that includes the following headers: REMOTE_ADDR HTTP_X_FORWARDED_FOR HTTP_COOKIE HTTP_USER_AGENT More may be added later. This changes ensures that middleware which work on the client IP aren't flumuxed by its absense, and also makes it possible for previews to be rendered as the logged in user (they had previously been rendered using an AnnonymousUser). Because the user's logged in state is now detectable in a Page previews, the Wagtail userbar now hides itself explicitly during previews, rather than relying on the fact that previews used to be built with AnonymousUser. --- CHANGELOG.txt | 1 + docs/releases/1.6.rst | 1 + .../templatetags/wagtailuserbar.py | 5 ++++ wagtail/wagtailadmin/views/pages.py | 8 +++--- wagtail/wagtailcore/models.py | 25 ++++++++++++++++--- wagtail/wagtailcore/tests/test_page_model.py | 19 ++++++++++++++ 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index fa594087d..1f5e4fa35 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -22,6 +22,7 @@ Changelog * `wagtailforms.models.AbstractEmailForm` now supports multiple email recipients (Serafeim Papastefanos) * Added the ``include_block`` template tag for improved StreamField template inclusion (Matt Westcott) * Added ability to delete users through Settings -> Users (Vincent Audebert; thanks also to Ludolf Takens and Tobias Schmidt for alternative implementations) + * Page previews now pass additional HTTP headers, to simulate the page being viewed by the logged-in user and avoid clashes with middleware (Robert Rollins) * Fix: Email templates and document uploader now support custom `STATICFILES_STORAGE` (Jonny Scholes) * Fix: Removed alignment options (deprecated in HTML and not rendered by Wagtail) from `TableBlock` context menu (Moritz Pfeiffer) * Fix: Fixed incorrect CSS path on ModelAdmin's "choose a parent page" view diff --git a/docs/releases/1.6.rst b/docs/releases/1.6.rst index fda4abe9e..f6ef31218 100644 --- a/docs/releases/1.6.rst +++ b/docs/releases/1.6.rst @@ -37,6 +37,7 @@ Minor features * ``wagtailforms.models.AbstractEmailForm`` now supports multiple email recipients (Serafeim Papastefanos) * Added the ``include_block`` template tag for improved StreamField template inclusion. See :doc:`/topics/streamfield` for documentation (Matt Westcott) * Added ability to delete users through Settings -> Users (Vincent Audebert; thanks also to Ludolf Takens and Tobias Schmidt for alternative implementations) + * Page previews now pass additional HTTP headers, to simulate the page being viewed by the logged-in user and avoid clashes with middleware (Robert Rollins) Bug fixes diff --git a/wagtail/wagtailadmin/templatetags/wagtailuserbar.py b/wagtail/wagtailadmin/templatetags/wagtailuserbar.py index 8c7cd1413..a8e62cdd0 100644 --- a/wagtail/wagtailadmin/templatetags/wagtailuserbar.py +++ b/wagtail/wagtailadmin/templatetags/wagtailuserbar.py @@ -40,6 +40,11 @@ def wagtailuserbar(context, position='bottom-right'): if not request.user.has_perm('wagtailadmin.access_admin'): return '' + # Don't render if this is a preview. Since some routes can render the userbar without going through Page.serve(), + # request.is_preview might not be defined. + if getattr(request, 'is_preview', False): + return '' + # Only render if the context contains a variable referencing a saved page page = get_page_instance(context) if page is None: diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 6f45effb0..1f2e76208 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -496,7 +496,7 @@ def delete(request, page_id): def view_draft(request, page_id): page = get_object_or_404(Page, id=page_id).get_latest_revision_as_page() - return page.serve_preview(page.dummy_request(), page.default_preview_mode) + return page.serve_preview(page.dummy_request(request), page.default_preview_mode) def preview_on_edit(request, page_id): @@ -516,7 +516,7 @@ def preview_on_edit(request, page_id): page.full_clean() preview_mode = request.GET.get('mode', page.default_preview_mode) - response = page.serve_preview(page.dummy_request(), preview_mode) + response = page.serve_preview(page.dummy_request(request), preview_mode) response['X-Wagtail-Preview'] = 'ok' return response @@ -576,7 +576,7 @@ def preview_on_create(request, content_type_app_name, content_type_model_name, p page.path = Page._get_children_path_interval(parent_page.path)[1] preview_mode = request.GET.get('mode', page.default_preview_mode) - response = page.serve_preview(page.dummy_request(), preview_mode) + response = page.serve_preview(page.dummy_request(request), preview_mode) response['X-Wagtail-Preview'] = 'ok' return response @@ -1013,4 +1013,4 @@ def revisions_view(request, page_id, revision_id): revision = get_object_or_404(page.revisions, id=revision_id) revision_page = revision.as_page_object() - return revision_page.serve_preview(page.dummy_request(), page.default_preview_mode) + return revision_page.serve_preview(page.dummy_request(request), page.default_preview_mode) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 57dff11f2..a58ec4120 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -1168,12 +1168,15 @@ class Page(six.with_metaclass(PageBase, MP_Node, index.Indexed, ClusterableModel user_perms = UserPagePermissionsProxy(user) return user_perms.for_page(self) - def dummy_request(self): + def dummy_request(self, original_request=None, **meta): """ Construct a HttpRequest object that is, as far as possible, representative of ones that would receive this page as a response. Used for previewing / moderation and any other place where we want to display a view of this page in the admin interface without going through the regular page routing logic. + + If you pass in a real request object as original_request, additional information (e.g. client IP, cookies) + will be included in the dummy request. """ url = self.full_url if url: @@ -1195,14 +1198,30 @@ class Page(six.with_metaclass(PageBase, MP_Node, index.Indexed, ClusterableModel path = '/' port = 80 - request = WSGIRequest({ + dummy_values = { 'REQUEST_METHOD': 'GET', 'PATH_INFO': path, 'SERVER_NAME': hostname, 'SERVER_PORT': port, 'HTTP_HOST': hostname, 'wsgi.input': StringIO(), - }) + } + + # Add important values from the original request object, if it was provided. + if original_request: + if original_request.META.get('REMOTE_ADDR'): + dummy_values['REMOTE_ADDR'] = original_request.META['REMOTE_ADDR'] + if original_request.META.get('HTTP_X_FORWARDED_FOR'): + dummy_values['HTTP_X_FORWARDED_FOR'] = original_request.META['HTTP_X_FORWARDED_FOR'] + if original_request.META.get('HTTP_COOKIE'): + dummy_values['HTTP_COOKIE'] = original_request.META['HTTP_COOKIE'] + if original_request.META.get('HTTP_USER_AGENT'): + dummy_values['HTTP_USER_AGENT'] = original_request.META['HTTP_USER_AGENT'] + + # Add additional custom metadata sent by the caller. + dummy_values.update(**meta) + + request = WSGIRequest(dummy_values) # Apply middleware to the request - see http://www.mellowmorning.com/2011/04/18/mock-django-request-for-testing/ handler = BaseHandler() diff --git a/wagtail/wagtailcore/tests/test_page_model.py b/wagtail/wagtailcore/tests/test_page_model.py index 0f19e0dda..7d64302f8 100644 --- a/wagtail/wagtailcore/tests/test_page_model.py +++ b/wagtail/wagtailcore/tests/test_page_model.py @@ -10,6 +10,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.http import Http404, HttpRequest from django.test import Client, TestCase +from django.test.client import RequestFactory from django.test.utils import override_settings from wagtail.tests.testapp.models import ( @@ -1156,6 +1157,24 @@ class TestDummyRequest(TestCase): self.assertEqual(request.path, '/events/') self.assertEqual(request.META['HTTP_HOST'], 'localhost') + def test_dummy_request_for_accessible_page_with_original_request(self): + event_index = Page.objects.get(url_path='/home/events/') + original_headers = { + 'REMOTE_ADDR': '192.168.0.1', + 'HTTP_X_FORWARDED_FOR': '192.168.0.2,192.168.0.3', + 'HTTP_COOKIE': "test=1;blah=2", + 'HTTP_USER_AGENT': "Test Agent", + } + factory = RequestFactory(**original_headers) + original_request = factory.get('/home/events/') + request = event_index.dummy_request(original_request) + + # request should have the all the special headers we set in original_request + self.assertEqual(request.META['REMOTE_ADDR'], original_request.META['REMOTE_ADDR']) + self.assertEqual(request.META['HTTP_X_FORWARDED_FOR'], original_request.META['HTTP_X_FORWARDED_FOR']) + self.assertEqual(request.META['HTTP_COOKIE'], original_request.META['HTTP_COOKIE']) + self.assertEqual(request.META['HTTP_USER_AGENT'], original_request.META['HTTP_USER_AGENT']) + @override_settings(ALLOWED_HOSTS=['production.example.com']) def test_dummy_request_for_inaccessible_page_should_use_valid_host(self): root_page = Page.objects.get(url_path='/')