150 lines
5.9 KiB
Python
150 lines
5.9 KiB
Python
from django.conf import settings
|
|
from django.http import FileResponse, Http404, HttpResponse
|
|
from django.shortcuts import get_object_or_404, redirect
|
|
from django.template.response import TemplateResponse
|
|
from django.urls import reverse
|
|
from django.utils.http import url_has_allowed_host_and_scheme
|
|
from django.views.decorators.http import etag
|
|
|
|
from wagtail import hooks
|
|
from wagtail.documents import get_document_model
|
|
from wagtail.documents.models import document_served
|
|
from wagtail.forms import PasswordViewRestrictionForm
|
|
from wagtail.models import CollectionViewRestriction
|
|
from wagtail.utils import sendfile_streaming_backend
|
|
from wagtail.utils.sendfile import sendfile
|
|
|
|
|
|
def document_etag(request, document_id, document_filename):
|
|
Document = get_document_model()
|
|
if hasattr(Document, "file_hash"):
|
|
return (
|
|
Document.objects.filter(id=document_id)
|
|
.values_list("file_hash", flat=True)
|
|
.first()
|
|
)
|
|
|
|
|
|
@etag(document_etag)
|
|
def serve(request, document_id, document_filename):
|
|
Document = get_document_model()
|
|
doc = get_object_or_404(Document, id=document_id)
|
|
|
|
# We want to ensure that the document filename provided in the URL matches the one associated with the considered
|
|
# document_id. If not we can't be sure that the document the user wants to access is the one corresponding to the
|
|
# <document_id, document_filename> pair.
|
|
if doc.filename != document_filename:
|
|
raise Http404("This document does not match the given filename.")
|
|
|
|
for fn in hooks.get_hooks("before_serve_document"):
|
|
result = fn(doc, request)
|
|
if isinstance(result, HttpResponse):
|
|
return result
|
|
|
|
# Send document_served signal
|
|
document_served.send(sender=Document, instance=doc, request=request)
|
|
|
|
try:
|
|
local_path = doc.file.path
|
|
except NotImplementedError:
|
|
local_path = None
|
|
|
|
try:
|
|
direct_url = doc.file.url
|
|
except NotImplementedError:
|
|
direct_url = None
|
|
|
|
serve_method = getattr(settings, "WAGTAILDOCS_SERVE_METHOD", None)
|
|
|
|
# If no serve method has been specified, select an appropriate default for the storage backend:
|
|
# redirect for remote storages (i.e. ones that provide a url but not a local path) and
|
|
# serve_view for all other cases
|
|
if serve_method is None:
|
|
if direct_url and not local_path:
|
|
serve_method = "redirect"
|
|
else:
|
|
serve_method = "serve_view"
|
|
|
|
if serve_method in ("redirect", "direct") and direct_url:
|
|
# Serve the file by redirecting to the URL provided by the underlying storage;
|
|
# this saves the cost of delivering the file via Python.
|
|
# For serve_method == 'direct', this view should not normally be reached
|
|
# (the document URL as used in links should point directly to the storage URL instead)
|
|
# but we handle it as a redirect to provide sensible fallback /
|
|
# backwards compatibility behaviour.
|
|
return redirect(direct_url)
|
|
|
|
if local_path:
|
|
# Use wagtail.utils.sendfile to serve the file;
|
|
# this provides support for mimetypes, if-modified-since and django-sendfile backends
|
|
|
|
sendfile_opts = {
|
|
"attachment": (doc.content_disposition != "inline"),
|
|
"attachment_filename": doc.filename,
|
|
"mimetype": doc.content_type,
|
|
}
|
|
if not hasattr(settings, "SENDFILE_BACKEND"):
|
|
# Fallback to streaming backend if user hasn't specified SENDFILE_BACKEND
|
|
sendfile_opts["backend"] = sendfile_streaming_backend.sendfile
|
|
|
|
response = sendfile(request, local_path, **sendfile_opts)
|
|
|
|
else:
|
|
# We are using a storage backend which does not expose filesystem paths
|
|
# (e.g. storages.backends.s3boto.S3BotoStorage) AND the developer has not allowed
|
|
# redirecting to the file url directly.
|
|
# Fall back on pre-sendfile behaviour of reading the file content and serving it
|
|
# as a FileResponse
|
|
response = FileResponse(doc.file, doc.content_type)
|
|
|
|
# set filename and filename* to handle non-ascii characters in filename
|
|
# see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
|
|
response["Content-Disposition"] = doc.content_disposition
|
|
|
|
# FIXME: storage backends are not guaranteed to implement 'size'
|
|
response["Content-Length"] = doc.file.size
|
|
|
|
# Add a CSP header to prevent inline execution
|
|
if getattr(settings, "WAGTAILDOCS_BLOCK_EMBEDDED_CONTENT", True):
|
|
response["Content-Security-Policy"] = "default-src 'none'"
|
|
|
|
# Prevent browsers from auto-detecting the content-type of a document
|
|
response["X-Content-Type-Options"] = "nosniff"
|
|
|
|
return response
|
|
|
|
|
|
def authenticate_with_password(request, restriction_id):
|
|
"""
|
|
Handle a submission of PasswordViewRestrictionForm to grant view access over a
|
|
subtree that is protected by a PageViewRestriction
|
|
"""
|
|
restriction = get_object_or_404(CollectionViewRestriction, id=restriction_id)
|
|
|
|
if request.method == "POST":
|
|
form = PasswordViewRestrictionForm(request.POST, instance=restriction)
|
|
if form.is_valid():
|
|
return_url = form.cleaned_data["return_url"]
|
|
|
|
if not url_has_allowed_host_and_scheme(
|
|
return_url, request.get_host(), request.is_secure()
|
|
):
|
|
return_url = settings.LOGIN_REDIRECT_URL
|
|
|
|
restriction.mark_as_passed(request)
|
|
return redirect(return_url)
|
|
else:
|
|
form = PasswordViewRestrictionForm(instance=restriction)
|
|
|
|
action_url = reverse(
|
|
"wagtaildocs_authenticate_with_password", args=[restriction.id]
|
|
)
|
|
|
|
password_required_template = getattr(
|
|
settings,
|
|
"WAGTAILDOCS_PASSWORD_REQUIRED_TEMPLATE",
|
|
"wagtaildocs/password_required.html",
|
|
)
|
|
|
|
context = {"form": form, "action_url": action_url}
|
|
return TemplateResponse(request, password_required_template, context)
|