MSC4452: Preview URL capability (#19715)

Implementation of
https://github.com/matrix-org/matrix-spec-proposals/pull/4452
This commit is contained in:
Will Hunt
2026-05-11 12:39:38 +01:00
committed by GitHub
parent b2d196f3ed
commit 5c87faf9e9
9 changed files with 135 additions and 14 deletions
+3
View File
@@ -0,0 +1,3 @@
Add support for "MSC4452 Preview URL capabilities API" which exposes a `io.element.msc4452.preview_url` capability.
If `experimental_features.msc4452_enabled` is `true`, the `/_matrix/(client/v1/media|media/v3)/preview_url` endpoint
now responds with a 403 status code when the capability is disabled.
+4
View File
@@ -613,3 +613,7 @@ class ExperimentalConfig(Config):
# Tracked in: https://github.com/element-hq/synapse/issues/19691
# Note that this is only applicable to legacy auth, not MAS integration (OAuth 2.0).
self.msc4450_enabled: bool = experimental.get("msc4450_enabled", False)
# MSC4455: Preview URL capability
# Tracked in: https://github.com/element-hq/synapse/issues/19719
self.msc4452_enabled: bool = experimental.get("msc4452_enabled", False)
+2 -1
View File
@@ -242,7 +242,8 @@ class ContentRepositoryConfig(Config):
self.thumbnail_requirements = parse_thumbnail_requirements(
config.get("thumbnail_sizes", DEFAULT_THUMBNAIL_SIZES)
)
self.url_preview_enabled = config.get("url_preview_enabled", False)
self.url_preview_enabled = bool(config.get("url_preview_enabled", False))
if self.url_preview_enabled:
check_requirements("url-preview")
+5
View File
@@ -77,6 +77,11 @@ class CapabilitiesRestServlet(RestServlet):
}
}
if self.config.experimental.msc4452_enabled:
response["capabilities"]["io.element.msc4452.preview_url"] = {
"enabled": self.config.media.url_preview_enabled,
}
if self.config.experimental.msc3720_enabled:
response["capabilities"]["org.matrix.msc3720.account_status"] = {
"enabled": True,
+14 -6
View File
@@ -23,7 +23,12 @@
import logging
import re
from synapse.api.errors import Codes, cs_error
from synapse.api.errors import (
Codes,
SynapseError,
UnrecognizedRequestError,
cs_error,
)
from synapse.http.server import (
HttpServer,
respond_with_json,
@@ -79,11 +84,17 @@ class PreviewURLServlet(RestServlet):
self.clock = hs.get_clock()
self.media_repo = media_repo
self.media_storage = media_storage
assert self.media_repo.url_previewer is not None
self.url_previewer = self.media_repo.url_previewer
self.can_respond_403 = hs.config.experimental.msc4452_enabled
async def on_GET(self, request: SynapseRequest) -> None:
requester = await self.auth.get_user_by_req(request)
if self.url_previewer is None:
# If we have no url_previewer then it has been disabled by the server.
if self.can_respond_403:
raise SynapseError(403, "URL Previews are disabled", Codes.FORBIDDEN)
else:
raise UnrecognizedRequestError(code=404)
url = parse_string(request, "url", required=True)
ts = parse_integer(request, "ts")
if ts is None:
@@ -299,10 +310,7 @@ class DownloadResource(RestServlet):
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
media_repo = hs.get_media_repository()
if hs.config.media.url_preview_enabled:
PreviewURLServlet(hs, media_repo, media_repo.media_storage).register(
http_server
)
PreviewURLServlet(hs, media_repo, media_repo.media_storage).register(http_server)
MediaConfigResource(hs).register(http_server)
ThumbnailResource(hs, media_repo, media_repo.media_storage).register(http_server)
DownloadResource(hs, media_repo).register(http_server)
@@ -106,8 +106,7 @@ class MediaRepositoryResource(JsonResource):
ThumbnailResource(hs, media_repo, media_repo.media_storage).register(
http_server
)
if hs.config.media.url_preview_enabled:
PreviewUrlResource(hs, media_repo, media_repo.media_storage).register(
http_server
)
PreviewUrlResource(hs, media_repo, media_repo.media_storage).register(
http_server
)
MediaConfigResource(hs).register(http_server)
+8 -2
View File
@@ -23,6 +23,7 @@
import re
from typing import TYPE_CHECKING
from synapse.api.errors import Codes, SynapseError, UnrecognizedRequestError
from synapse.http.server import respond_with_json_bytes
from synapse.http.servlet import RestServlet, parse_integer, parse_string
from synapse.http.site import SynapseRequest
@@ -65,12 +66,17 @@ class PreviewUrlResource(RestServlet):
self.clock = hs.get_clock()
self.media_repo = media_repo
self.media_storage = media_storage
assert self.media_repo.url_previewer is not None
self.url_previewer = self.media_repo.url_previewer
self.can_respond_403 = hs.config.experimental.msc4452_enabled
async def on_GET(self, request: SynapseRequest) -> None:
# XXX: if get_user_by_req fails, what should we do in an async render?
requester = await self.auth.get_user_by_req(request)
if self.url_previewer is None:
# If we have no url_previewer then it has been disabled by the server.
if self.can_respond_403:
raise SynapseError(403, "URL Previews are disabled", Codes.FORBIDDEN)
else:
raise UnrecognizedRequestError(code=404)
url = parse_string(request, "url", required=True)
ts = parse_integer(request, "ts", default=self.clock.time_msec())
og = await self.url_previewer.preview(url, requester.user, ts)
+42 -1
View File
@@ -28,7 +28,12 @@ from synapse.server import HomeServer
from synapse.util.clock import Clock
from tests import unittest
from tests.unittest import override_config
from tests.unittest import override_config, skip_unless
try:
import lxml
except ImportError:
lxml = None # type: ignore[assignment]
class CapabilitiesTestCase(unittest.HomeserverTestCase):
@@ -276,3 +281,39 @@ class CapabilitiesTestCase(unittest.HomeserverTestCase):
self.assertFalse(
capabilities["org.matrix.msc4267.forget_forced_upon_leave"]["enabled"]
)
@override_config(
{
"url_preview_enabled": False,
"experimental_features": {"msc4452_enabled": True},
}
)
def test_url_previews_disabled(self) -> None:
access_token = self.get_success(
self.auth_handler.create_access_token_for_user_id(
self.user, device_id=None, valid_until_ms=None
)
)
channel = self.make_request("GET", self.url, access_token=access_token)
capabilities = channel.json_body["capabilities"]
self.assertEqual(channel.code, HTTPStatus.OK)
self.assertFalse(capabilities["io.element.msc4452.preview_url"]["enabled"])
@skip_unless(lxml is not None, "Requires lxml")
@override_config(
{
"url_preview_enabled": True,
"url_preview_ip_range_blacklist": ["127.0.0.1"],
"experimental_features": {"msc4452_enabled": True},
},
)
def test_url_previews_enabled(self) -> None:
access_token = self.get_success(
self.auth_handler.create_access_token_for_user_id(
self.user, device_id=None, valid_until_ms=None
)
)
channel = self.make_request("GET", self.url, access_token=access_token)
capabilities = channel.json_body["capabilities"]
self.assertEqual(channel.code, HTTPStatus.OK)
self.assertTrue(capabilities["io.element.msc4452.preview_url"]["enabled"])
+54
View File
@@ -1573,6 +1573,60 @@ class URLPreviewTests(unittest.HomeserverTestCase):
self.assertEqual(channel.code, 403, channel.result)
# We test this here because this endpoint must still work
# even if lxml is not installed.
class URLPreviewDisabledTests(unittest.HomeserverTestCase):
servlets = [
admin.register_servlets,
login.register_servlets,
media.register_servlets,
]
def prepare(
self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
) -> None:
self.register_user("user", "password")
self.tok = self.login("user", "password")
@override_config(
{
"url_preview_enabled": False,
"experimental_features": {"msc4452_enabled": False},
}
)
def test_disabled_previews(self) -> None:
"""Tests that disabling URL previews gives back a sane response."""
channel = self.make_request(
"GET",
"/_matrix/client/v1/media/preview_url?url=" + quote("http://example.com"),
access_token=self.tok,
)
self.assertEqual(channel.code, 404, channel.result)
self.assertEqual(
channel.json_body,
{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"},
)
@override_config(
{
"url_preview_enabled": False,
"experimental_features": {"msc4452_enabled": True},
}
)
def test_disabled_previews_with_msc4452(self) -> None:
"""Tests that disabling URL previews gives back a sane response."""
channel = self.make_request(
"GET",
"/_matrix/client/v1/media/preview_url?url=" + quote("http://example.com"),
access_token=self.tok,
)
self.assertEqual(channel.code, 403, channel.result)
self.assertEqual(
channel.json_body,
{"errcode": "M_FORBIDDEN", "error": "URL Previews are disabled"},
)
class MediaConfigTest(unittest.HomeserverTestCase):
servlets = [
media.register_servlets,