Introduce make_preview_request method to supersede dummy_request

Django-1.10-style middleware inherently doesn't support applying middleware to the request independently of running the view function, as the design of dummy_request requires. The current implementation of dummy_request unwittingly works around this by running the entire request/response cycle on the page's live URL (regardless of whether the page actually exists there at that moment), throwing away the response, and returning the request object to be used a second time (at which point it will be hopefully be populated with middleware-supplied attributes such as request.user and request.site - unless something caused the middleware to abort).

The new make_preview_request method wraps the call to serve_preview inside the middleware processing, so there is no longer a bogus 'background' request and no response has to be thrown away (meaning that any response returned by middleware will be correctly returned).

Fixes #3546
This commit is contained in:
Matt Westcott 2019-07-10 23:17:18 +02:00
parent 1d9e0acfb8
commit e400d8d0c8
4 changed files with 58 additions and 14 deletions

View file

@ -1,7 +1,6 @@
import datetime
import logging
import os
import unittest
from itertools import chain
from unittest import mock
@ -5276,7 +5275,6 @@ class TestDraftAccess(TestCase, WagtailTestUtils):
# User can view
self.assertEqual(response.status_code, 200)
@unittest.expectedFailure
def test_middleware_response_is_returned(self):
"""
If middleware returns a response while serving a page preview, that response should be

View file

@ -586,7 +586,7 @@ def view_draft(request, page_id):
perms = page.permissions_for_user(request.user)
if not (perms.can_publish() or perms.can_edit()):
raise PermissionDenied
return page.serve_preview(page.dummy_request(request), page.default_preview_mode)
return page.make_preview_request(request, page.default_preview_mode)
class PreviewOnEdit(View):
@ -646,8 +646,7 @@ class PreviewOnEdit(View):
form.save(commit=False)
preview_mode = request.GET.get('mode', page.default_preview_mode)
return page.serve_preview(page.dummy_request(request),
preview_mode)
return page.make_preview_request(request, preview_mode)
class PreviewOnCreate(PreviewOnEdit):
@ -1055,11 +1054,9 @@ def preview_for_moderation(request, revision_id):
page = revision.as_page_object()
request.revision_id = revision_id
# pass in the real user request rather than page.dummy_request(), so that request.user
# and request.revision_id will be picked up by the wagtail user bar
return page.serve_preview(request, page.default_preview_mode)
return page.make_preview_request(request, page.default_preview_mode, extra_request_attrs={
'revision_id': revision_id
})
@require_POST
@ -1180,7 +1177,7 @@ 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(request), page.default_preview_mode)
return revision_page.make_preview_request(request, page.default_preview_mode)
def revisions_compare(request, page_id, revision_id_a, revision_id_b):

View file

@ -3,6 +3,7 @@ import logging
from collections import defaultdict
from io import StringIO
from urllib.parse import urlparse
from warnings import warn
from django.conf import settings
from django.contrib.auth.models import Group, Permission
@ -32,6 +33,8 @@ from wagtail.core.sites import get_site_for_hostname
from wagtail.core.url_routing import RouteResult
from wagtail.core.utils import WAGTAIL_APPEND_SLASH, camelcase_to_underscore, resolve_model_string
from wagtail.search import index
from wagtail.utils.deprecation import RemovedInWagtail29Warning
logger = logging.getLogger('wagtail.core')
@ -1216,16 +1219,51 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
user_perms = UserPagePermissionsProxy(user)
return user_perms.for_page(self)
def dummy_request(self, original_request=None, **meta):
def make_preview_request(self, original_request=None, preview_mode=None, extra_request_attrs=None):
"""
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
Simulate a request to this page, by constructing a fake HttpRequest object that is (as far
as possible) representative of a real request to this page's front-end URL, and invoking
serve_preview with that request (and the given preview_mode).
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.
"""
dummy_meta = self._get_dummy_headers(original_request)
request = WSGIRequest(dummy_meta)
# Add a flag to let middleware know that this is a dummy request.
request.is_dummy = True
if extra_request_attrs:
for k, v in extra_request_attrs.items():
setattr(request, k, v)
page = self
# Build a custom django.core.handlers.BaseHandler subclass that invokes serve_preview as
# the eventual view function called at the end of the middleware chain, rather than going
# through the URL resolver
class Handler(BaseHandler):
def _get_response(self, request):
response = page.serve_preview(request, preview_mode)
if hasattr(response, 'render') and callable(response.render):
response = response.render()
return response
# Invoke this custom handler.
handler = Handler()
handler.load_middleware()
return handler.get_response(request)
def _get_dummy_headers(self, original_request=None):
"""
Return a dict of META information to be included in a faked HttpRequest object to pass to
serve_preview.
"""
url = self.full_url
if url:
url_info = urlparse(url)
@ -1279,6 +1317,16 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
if header in original_request.META:
dummy_values[header] = original_request.META[header]
return dummy_values
def dummy_request(self, original_request=None, **meta):
warn(
"Page.dummy_request is deprecated. Use Page.make_preview_request instead",
category=RemovedInWagtail29Warning
)
dummy_values = self._get_dummy_headers(original_request)
# Add additional custom metadata sent by the caller.
dummy_values.update(**meta)

View file

@ -158,6 +158,7 @@ PASSWORD_HASHERS = (
'django.contrib.auth.hashers.MD5PasswordHasher', # don't use the intentionally slow default password hasher
)
ALLOWED_HOSTS = ['localhost', 'testserver']
WAGTAILSEARCH_BACKENDS = {
'default': {