serve.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. from warnings import warn
  2. from django.conf import settings
  3. from django.http import FileResponse, Http404, HttpResponse
  4. from django.shortcuts import get_object_or_404, redirect
  5. from django.template.response import TemplateResponse
  6. from django.urls import reverse
  7. from django.utils.http import url_has_allowed_host_and_scheme
  8. from django.views.decorators.http import etag
  9. from wagtail import hooks
  10. from wagtail.documents import get_document_model
  11. from wagtail.documents.models import document_served
  12. from wagtail.forms import PasswordViewRestrictionForm
  13. from wagtail.models import CollectionViewRestriction
  14. from wagtail.utils import sendfile_streaming_backend
  15. from wagtail.utils.deprecation import RemovedInWagtail70Warning
  16. from wagtail.utils.sendfile import sendfile
  17. def document_etag(request, document_id, document_filename):
  18. Document = get_document_model()
  19. if hasattr(Document, "file_hash"):
  20. return (
  21. Document.objects.filter(id=document_id)
  22. .values_list("file_hash", flat=True)
  23. .first()
  24. )
  25. @etag(document_etag)
  26. def serve(request, document_id, document_filename):
  27. Document = get_document_model()
  28. doc = get_object_or_404(Document, id=document_id)
  29. # We want to ensure that the document filename provided in the URL matches the one associated with the considered
  30. # document_id. If not we can't be sure that the document the user wants to access is the one corresponding to the
  31. # <document_id, document_filename> pair.
  32. if doc.filename != document_filename:
  33. raise Http404("This document does not match the given filename.")
  34. for fn in hooks.get_hooks("before_serve_document"):
  35. result = fn(doc, request)
  36. if isinstance(result, HttpResponse):
  37. return result
  38. # Send document_served signal
  39. document_served.send(sender=Document, instance=doc, request=request)
  40. try:
  41. local_path = doc.file.path
  42. except NotImplementedError:
  43. local_path = None
  44. try:
  45. direct_url = doc.file.url
  46. except NotImplementedError:
  47. direct_url = None
  48. serve_method = getattr(settings, "WAGTAILDOCS_SERVE_METHOD", None)
  49. # If no serve method has been specified, select an appropriate default for the storage backend:
  50. # redirect for remote storages (i.e. ones that provide a url but not a local path) and
  51. # serve_view for all other cases
  52. if serve_method is None:
  53. if direct_url and not local_path:
  54. serve_method = "redirect"
  55. else:
  56. serve_method = "serve_view"
  57. if serve_method in ("redirect", "direct") and direct_url:
  58. # Serve the file by redirecting to the URL provided by the underlying storage;
  59. # this saves the cost of delivering the file via Python.
  60. # For serve_method == 'direct', this view should not normally be reached
  61. # (the document URL as used in links should point directly to the storage URL instead)
  62. # but we handle it as a redirect to provide sensible fallback /
  63. # backwards compatibility behaviour.
  64. return redirect(direct_url)
  65. if local_path:
  66. # Use wagtail.utils.sendfile to serve the file;
  67. # this provides support for mimetypes, if-modified-since and django-sendfile backends
  68. sendfile_opts = {
  69. "attachment": (doc.content_disposition != "inline"),
  70. "attachment_filename": doc.filename,
  71. "mimetype": doc.content_type,
  72. }
  73. if not hasattr(settings, "SENDFILE_BACKEND"):
  74. # Fallback to streaming backend if user hasn't specified SENDFILE_BACKEND
  75. sendfile_opts["backend"] = sendfile_streaming_backend.sendfile
  76. response = sendfile(request, local_path, **sendfile_opts)
  77. else:
  78. # We are using a storage backend which does not expose filesystem paths
  79. # (e.g. storages.backends.s3boto.S3BotoStorage) AND the developer has not allowed
  80. # redirecting to the file url directly.
  81. # Fall back on pre-sendfile behaviour of reading the file content and serving it
  82. # as a FileResponse
  83. response = FileResponse(doc.file, doc.content_type)
  84. # set filename and filename* to handle non-ascii characters in filename
  85. # see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
  86. response["Content-Disposition"] = doc.content_disposition
  87. # FIXME: storage backends are not guaranteed to implement 'size'
  88. response["Content-Length"] = doc.file.size
  89. # Add a CSP header to prevent inline execution
  90. if getattr(settings, "WAGTAILDOCS_BLOCK_EMBEDDED_CONTENT", True):
  91. response["Content-Security-Policy"] = "default-src 'none'"
  92. # Prevent browsers from auto-detecting the content-type of a document
  93. response["X-Content-Type-Options"] = "nosniff"
  94. return response
  95. def authenticate_with_password(request, restriction_id):
  96. """
  97. Handle a submission of PasswordViewRestrictionForm to grant view access over a
  98. subtree that is protected by a PageViewRestriction
  99. """
  100. restriction = get_object_or_404(CollectionViewRestriction, id=restriction_id)
  101. if request.method == "POST":
  102. form = PasswordViewRestrictionForm(request.POST, instance=restriction)
  103. if form.is_valid():
  104. return_url = form.cleaned_data["return_url"]
  105. if not url_has_allowed_host_and_scheme(
  106. return_url, request.get_host(), request.is_secure()
  107. ):
  108. return_url = settings.LOGIN_REDIRECT_URL
  109. restriction.mark_as_passed(request)
  110. return redirect(return_url)
  111. else:
  112. form = PasswordViewRestrictionForm(instance=restriction)
  113. action_url = reverse(
  114. "wagtaildocs_authenticate_with_password", args=[restriction.id]
  115. )
  116. password_required_template = getattr(
  117. settings,
  118. "WAGTAILDOCS_PASSWORD_REQUIRED_TEMPLATE",
  119. "wagtaildocs/password_required.html",
  120. )
  121. if hasattr(settings, "DOCUMENT_PASSWORD_REQUIRED_TEMPLATE"):
  122. warn(
  123. "The `DOCUMENT_PASSWORD_REQUIRED_TEMPLATE` setting is deprecated - use `WAGTAILDOCS_PASSWORD_REQUIRED_TEMPLATE` instead.",
  124. category=RemovedInWagtail70Warning,
  125. )
  126. password_required_template = getattr(
  127. settings,
  128. "DOCUMENT_PASSWORD_REQUIRED_TEMPLATE",
  129. password_required_template,
  130. )
  131. context = {"form": form, "action_url": action_url}
  132. return TemplateResponse(request, password_required_template, context)