2020-06-11 00:54:34 +02:00
|
|
|
from mimetypes import guess_type
|
|
|
|
|
|
|
|
from django.conf import settings
|
|
|
|
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden, HttpResponseNotFound
|
2016-06-07 01:09:05 +02:00
|
|
|
from django.shortcuts import redirect
|
2019-10-02 00:10:30 +02:00
|
|
|
from django.utils.cache import patch_cache_control
|
2021-04-16 00:57:30 +02:00
|
|
|
from django.utils.translation import gettext as _
|
2020-06-11 00:54:34 +02:00
|
|
|
from django_sendfile import sendfile
|
2016-06-07 01:09:05 +02:00
|
|
|
|
2021-06-30 18:35:50 +02:00
|
|
|
from zerver.lib.exceptions import JsonableError
|
|
|
|
from zerver.lib.response import json_success
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.upload import (
|
|
|
|
INLINE_MIME_TYPES,
|
|
|
|
check_upload_within_quota,
|
|
|
|
generate_unauthed_file_access_url,
|
|
|
|
get_local_file_path,
|
|
|
|
get_local_file_path_id_from_token,
|
|
|
|
get_signed_upload_url,
|
|
|
|
upload_message_image_from_request,
|
|
|
|
)
|
2016-06-17 19:48:17 +02:00
|
|
|
from zerver.models import UserProfile, validate_attachment_request
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2016-06-07 01:09:05 +02:00
|
|
|
|
2020-04-08 00:27:24 +02:00
|
|
|
def serve_s3(request: HttpRequest, url_path: str, url_only: bool) -> HttpResponse:
|
|
|
|
url = get_signed_upload_url(url_path)
|
|
|
|
if url_only:
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data=dict(url=url))
|
2016-06-08 11:22:06 +02:00
|
|
|
|
2020-04-08 00:27:24 +02:00
|
|
|
return redirect(url)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-04-08 00:27:24 +02:00
|
|
|
def serve_local(request: HttpRequest, path_id: str, url_only: bool) -> HttpResponse:
|
2016-06-09 12:19:56 +02:00
|
|
|
local_path = get_local_file_path(path_id)
|
|
|
|
if local_path is None:
|
2021-02-12 08:20:45 +01:00
|
|
|
return HttpResponseNotFound("<p>File not found</p>")
|
2018-03-13 07:08:27 +01:00
|
|
|
|
2020-04-08 00:27:24 +02:00
|
|
|
if url_only:
|
|
|
|
url = generate_unauthed_file_access_url(path_id)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data=dict(url=url))
|
2020-04-08 00:27:24 +02:00
|
|
|
|
2018-03-13 07:08:27 +01:00
|
|
|
# Here we determine whether a browser should treat the file like
|
|
|
|
# an attachment (and thus clicking a link to it should download)
|
|
|
|
# or like a link (and thus clicking a link to it should display it
|
|
|
|
# in a browser tab). This is controlled by the
|
2019-08-12 01:52:09 +02:00
|
|
|
# Content-Disposition header; `django-sendfile2` sends the
|
2018-03-13 07:08:27 +01:00
|
|
|
# attachment-style version of that header if and only if the
|
|
|
|
# attachment argument is passed to it. For attachments,
|
2019-08-12 01:52:09 +02:00
|
|
|
# django-sendfile2 sets the response['Content-disposition'] like
|
|
|
|
# this: `attachment; filename="zulip.txt"; filename*=UTF-8''zulip.txt`.
|
|
|
|
# The filename* parameter is omitted for ASCII filenames like this one.
|
2018-03-13 07:08:27 +01:00
|
|
|
#
|
|
|
|
# The "filename" field (used to name the file when downloaded) is
|
|
|
|
# unreliable because it doesn't have a well-defined encoding; the
|
|
|
|
# newer filename* field takes precedence, since it uses a
|
|
|
|
# consistent format (urlquoted). For more details on filename*
|
|
|
|
# and filename, see the below docs:
|
|
|
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
|
2019-09-10 00:21:31 +02:00
|
|
|
mimetype, encoding = guess_type(local_path)
|
|
|
|
attachment = mimetype not in INLINE_MIME_TYPES
|
2018-03-13 07:08:27 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
response = sendfile(
|
|
|
|
request, local_path, attachment=attachment, mimetype=mimetype, encoding=encoding
|
|
|
|
)
|
2019-10-02 00:10:30 +02:00
|
|
|
patch_cache_control(response, private=True, immutable=True)
|
|
|
|
return response
|
2016-06-09 12:19:56 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def serve_file_backend(
|
|
|
|
request: HttpRequest, user_profile: UserProfile, realm_id_str: str, filename: str
|
|
|
|
) -> HttpResponse:
|
2020-04-08 00:27:24 +02:00
|
|
|
return serve_file(request, user_profile, realm_id_str, filename, url_only=False)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def serve_file_url_backend(
|
|
|
|
request: HttpRequest, user_profile: UserProfile, realm_id_str: str, filename: str
|
|
|
|
) -> HttpResponse:
|
2020-04-08 00:27:24 +02:00
|
|
|
"""
|
|
|
|
We should return a signed, short-lived URL
|
|
|
|
that the client can use for native mobile download, rather than serving a redirect.
|
|
|
|
"""
|
|
|
|
|
|
|
|
return serve_file(request, user_profile, realm_id_str, filename, url_only=True)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def serve_file(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
|
|
|
realm_id_str: str,
|
|
|
|
filename: str,
|
|
|
|
url_only: bool = False,
|
|
|
|
) -> HttpResponse:
|
2020-06-10 06:41:04 +02:00
|
|
|
path_id = f"{realm_id_str}/{filename}"
|
2016-06-17 19:48:17 +02:00
|
|
|
is_authorized = validate_attachment_request(user_profile, path_id)
|
|
|
|
|
|
|
|
if is_authorized is None:
|
|
|
|
return HttpResponseNotFound(_("<p>File not found.</p>"))
|
|
|
|
if not is_authorized:
|
|
|
|
return HttpResponseForbidden(_("<p>You are not authorized to view this file.</p>"))
|
2016-06-08 11:22:06 +02:00
|
|
|
if settings.LOCAL_UPLOADS_DIR is not None:
|
2020-04-08 00:27:24 +02:00
|
|
|
return serve_local(request, path_id, url_only)
|
|
|
|
|
|
|
|
return serve_s3(request, path_id, url_only)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-04-18 16:11:13 +02:00
|
|
|
def serve_local_file_unauthed(request: HttpRequest, token: str, filename: str) -> HttpResponse:
|
2020-04-08 00:27:24 +02:00
|
|
|
path_id = get_local_file_path_id_from_token(token)
|
|
|
|
if path_id is None:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("Invalid token"))
|
2021-02-12 08:20:45 +01:00
|
|
|
if path_id.split("/")[-1] != filename:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("Invalid filename"))
|
2016-06-08 11:22:06 +02:00
|
|
|
|
2020-04-08 00:27:24 +02:00
|
|
|
return serve_local(request, path_id, url_only=False)
|
2016-06-08 11:22:06 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-11-27 09:28:57 +01:00
|
|
|
def upload_file_backend(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
2016-06-27 19:28:09 +02:00
|
|
|
if len(request.FILES) == 0:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("You must specify a file to upload"))
|
2016-06-27 19:28:09 +02:00
|
|
|
if len(request.FILES) != 1:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(_("You may only upload one file at a time"))
|
2016-06-27 19:28:09 +02:00
|
|
|
|
|
|
|
user_file = list(request.FILES.values())[0]
|
2018-02-02 05:43:18 +01:00
|
|
|
file_size = user_file.size
|
2017-03-02 11:17:10 +01:00
|
|
|
if settings.MAX_FILE_UPLOAD_SIZE * 1024 * 1024 < file_size:
|
2021-06-30 18:35:50 +02:00
|
|
|
raise JsonableError(
|
2021-02-12 08:19:30 +01:00
|
|
|
_("Uploaded file is larger than the allowed limit of {} MiB").format(
|
|
|
|
settings.MAX_FILE_UPLOAD_SIZE,
|
|
|
|
)
|
|
|
|
)
|
2018-01-26 16:13:33 +01:00
|
|
|
check_upload_within_quota(user_profile.realm, file_size)
|
2016-06-27 19:28:09 +02:00
|
|
|
|
|
|
|
uri = upload_message_image_from_request(request, user_file, user_profile)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request, data={"uri": uri})
|