markdown: Change URL structure for dropbox preview to be up-to-date.

The URL structure for a shared link has changed since this function was
returned and this commit makes sure our code is in compliance with that
structure.

The concept of an album doesn't exist anymore and folders exist in-lieu
of that.

For dropbox links that are folders on non-image files, we show previews
same as any other link previews. It is not possible to get information
about the shared link except whether it is a file or folder. So for
title and description for that linked preview, we use `Dropbox file` or
`Dropbox folder` respectively.

Earlier, we were just having raw=1 as the query param to get the image
file if required, but now for every dropbox sharing link, preserving
query params is important (otherwise we get a 404), this commit makes
changes to address that.

For /sc/ links, it is not possible to generate them anymore (afaik), but
it is possible to view those existing links, so we support that link but
treat it as a folder instead.

You can check
https://www.dropboxforum.com/discussions/101001012/shared-link--scl-to-s/689070/replies/695266
for URL structure info.

We have used inline ignore for codespell since fo can be a valid
misspell of `of` and we don't want to ignore that.

https://chat.zulip.org/#narrow/channel/9-issues/topic/.F0.9F.93.82.20message_inline_ref.20dropbox.20links

Co-authored-by: Tim Abbott <tabbott@zulip.com>
This commit is contained in:
Shubham Padia
2025-06-11 11:42:46 +00:00
committed by Tim Abbott
parent 22b5744726
commit bace83ec5a
5 changed files with 84 additions and 35 deletions

View File

