snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
143
zerver/actions/uploads.py
Normal file
143
zerver/actions/uploads.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from zerver.lib.attachments import get_old_unclaimed_attachments, validate_attachment_request
|
||||
from zerver.lib.markdown import MessageRenderingResult
|
||||
from zerver.lib.upload import claim_attachment, delete_message_attachments
|
||||
from zerver.models import (
|
||||
ArchivedAttachment,
|
||||
Attachment,
|
||||
ImageAttachment,
|
||||
Message,
|
||||
ScheduledMessage,
|
||||
Stream,
|
||||
UserProfile,
|
||||
)
|
||||
from zerver.tornado.django_api import send_event_on_commit
|
||||
|
||||
|
||||
@dataclass
|
||||
class AttachmentChangeResult:
|
||||
did_attachment_change: bool
|
||||
detached_attachments: list[dict[str, Any]]
|
||||
|
||||
|
||||
def notify_attachment_update(
|
||||
user_profile: UserProfile, op: str, attachment_dict: dict[str, Any]
|
||||
) -> None:
|
||||
event = {
|
||||
"type": "attachment",
|
||||
"op": op,
|
||||
"attachment": attachment_dict,
|
||||
"upload_space_used": user_profile.realm.currently_used_upload_space_bytes(),
|
||||
}
|
||||
send_event_on_commit(user_profile.realm, event, [user_profile.id])
|
||||
|
||||
|
||||
def do_claim_attachments(
|
||||
message: Message | ScheduledMessage, potential_path_ids: list[str]
|
||||
) -> bool:
|
||||
claimed = False
|
||||
for path_id in potential_path_ids:
|
||||
user_profile = message.sender
|
||||
is_message_realm_public = False
|
||||
is_message_web_public = False
|
||||
if isinstance(message, Message):
|
||||
is_channel_message = message.is_channel_message
|
||||
else:
|
||||
assert isinstance(message, ScheduledMessage)
|
||||
is_channel_message = message.is_channel_message()
|
||||
|
||||
if is_channel_message:
|
||||
stream = Stream.objects.get(id=message.recipient.type_id)
|
||||
is_message_realm_public = stream.is_public()
|
||||
is_message_web_public = (
|
||||
user_profile.realm.web_public_streams_enabled() and stream.is_web_public
|
||||
)
|
||||
|
||||
if not validate_attachment_request(user_profile, path_id)[0]:
|
||||
# Technically, there are 2 cases here:
|
||||
# * The user put something in their message that has the form
|
||||
# of an upload URL, but does not actually correspond to a previously
|
||||
# uploaded file. validate_attachment_request will return None.
|
||||
# * The user is trying to send a link to a file they don't have permission to
|
||||
# access themselves. validate_attachment_request will return False.
|
||||
#
|
||||
# Either case is unusual and suggests a UI bug that got
|
||||
# the user in this situation, so we log in these cases.
|
||||
logging.warning(
|
||||
"User %s tried to share upload %s in message %s, but lacks permission",
|
||||
user_profile.id,
|
||||
path_id,
|
||||
message.id,
|
||||
)
|
||||
continue
|
||||
|
||||
claimed = True
|
||||
attachment = claim_attachment(
|
||||
path_id, message, is_message_realm_public, is_message_web_public
|
||||
)
|
||||
if not isinstance(message, ScheduledMessage):
|
||||
# attachment update events don't say anything about scheduled messages,
|
||||
# so sending an event is pointless.
|
||||
notify_attachment_update(user_profile, "update", attachment.to_dict())
|
||||
return claimed
|
||||
|
||||
|
||||
@transaction.atomic(durable=True)
|
||||
def do_delete_old_unclaimed_attachments(weeks_ago: int) -> None:
|
||||
old_unclaimed_attachments, old_unclaimed_archived_attachments = get_old_unclaimed_attachments(
|
||||
weeks_ago
|
||||
)
|
||||
|
||||
with delete_message_attachments(delete_from=(ImageAttachment, Attachment)) as delete_one:
|
||||
for path_id in (
|
||||
old_unclaimed_attachments.values_list("path_id", flat=True)
|
||||
.select_for_update(of=("self",), no_key=False)
|
||||
.iterator()
|
||||
):
|
||||
delete_one(path_id)
|
||||
with delete_message_attachments(
|
||||
delete_from=(ImageAttachment, ArchivedAttachment)
|
||||
) as delete_one:
|
||||
for path_id in (
|
||||
old_unclaimed_archived_attachments.values_list("path_id", flat=True)
|
||||
.select_for_update(of=("self",), no_key=False)
|
||||
.iterator()
|
||||
):
|
||||
delete_one(path_id)
|
||||
|
||||
|
||||
def check_attachment_reference_change(
|
||||
message: Message | ScheduledMessage, rendering_result: MessageRenderingResult
|
||||
) -> AttachmentChangeResult:
|
||||
# For an unsaved message edit (message.* has been updated, but not
|
||||
# saved to the database), adjusts Attachment data to correspond to
|
||||
# the new content.
|
||||
prev_attachments = {a.path_id for a in message.attachment_set.all()}
|
||||
new_attachments = set(rendering_result.potential_attachment_path_ids)
|
||||
|
||||
if new_attachments == prev_attachments:
|
||||
return AttachmentChangeResult(bool(prev_attachments), [])
|
||||
|
||||
to_remove = list(prev_attachments - new_attachments)
|
||||
if len(to_remove) > 0:
|
||||
attachments_to_update = Attachment.objects.filter(path_id__in=to_remove).select_for_update(
|
||||
no_key=True
|
||||
)
|
||||
message.attachment_set.remove(*attachments_to_update)
|
||||
|
||||
sender = message.sender
|
||||
detached_attachments_query = Attachment.objects.filter(
|
||||
path_id__in=to_remove, messages__isnull=True, owner=sender
|
||||
)
|
||||
detached_attachments = [attachment.to_dict() for attachment in detached_attachments_query]
|
||||
|
||||
to_add = list(new_attachments - prev_attachments)
|
||||
if len(to_add) > 0:
|
||||
do_claim_attachments(message, to_add)
|
||||
|
||||
return AttachmentChangeResult(message.attachment_set.exists(), detached_attachments)
|
||||
Reference in New Issue
Block a user