Files
upstream-zulip/zerver/lib/url_decoding.py
upstream-mirror 64898b2c79
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
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

146 lines
4.7 KiB
Python

# See the Zulip URL spec at https://zulip.com/api/zulip-urls
from urllib.parse import unquote, urlsplit
from django.conf import settings
from zerver.lib.narrow_helpers import NarrowTerm
from zerver.lib.topic import DB_TOPIC_NAME
def is_same_server_message_link(url: str) -> bool:
split_result = urlsplit(url)
hostname = split_result.hostname
fragment = split_result.fragment
if hostname not in {None, settings.EXTERNAL_HOST_WITHOUT_PORT}:
return False
# A message link always has category `narrow`, section `channel`
# or `dm`, and ends with `/near/<message_id>`, where <message_id>
# is a sequence of digits. The URL fragment of a message link has
# at least 5 parts. e.g. '#narrow/dm/9,15-dm/near/43'
fragment_parts = fragment.split("/")
if len(fragment_parts) < 5:
return False
category = fragment_parts[0]
section = fragment_parts[1]
ends_with_near_message_id = fragment_parts[-2] == "near" and fragment_parts[-1].isdigit()
return category == "narrow" and section in {"channel", "dm"} and ends_with_near_message_id
CHANNEL_SYNONYMS = {"stream": "channel", "streams": "channels"}
OPERATOR_SYNONYMS = {
**CHANNEL_SYNONYMS,
# "pm-with:" was renamed to "dm:"
"pm-with": "dm",
# "group-pm-with:" was replaced with "dm-including:"
"group-pm-with": "dm-including",
"from": "sender",
DB_TOPIC_NAME: "topic",
}
def canonicalize_operator_synonyms(text: str) -> str:
text = text.lower()
if text in OPERATOR_SYNONYMS.values():
return text
if text in OPERATOR_SYNONYMS:
return OPERATOR_SYNONYMS[text]
return text
def parse_recipient_slug(slug: str) -> tuple[int | list[int], str] | None:
"""
Parses operands formatted in slug containing object ID or IDs.
Typical of "channel" or private message operands.
Doesn't parse the legacy pre-2018 stream slug format, which would
require using data for what channels exist for a proper parse.
e.g. "stream-name"
Returns a tuple of parsed ids and the recipient info (channel name,
DM'ed users name, etc) or only `None` if the operand is invalid.
e.g.
- "12,13,14-group" -> ([12, 13, 14], "group")
- "89-Markl" -> (89, "Markl")
- "stream-name" -> None
"""
try:
ids_string, suffix = slug.split("-", maxsplit=1)
ids = [int(id) for id in ids_string.split(",")]
return (ids if len(ids) > 1 else ids[0], suffix)
except ValueError:
# We expect this to happen both for invalid URLs and legacy
# pre-2018 channel link URLs that don't have a channel ID in
# the slug.
return None
def decode_hash_component(string: str) -> str:
# This matches the web app's implementation of decodeHashComponent.
return unquote(string.replace(".", "%"))
def decode_narrow_operand(operator: str, operand: str) -> str | int | list[int]:
# These have the similar slug formatting for their operands which
# contain object ID(s).
if operator in ["dm-including", "dm", "mentions", "sender", "channel"]:
result = parse_recipient_slug(operand)
return result[0] if isinstance(result, tuple) else ""
if operator == "near":
return int(operand) if operand.isdigit() else ""
operand = decode_hash_component(operand).strip()
return operand
def parse_narrow_url(
narrow_url: str,
) -> list[NarrowTerm] | None:
"""This server implementation is intended to match the algorithm
for the web app's `parse_narrow` in `hash_util.ts`. It largely
behaves the same way and has the same purpose: to parse a narrow
URL into a list of `NarrowTerm`.
The key difference from the web app implementation is that this
does not validate the referenced objects (users and channels).
"""
split_result = urlsplit(narrow_url)
fragment = split_result.fragment
fragment_parts = fragment.split("/")
terms: list[NarrowTerm] = []
for i in range(1, len(fragment_parts), 2):
raw_operator = decode_hash_component(fragment_parts[i]).strip()
if not raw_operator:
return None
negated = False
if raw_operator.startswith("-"):
negated = True
raw_operator = raw_operator[1:]
operator = canonicalize_operator_synonyms(raw_operator)
try:
raw_operand = fragment_parts[i + 1]
except IndexError:
raw_operand = ""
operand = decode_narrow_operand(operator, raw_operand)
if operand == "" and operator != "topic":
# The empty string is a valid topic (realm_empty_topic_display_name).
#
# Other empty string operands are invalid.
return None
terms.append(NarrowTerm(operator, operand, negated))
return terms