@@ -20,6 +20,14 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 11.0
**Feature level 395**
* [Markdown message
formatting](/api/message-formatting#removed-features): Previously,
Zulip's Markdown syntax had special support for previewing Dropbox
albums. Dropbox albums no longer exist, and links to Dropbox folders
now consistently use Zulip's standard open graph preview markup.
**Feature level 394**
* [`POST /register`](/api/register-queue), [`GET

View File

@@ -365,7 +365,33 @@ features.
## Removed features
**Changes**: In Zulip 4.0 (feature level 24), the rarely used `!avatar()`
### Removed legacy Dropbox link preview markup
In Zulip 11.0 (feature level 395), the Zulip server stopped generating
legacy Dropbox link previews. Dropbox links are now previewed just
like standard Zulip image/link previews. However, some legacy Dropbox
previews may exist in existing messages.
Clients are recommended to prune these previews from message HTML;
since they always appear after the actual link, there is no loss of
information/functionality. They can be recognized via the classes
`message_inline_ref`, `message_inline_image_desc`, and
`message_inline_image_title`:
``` html
<div class="message_inline_ref">
<a href="https://www.dropbox.com/sh/cm39k9e04z7fhim/AAAII5NK-9daee3FcF41anEua?dl=" title="Saves">
<img src="/path/to/folder_dropbox.png">
</a>
<div><div class="message_inline_image_title">Saves</div>
<desc class="message_inline_image_desc"></desc>
</div>
</div>
```
### Removed legacy avatar markup
In Zulip 4.0 (feature level 24), the rarely used `!avatar()`
and `!gravatar()` markup syntax, which was never documented and had an
inconsistent syntax, were removed.

View File

@@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 394
API_FEATURE_LEVEL = 395
# Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump

View File

@@ -13,7 +13,7 @@ from email.message import EmailMessage
from functools import lru_cache
from re import Match, Pattern
from typing import Any, Generic, Optional, TypeAlias, TypedDict, TypeVar, cast
from urllib.parse import parse_qs, quote, urljoin, urlsplit, urlunsplit
from urllib.parse import parse_qs, parse_qsl, quote, urlencode, urljoin, urlsplit, urlunsplit
from xml.etree.ElementTree import Element, SubElement
import ahocorasick
@@ -803,11 +803,15 @@ class InlineInterestingLinkProcessor(markdown.treeprocessors.Treeprocessor):
# TODO: The returned Dict could possibly be a TypedDict in future.
parsed_url = urlsplit(url)
if parsed_url.netloc == "dropbox.com" or parsed_url.netloc.endswith(".dropbox.com"):
is_album = parsed_url.path.startswith("/sc/") or parsed_url.path.startswith("/photos/")
# Only allow preview Dropbox shared links
if not (
parsed_url.path.startswith("/s/") or parsed_url.path.startswith("/sh/") or is_album
):
# See https://www.dropboxforum.com/discussions/101001012/shared-link--scl-to-s/689070/replies/695266
# for more info on the URL structure mentioned here.
# It is not possible to generate /sc/ links which is kind of a showcase
# for multiple images. We treat it now as a folder instead.
is_album = parsed_url.path.startswith(
"/scl/fo/" # codespell:ignore fo
) or parsed_url.path.startswith("/sc/")
is_file = parsed_url.path.startswith("/scl/fi/")
if not (is_file or is_album):
return None
# Try to retrieve open graph protocol info for a preview
@@ -817,7 +821,7 @@ class InlineInterestingLinkProcessor(markdown.treeprocessors.Treeprocessor):
# want to use the open graph image.
image_info = fetch_open_graph_image(url)
is_image = is_album or self.is_image(url)
is_image = self.is_image(url)
# If it is from an album or not an actual image file,
# just use open graph image.
@@ -827,17 +831,31 @@ class InlineInterestingLinkProcessor(markdown.treeprocessors.Treeprocessor):
if image_info is None:
return None
if is_album:
image_info["title"] = "Dropbox folder"
image_info["desc"] = "Click to open folder."
else:
image_info["title"] = "Dropbox file"
image_info["desc"] = "Click to open file."
image_info["is_image"] = is_image
return image_info
# Otherwise, try to retrieve the actual image.
# Try to retrieve the actual image.
# This is because open graph image from Dropbox may have padding
# and gifs do not work.
# TODO: What if image is huge? Should we get headers first?
if image_info is None:
image_info = {}
image_info["is_image"] = True
image_info["image"] = parsed_url._replace(query="raw=1").geturl()
# Adding raw=1 as query param will give us the URL of the
# actual image instead of the dropbox image preview page.
query_params = dict(parse_qsl(parsed_url.query))
query_params["raw"] = "1"
query = urlencode(query_params)
image_info["image"] = parsed_url._replace(query=query).geturl()
return image_info
return None
@@ -1339,20 +1357,22 @@ class InlineInterestingLinkProcessor(markdown.treeprocessors.Treeprocessor):
dropbox_image = self.dropbox_image(url)
if dropbox_image is not None:
class_attr = "message_inline_ref"
is_image = dropbox_image["is_image"]
if is_image:
class_attr = "message_inline_image"
# Not making use of title and description of images
self.add_a(
root,
image_url=dropbox_image["image"],
link=url,
title=dropbox_image.get("title"),
desc=dropbox_image.get("desc", ""),
class_attr=class_attr,
already_thumbnailed=True,
found_url = ResultWithFamily(
family=found_url.family,
result=(dropbox_image["image"], dropbox_image["image"]),
)
self.handle_image_inlining(root, found_url)
continue
dropbox_embed_data = UrlEmbedData(
type="image",
title=dropbox_image["title"],
description=dropbox_image["desc"],
image=dropbox_image["image"],
)
self.add_embed(root, url, dropbox_embed_data)
continue
if self.is_image(url):

View File

@@ -1062,32 +1062,28 @@ class MarkdownEmbedsTest(ZulipTestCase):
self.assertFalse(url_embed_preview_enabled(message, no_previews=True))
def test_inline_dropbox(self) -> None:
msg = "Look at how hilarious our old office was: https://www.dropbox.com/s/ymdijjcg67hv2ta/IMG_0923.JPG"
msg = "Look at how hilarious our old office was: https://www.dropbox.com/scl/fi/cbabl5ryv1veky9ehs603/IMG_0923.JPG?rlkey=24sfgf0k0dneebzf5tfccldg0"
image_info = {
"image": "https://photos-4.dropbox.com/t/2/AABIre1oReJgPYuc_53iv0IHq1vUzRaDg2rrCfTpiWMccQ/12/129/jpeg/1024x1024/2/_/0/4/IMG_0923.JPG/CIEBIAEgAiAHKAIoBw/ymdijjcg67hv2ta/AABz2uuED1ox3vpWWvMpBxu6a/IMG_0923.JPG",
"desc": "Shared with Dropbox",
"title": "IMG_0923.JPG",
"image": "https://www.dropbox.com/scl/fi/cbabl5ryv1veky9ehs603/IMG_0923.JPG?rlkey=24sfgf0k0dneebzf5tfccldg0&raw=1",
}
with mock.patch("zerver.lib.markdown.fetch_open_graph_image", return_value=image_info):
converted = markdown_convert_wrapper(msg)
self.assertEqual(
converted,
f"""<p>Look at how hilarious our old office was: <a href="https://www.dropbox.com/s/ymdijjcg67hv2ta/IMG_0923.JPG">https://www.dropbox.com/s/ymdijjcg67hv2ta/IMG_0923.JPG</a></p>\n<div class="message_inline_image"><a href="https://www.dropbox.com/s/ymdijjcg67hv2ta/IMG_0923.JPG" title="IMG_0923.JPG"><img src="{get_camo_url("https://www.dropbox.com/s/ymdijjcg67hv2ta/IMG_0923.JPG?raw=1")}"></a></div>""",
f"""<p>Look at how hilarious our old office was: <a href="https://www.dropbox.com/scl/fi/cbabl5ryv1veky9ehs603/IMG_0923.JPG?rlkey=24sfgf0k0dneebzf5tfccldg0">https://www.dropbox.com/scl/fi/cbabl5ryv1veky9ehs603/IMG_0923.JPG?rlkey=24sfgf0k0dneebzf5tfccldg0</a></p>\n<div class="message_inline_image"><a href="https://www.dropbox.com/scl/fi/cbabl5ryv1veky9ehs603/IMG_0923.JPG?rlkey=24sfgf0k0dneebzf5tfccldg0&amp;raw=1"><img src="{get_camo_url("https://www.dropbox.com/scl/fi/cbabl5ryv1veky9ehs603/IMG_0923.JPG?rlkey=24sfgf0k0dneebzf5tfccldg0&raw=1")}"></a></div>""",
)
msg = "Look at my hilarious drawing folder: https://www.dropbox.com/sh/cm39k9e04z7fhim/AAAII5NK-9daee3FcF41anEua?dl="
msg = "Look at my hilarious drawing folder: https://www.dropbox.com/scl/fo/ty22bx4thyhl9r89p839g/AAzfPX5IbiOb8wmxHvns2pM?rlkey=5pinfuoghias9cueq0zyhj2rp&st=dz5p1ytw" # codespell:ignore fo
image_info = {
"image": "https://cf.dropboxstatic.com/static/images/icons128/folder_dropbox.png",
"desc": "Shared with Dropbox",
"title": "Saves",
"image": "https://www.dropbox.com/static/metaserver/static/images/opengraph/opengraph-content-icon-folder-dropbox-landscape.png",
}
with mock.patch("zerver.lib.markdown.fetch_open_graph_image", return_value=image_info):
converted = markdown_convert_wrapper(msg)
self.assertEqual(
converted,
f"""<p>Look at my hilarious drawing folder: <a href="https://www.dropbox.com/sh/cm39k9e04z7fhim/AAAII5NK-9daee3FcF41anEua?dl=">https://www.dropbox.com/sh/cm39k9e04z7fhim/AAAII5NK-9daee3FcF41anEua?dl=</a></p>\n<div class="message_inline_ref"><a href="https://www.dropbox.com/sh/cm39k9e04z7fhim/AAAII5NK-9daee3FcF41anEua?dl=" title="Saves"><img src="{get_camo_url("https://cf.dropboxstatic.com/static/images/icons128/folder_dropbox.png")}"></a><div><div class="message_inline_image_title">Saves</div><desc class="message_inline_image_desc"></desc></div></div>""",
"""<p>Look at my hilarious drawing folder: <a href="https://www.dropbox.com/scl/fo/ty22bx4thyhl9r89p839g/AAzfPX5IbiOb8wmxHvns2pM?rlkey=5pinfuoghias9cueq0zyhj2rp&amp;st=dz5p1ytw">https://www.dropbox.com/scl/fo/ty22bx4thyhl9r89p839g/AAzfPX5IbiOb8wmxHvns2pM?rlkey=5pinfuoghias9cueq0zyhj2rp&amp;st=dz5p1ytw</a></p>\n<div class="message_embed"><a class="message_embed_image" href="https://www.dropbox.com/scl/fo/ty22bx4thyhl9r89p839g/AAzfPX5IbiOb8wmxHvns2pM?rlkey=5pinfuoghias9cueq0zyhj2rp&amp;st=dz5p1ytw" style="background-image: url(&quot;https://external-content.zulipcdn.net/external_content/a301902b9942efb85cfe2a6f4bb07d76ba7b86de/68747470733a2f2f7777772e64726f70626f782e636f6d2f7374617469632f6d6574617365727665722f7374617469632f696d616765732f6f70656e67726170682f6f70656e67726170682d636f6e74656e742d69636f6e2d666f6c6465722d64726f70626f782d6c616e6473636170652e706e67&quot;)"></a><div class="data-container"><div class="message_embed_title"><a href="https://www.dropbox.com/scl/fo/ty22bx4thyhl9r89p839g/AAzfPX5IbiOb8wmxHvns2pM?rlkey=5pinfuoghias9cueq0zyhj2rp&amp;st=dz5p1ytw" title="Dropbox folder">Dropbox folder</a></div><div class="message_embed_description">Click to open folder.</div></div></div>""", # codespell:ignore fo
)
def test_inline_dropbox_preview(self) -> None:
@@ -1095,15 +1091,14 @@ class MarkdownEmbedsTest(ZulipTestCase):
msg = "https://www.dropbox.com/sc/tditp9nitko60n5/03rEiZldy5"
image_info = {
"image": "https://photos-6.dropbox.com/t/2/AAAlawaeD61TyNewO5vVi-DGf2ZeuayfyHFdNTNzpGq-QA/12/271544745/jpeg/1024x1024/2/_/0/5/baby-piglet.jpg/CKnjvYEBIAIgBygCKAc/tditp9nitko60n5/AADX03VAIrQlTl28CtujDcMla/0",
"desc": "Shared with Dropbox",
"title": "1 photo",
}
with mock.patch("zerver.lib.markdown.fetch_open_graph_image", return_value=image_info):
converted = markdown_convert_wrapper(msg)
self.assertEqual(
converted,
f"""<p><a href="https://www.dropbox.com/sc/tditp9nitko60n5/03rEiZldy5">https://www.dropbox.com/sc/tditp9nitko60n5/03rEiZldy5</a></p>\n<div class="message_inline_image"><a href="https://www.dropbox.com/sc/tditp9nitko60n5/03rEiZldy5" title="1 photo"><img src="{get_camo_url("https://photos-6.dropbox.com/t/2/AAAlawaeD61TyNewO5vVi-DGf2ZeuayfyHFdNTNzpGq-QA/12/271544745/jpeg/1024x1024/2/_/0/5/baby-piglet.jpg/CKnjvYEBIAIgBygCKAc/tditp9nitko60n5/AADX03VAIrQlTl28CtujDcMla/0")}"></a></div>""",
"""<p><a href="https://www.dropbox.com/sc/tditp9nitko60n5/03rEiZldy5">https://www.dropbox.com/sc/tditp9nitko60n5/03rEiZldy5</a></p>
<div class="message_embed"><a class="message_embed_image" href="https://www.dropbox.com/sc/tditp9nitko60n5/03rEiZldy5" style="background-image: url(&quot;https://external-content.zulipcdn.net/external_content/e7584a56d9ba7f2a2aee96ee1427a6b746eab5ff/68747470733a2f2f70686f746f732d362e64726f70626f782e636f6d2f742f322f4141416c6177616544363154794e65774f357656692d444766325a6575617966794846644e544e7a7047712d51412f31322f3237313534343734352f6a7065672f3130323478313032342f322f5f2f302f352f626162792d7069676c65742e6a70672f434b6e6a7659454249414967427967434b41632f7464697470396e69746b6f36306e352f41414458303356414972516c546c32384374756a44634d6c612f30&quot;)"></a><div class="data-container"><div class="message_embed_title"><a href="https://www.dropbox.com/sc/tditp9nitko60n5/03rEiZldy5" title="Dropbox folder">Dropbox folder</a></div><div class="message_embed_description">Click to open folder.</div></div></div>""",
)
def test_inline_dropbox_negative(self) -> None: