Remove enable_authenticated_media flag
Fixes https://github.com/element-hq/synapse/issues/17950
This commit is contained in:
@@ -2038,30 +2038,6 @@ federation_rr_transactions_per_room_per_second: 40
|
|||||||
|
|
||||||
Config options related to Synapse's media store.
|
Config options related to Synapse's media store.
|
||||||
|
|
||||||
---
|
|
||||||
### `enable_authenticated_media`
|
|
||||||
|
|
||||||
*(boolean)* When set to true, all subsequent media uploads will be marked as authenticated, and will not be available over legacy unauthenticated media endpoints (`/_matrix/media/(r0|v3|v1)/download` and `/_matrix/media/(r0|v3|v1)/thumbnail`) – requests for authenticated media over these endpoints will result in a 404. All media, including authenticated media, will be available over the authenticated media endpoints `_matrix/client/v1/media/download` and `_matrix/client/v1/media/thumbnail`. Media uploaded prior to setting this option to true will still be available over the legacy endpoints. Note if the setting is switched to false after enabling, media marked as authenticated will be available over legacy endpoints. Defaults to true (previously false). In a future release of Synapse, this option will be removed and become always-on.
|
|
||||||
|
|
||||||
In all cases, authenticated requests to download media will succeed, but for unauthenticated requests, this case-by-case breakdown describes whether media downloads are permitted:
|
|
||||||
|
|
||||||
* `enable_authenticated_media = False`:
|
|
||||||
* unauthenticated client or homeserver requesting local media: allowed
|
|
||||||
* unauthenticated client or homeserver requesting remote media: allowed as long as the media is in the cache, or as long as the remote homeserver does not require authentication to retrieve the media
|
|
||||||
* `enable_authenticated_media = True`:
|
|
||||||
* unauthenticated client or homeserver requesting local media: allowed if the media was stored on the server whilst `enable_authenticated_media` was `False` (or in a previous Synapse version where this option did not exist); otherwise denied.
|
|
||||||
* unauthenticated client or homeserver requesting remote media: the same as for local media; allowed if the media was stored on the server whilst `enable_authenticated_media` was `False` (or in a previous Synapse version where this option did not exist); otherwise denied.
|
|
||||||
|
|
||||||
It is especially notable that media downloaded before this option existed (in older Synapse versions), or whilst this option was set to `False`, will perpetually be available over the legacy, unauthenticated endpoint, even after this option is set to `True`. This is for backwards compatibility with older clients and homeservers that do not yet support requesting authenticated media; those older clients or homeservers will not be cut off from media they can already see.
|
|
||||||
|
|
||||||
_Changed in Synapse 1.120:_ This option now defaults to `True` when not set, whereas before this version it defaulted to `False`.
|
|
||||||
|
|
||||||
Defaults to `true`.
|
|
||||||
|
|
||||||
Example configuration:
|
|
||||||
```yaml
|
|
||||||
enable_authenticated_media: false
|
|
||||||
```
|
|
||||||
---
|
---
|
||||||
### `enable_media_repo`
|
### `enable_media_repo`
|
||||||
|
|
||||||
|
|||||||
@@ -2251,50 +2251,6 @@ properties:
|
|||||||
default: 50
|
default: 50
|
||||||
examples:
|
examples:
|
||||||
- 40
|
- 40
|
||||||
enable_authenticated_media:
|
|
||||||
type: boolean
|
|
||||||
description: >-
|
|
||||||
When set to true, all subsequent media uploads will be marked as
|
|
||||||
authenticated, and will not be available over legacy unauthenticated media
|
|
||||||
endpoints (`/_matrix/media/(r0|v3|v1)/download` and
|
|
||||||
`/_matrix/media/(r0|v3|v1)/thumbnail`) – requests for authenticated media
|
|
||||||
over these endpoints will result in a 404. All media, including
|
|
||||||
authenticated media, will be available over the authenticated media
|
|
||||||
endpoints `_matrix/client/v1/media/download` and
|
|
||||||
`_matrix/client/v1/media/thumbnail`. Media uploaded prior to setting this
|
|
||||||
option to true will still be available over the legacy endpoints. Note if
|
|
||||||
the setting is switched to false after enabling, media marked as
|
|
||||||
authenticated will be available over legacy endpoints. Defaults to true
|
|
||||||
(previously false). In a future release of Synapse, this option will be
|
|
||||||
removed and become always-on.
|
|
||||||
|
|
||||||
|
|
||||||
In all cases, authenticated requests to download media will succeed, but
|
|
||||||
for unauthenticated requests, this case-by-case breakdown describes
|
|
||||||
whether media downloads are permitted:
|
|
||||||
|
|
||||||
|
|
||||||
* `enable_authenticated_media = False`:
|
|
||||||
* unauthenticated client or homeserver requesting local media: allowed
|
|
||||||
* unauthenticated client or homeserver requesting remote media: allowed as long as the media is in the cache, or as long as the remote homeserver does not require authentication to retrieve the media
|
|
||||||
* `enable_authenticated_media = True`:
|
|
||||||
* unauthenticated client or homeserver requesting local media: allowed if the media was stored on the server whilst `enable_authenticated_media` was `False` (or in a previous Synapse version where this option did not exist); otherwise denied.
|
|
||||||
* unauthenticated client or homeserver requesting remote media: the same as for local media; allowed if the media was stored on the server whilst `enable_authenticated_media` was `False` (or in a previous Synapse version where this option did not exist); otherwise denied.
|
|
||||||
|
|
||||||
It is especially notable that media downloaded before this option existed
|
|
||||||
(in older Synapse versions), or whilst this option was set to `False`,
|
|
||||||
will perpetually be available over the legacy, unauthenticated endpoint,
|
|
||||||
even after this option is set to `True`. This is for backwards
|
|
||||||
compatibility with older clients and homeservers that do not yet support
|
|
||||||
requesting authenticated media; those older clients or homeservers will
|
|
||||||
not be cut off from media they can already see.
|
|
||||||
|
|
||||||
|
|
||||||
_Changed in Synapse 1.120:_ This option now defaults to `True` when not
|
|
||||||
set, whereas before this version it defaulted to `False`.
|
|
||||||
default: true
|
|
||||||
examples:
|
|
||||||
- false
|
|
||||||
enable_media_repo:
|
enable_media_repo:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: >-
|
description: >-
|
||||||
|
|||||||
@@ -151,10 +151,6 @@ SECTION_HEADERS = {
|
|||||||
"being throttled."
|
"being throttled."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"enable_authenticated_media": {
|
|
||||||
"title": "Media Store",
|
|
||||||
"description": "Config options related to Synapse's media store.",
|
|
||||||
},
|
|
||||||
"recaptcha_public_key": {
|
"recaptcha_public_key": {
|
||||||
"title": "Captcha",
|
"title": "Captcha",
|
||||||
"description": (
|
"description": (
|
||||||
|
|||||||
@@ -288,8 +288,6 @@ class ContentRepositoryConfig(Config):
|
|||||||
remote_media_lifetime
|
remote_media_lifetime
|
||||||
)
|
)
|
||||||
|
|
||||||
self.enable_authenticated_media = config.get("enable_authenticated_media", True)
|
|
||||||
|
|
||||||
self.media_upload_limits: List[MediaUploadLimit] = []
|
self.media_upload_limits: List[MediaUploadLimit] = []
|
||||||
for limit_config in config.get("media_upload_limits", []):
|
for limit_config in config.get("media_upload_limits", []):
|
||||||
time_period_ms = self.parse_duration(limit_config["time_period"])
|
time_period_ms = self.parse_duration(limit_config["time_period"])
|
||||||
|
|||||||
@@ -487,7 +487,7 @@ class MediaRepository:
|
|||||||
if not media_info:
|
if not media_info:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
|
if not allow_authenticated:
|
||||||
if media_info.authenticated:
|
if media_info.authenticated:
|
||||||
raise NotFoundError()
|
raise NotFoundError()
|
||||||
|
|
||||||
@@ -684,7 +684,7 @@ class MediaRepository:
|
|||||||
"""
|
"""
|
||||||
media_info = await self.store.get_cached_remote_media(server_name, media_id)
|
media_info = await self.store.get_cached_remote_media(server_name, media_id)
|
||||||
|
|
||||||
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
|
if not allow_authenticated:
|
||||||
# if it isn't cached then don't fetch it or if it's authenticated then don't serve it
|
# if it isn't cached then don't fetch it or if it's authenticated then don't serve it
|
||||||
if not media_info or media_info.authenticated:
|
if not media_info or media_info.authenticated:
|
||||||
raise NotFoundError()
|
raise NotFoundError()
|
||||||
@@ -865,10 +865,8 @@ class MediaRepository:
|
|||||||
|
|
||||||
logger.info("Stored remote media in file %r", fname)
|
logger.info("Stored remote media in file %r", fname)
|
||||||
|
|
||||||
if self.hs.config.media.enable_authenticated_media:
|
# Media used to be optionally authenticated, but now we force-authenticate it
|
||||||
authenticated = True
|
authenticated = True
|
||||||
else:
|
|
||||||
authenticated = False
|
|
||||||
|
|
||||||
return RemoteMedia(
|
return RemoteMedia(
|
||||||
media_origin=server_name,
|
media_origin=server_name,
|
||||||
@@ -998,10 +996,8 @@ class MediaRepository:
|
|||||||
|
|
||||||
logger.debug("Stored remote media in file %r", fname)
|
logger.debug("Stored remote media in file %r", fname)
|
||||||
|
|
||||||
if self.hs.config.media.enable_authenticated_media:
|
# Media used to be optionally authenticated, but now we force-authenticate it
|
||||||
authenticated = True
|
authenticated = True
|
||||||
else:
|
|
||||||
authenticated = False
|
|
||||||
|
|
||||||
return RemoteMedia(
|
return RemoteMedia(
|
||||||
media_origin=server_name,
|
media_origin=server_name,
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ class ThumbnailProvider:
|
|||||||
|
|
||||||
# if the media the thumbnail is generated from is authenticated, don't serve the
|
# if the media the thumbnail is generated from is authenticated, don't serve the
|
||||||
# thumbnail over an unauthenticated endpoint
|
# thumbnail over an unauthenticated endpoint
|
||||||
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
|
if not allow_authenticated:
|
||||||
if media_info.authenticated:
|
if media_info.authenticated:
|
||||||
raise NotFoundError()
|
raise NotFoundError()
|
||||||
|
|
||||||
@@ -336,7 +336,7 @@ class ThumbnailProvider:
|
|||||||
|
|
||||||
# if the media the thumbnail is generated from is authenticated, don't serve the
|
# if the media the thumbnail is generated from is authenticated, don't serve the
|
||||||
# thumbnail over an unauthenticated endpoint
|
# thumbnail over an unauthenticated endpoint
|
||||||
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
|
if not allow_authenticated:
|
||||||
if media_info.authenticated:
|
if media_info.authenticated:
|
||||||
raise NotFoundError()
|
raise NotFoundError()
|
||||||
|
|
||||||
@@ -437,7 +437,7 @@ class ThumbnailProvider:
|
|||||||
|
|
||||||
# if the media the thumbnail is generated from is authenticated, don't serve the
|
# if the media the thumbnail is generated from is authenticated, don't serve the
|
||||||
# thumbnail over an unauthenticated endpoint
|
# thumbnail over an unauthenticated endpoint
|
||||||
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
|
if not allow_authenticated:
|
||||||
if media_info.authenticated:
|
if media_info.authenticated:
|
||||||
respond_404(request)
|
respond_404(request)
|
||||||
return
|
return
|
||||||
@@ -521,7 +521,7 @@ class ThumbnailProvider:
|
|||||||
|
|
||||||
# if the media the thumbnail is generated from is authenticated, don't serve the
|
# if the media the thumbnail is generated from is authenticated, don't serve the
|
||||||
# thumbnail over an unauthenticated endpoint
|
# thumbnail over an unauthenticated endpoint
|
||||||
if self.hs.config.media.enable_authenticated_media and not allow_authenticated:
|
if not allow_authenticated:
|
||||||
if media_info.authenticated:
|
if media_info.authenticated:
|
||||||
raise NotFoundError()
|
raise NotFoundError()
|
||||||
|
|
||||||
|
|||||||
@@ -452,10 +452,8 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore):
|
|||||||
time_now_ms: int,
|
time_now_ms: int,
|
||||||
user_id: UserID,
|
user_id: UserID,
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.hs.config.media.enable_authenticated_media:
|
# Media used to be optionally authenticated, but now we force-authenticate it
|
||||||
authenticated = True
|
authenticated = True
|
||||||
else:
|
|
||||||
authenticated = False
|
|
||||||
|
|
||||||
await self.db_pool.simple_insert(
|
await self.db_pool.simple_insert(
|
||||||
"local_media_repository",
|
"local_media_repository",
|
||||||
@@ -481,10 +479,8 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore):
|
|||||||
sha256: Optional[str] = None,
|
sha256: Optional[str] = None,
|
||||||
quarantined_by: Optional[str] = None,
|
quarantined_by: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.hs.config.media.enable_authenticated_media:
|
# Media used to be optionally authenticated, but now we force-authenticate it
|
||||||
authenticated = True
|
authenticated = True
|
||||||
else:
|
|
||||||
authenticated = False
|
|
||||||
|
|
||||||
await self.db_pool.simple_insert(
|
await self.db_pool.simple_insert(
|
||||||
"local_media_repository",
|
"local_media_repository",
|
||||||
@@ -730,10 +726,8 @@ class MediaRepositoryStore(MediaRepositoryBackgroundUpdateStore):
|
|||||||
filesystem_id: str,
|
filesystem_id: str,
|
||||||
sha256: Optional[str],
|
sha256: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.hs.config.media.enable_authenticated_media:
|
# Media used to be optionally authenticated, but now we force-authenticate it
|
||||||
authenticated = True
|
authenticated = True
|
||||||
else:
|
|
||||||
authenticated = False
|
|
||||||
|
|
||||||
await self.db_pool.simple_insert(
|
await self.db_pool.simple_insert(
|
||||||
"remote_media_cache",
|
"remote_media_cache",
|
||||||
|
|||||||
@@ -370,6 +370,9 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
|
|
||||||
self.media_id = "example.com/12345"
|
self.media_id = "example.com/12345"
|
||||||
|
|
||||||
|
self.register_user("user", "password")
|
||||||
|
self.access_token = self.login("user", "password")
|
||||||
|
|
||||||
def create_resource_dict(self) -> Dict[str, Resource]:
|
def create_resource_dict(self) -> Dict[str, Resource]:
|
||||||
resources = super().create_resource_dict()
|
resources = super().create_resource_dict()
|
||||||
resources["/_matrix/media"] = self.hs.get_media_repository_resource()
|
resources["/_matrix/media"] = self.hs.get_media_repository_resource()
|
||||||
@@ -380,9 +383,10 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
) -> FakeChannel:
|
) -> FakeChannel:
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/v3/download/{self.media_id}",
|
f"/_matrix/client/v1/media/download/{self.media_id}",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
await_result=False,
|
await_result=False,
|
||||||
|
access_token=self.access_token,
|
||||||
)
|
)
|
||||||
self.pump()
|
self.pump()
|
||||||
|
|
||||||
@@ -391,7 +395,7 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
self.assertEqual(len(self.fetches), 1)
|
self.assertEqual(len(self.fetches), 1)
|
||||||
self.assertEqual(self.fetches[0][1], "example.com")
|
self.assertEqual(self.fetches[0][1], "example.com")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.fetches[0][2], "/_matrix/media/v3/download/" + self.media_id
|
self.fetches[0][2], "/_matrix/client/v1/media/download/" + self.media_id
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.fetches[0][3],
|
self.fetches[0][3],
|
||||||
@@ -417,11 +421,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
|
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
@unittest.override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_handle_missing_content_type(self) -> None:
|
def test_handle_missing_content_type(self) -> None:
|
||||||
channel = self._req(
|
channel = self._req(
|
||||||
b"attachment; filename=out" + self.test_image.extension,
|
b"attachment; filename=out" + self.test_image.extension,
|
||||||
@@ -433,11 +432,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
headers.getRawHeaders(b"Content-Type"), [b"application/octet-stream"]
|
headers.getRawHeaders(b"Content-Type"), [b"application/octet-stream"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_disposition_filename_ascii(self) -> None:
|
def test_disposition_filename_ascii(self) -> None:
|
||||||
"""
|
"""
|
||||||
If the filename is filename=<ascii> then Synapse will decode it as an
|
If the filename is filename=<ascii> then Synapse will decode it as an
|
||||||
@@ -458,11 +452,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_disposition_filenamestar_utf8escaped(self) -> None:
|
def test_disposition_filenamestar_utf8escaped(self) -> None:
|
||||||
"""
|
"""
|
||||||
If the filename is filename=*utf8''<utf8 escaped> then Synapse will
|
If the filename is filename=*utf8''<utf8 escaped> then Synapse will
|
||||||
@@ -488,11 +477,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_disposition_none(self) -> None:
|
def test_disposition_none(self) -> None:
|
||||||
"""
|
"""
|
||||||
If there is no filename, Content-Disposition should only
|
If there is no filename, Content-Disposition should only
|
||||||
@@ -509,11 +493,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
[b"inline" if self.test_image.is_inline else b"attachment"],
|
[b"inline" if self.test_image.is_inline else b"attachment"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_thumbnail_crop(self) -> None:
|
def test_thumbnail_crop(self) -> None:
|
||||||
"""Test that a cropped remote thumbnail is available."""
|
"""Test that a cropped remote thumbnail is available."""
|
||||||
self._test_thumbnail(
|
self._test_thumbnail(
|
||||||
@@ -523,11 +502,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_thumbnail_scale(self) -> None:
|
def test_thumbnail_scale(self) -> None:
|
||||||
"""Test that a scaled remote thumbnail is available."""
|
"""Test that a scaled remote thumbnail is available."""
|
||||||
self._test_thumbnail(
|
self._test_thumbnail(
|
||||||
@@ -537,11 +511,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_invalid_type(self) -> None:
|
def test_invalid_type(self) -> None:
|
||||||
"""An invalid thumbnail type is never available."""
|
"""An invalid thumbnail type is never available."""
|
||||||
self._test_thumbnail(
|
self._test_thumbnail(
|
||||||
@@ -554,7 +523,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
@unittest.override_config(
|
@unittest.override_config(
|
||||||
{
|
{
|
||||||
"thumbnail_sizes": [{"width": 32, "height": 32, "method": "scale"}],
|
"thumbnail_sizes": [{"width": 32, "height": 32, "method": "scale"}],
|
||||||
"enable_authenticated_media": False,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
def test_no_thumbnail_crop(self) -> None:
|
def test_no_thumbnail_crop(self) -> None:
|
||||||
@@ -571,7 +539,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
@unittest.override_config(
|
@unittest.override_config(
|
||||||
{
|
{
|
||||||
"thumbnail_sizes": [{"width": 32, "height": 32, "method": "crop"}],
|
"thumbnail_sizes": [{"width": 32, "height": 32, "method": "crop"}],
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def test_no_thumbnail_scale(self) -> None:
|
def test_no_thumbnail_scale(self) -> None:
|
||||||
@@ -585,11 +552,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_thumbnail_repeated_thumbnail(self) -> None:
|
def test_thumbnail_repeated_thumbnail(self) -> None:
|
||||||
"""Test that fetching the same thumbnail works, and deleting the on disk
|
"""Test that fetching the same thumbnail works, and deleting the on disk
|
||||||
thumbnail regenerates it.
|
thumbnail regenerates it.
|
||||||
@@ -673,9 +635,10 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
params = "?width=32&height=32&method=" + method
|
params = "?width=32&height=32&method=" + method
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/r0/thumbnail/{self.media_id}{params}",
|
f"/_matrix/client/v1/media/thumbnail/{self.media_id}{params}",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
await_result=False,
|
await_result=False,
|
||||||
|
access_token=self.access_token,
|
||||||
)
|
)
|
||||||
self.pump()
|
self.pump()
|
||||||
headers = {
|
headers = {
|
||||||
@@ -708,7 +671,7 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
channel.json_body,
|
channel.json_body,
|
||||||
{
|
{
|
||||||
"errcode": "M_UNKNOWN",
|
"errcode": "M_UNKNOWN",
|
||||||
"error": "Cannot find any thumbnails for the requested media ('/_matrix/media/r0/thumbnail/example.com/12345'). This might mean the media is not a supported_media_format=(image/jpeg, image/jpg, image/webp, image/gif, image/png) or that thumbnailing failed for some other reason. (Dynamic thumbnails are disabled on this server.)",
|
"error": "Cannot find any thumbnails for the requested media ('/_matrix/client/v1/media/thumbnail/example.com/12345'). This might mean the media is not a supported_media_format=(image/jpeg, image/jpg, image/webp, image/gif, image/png) or that thumbnailing failed for some other reason. (Dynamic thumbnails are disabled on this server.)",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -718,7 +681,7 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
channel.json_body,
|
channel.json_body,
|
||||||
{
|
{
|
||||||
"errcode": "M_NOT_FOUND",
|
"errcode": "M_NOT_FOUND",
|
||||||
"error": "Not found '/_matrix/media/r0/thumbnail/example.com/12345'",
|
"error": "Not found '/_matrix/client/v1/media/thumbnail/example.com/12345'",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -764,11 +727,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_x_robots_tag_header(self) -> None:
|
def test_x_robots_tag_header(self) -> None:
|
||||||
"""
|
"""
|
||||||
Tests that the `X-Robots-Tag` header is present, which informs web crawlers
|
Tests that the `X-Robots-Tag` header is present, which informs web crawlers
|
||||||
@@ -782,11 +740,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
[b"noindex, nofollow, noarchive, noimageindex"],
|
[b"noindex, nofollow, noarchive, noimageindex"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_cross_origin_resource_policy_header(self) -> None:
|
def test_cross_origin_resource_policy_header(self) -> None:
|
||||||
"""
|
"""
|
||||||
Test that the Cross-Origin-Resource-Policy header is set to "cross-origin"
|
Test that the Cross-Origin-Resource-Policy header is set to "cross-origin"
|
||||||
@@ -801,11 +754,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
[b"cross-origin"],
|
[b"cross-origin"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def test_unknown_v3_endpoint(self) -> None:
|
def test_unknown_v3_endpoint(self) -> None:
|
||||||
"""
|
"""
|
||||||
If the v3 endpoint fails, try the r0 one.
|
If the v3 endpoint fails, try the r0 one.
|
||||||
@@ -1044,11 +992,6 @@ class RemoteDownloadLimiterTestCase(unittest.HomeserverTestCase):
|
|||||||
d.callback(52428800)
|
d.callback(52428800)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@patch(
|
@patch(
|
||||||
"synapse.http.matrixfederationclient.read_body_with_max_size",
|
"synapse.http.matrixfederationclient.read_body_with_max_size",
|
||||||
read_body_with_max_size_30MiB,
|
read_body_with_max_size_30MiB,
|
||||||
@@ -1124,7 +1067,6 @@ class RemoteDownloadLimiterTestCase(unittest.HomeserverTestCase):
|
|||||||
{
|
{
|
||||||
"remote_media_download_per_second": "50M",
|
"remote_media_download_per_second": "50M",
|
||||||
"remote_media_download_burst_count": "50M",
|
"remote_media_download_burst_count": "50M",
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@patch(
|
@patch(
|
||||||
@@ -1187,7 +1129,6 @@ class RemoteDownloadLimiterTestCase(unittest.HomeserverTestCase):
|
|||||||
@override_config(
|
@override_config(
|
||||||
{
|
{
|
||||||
"remote_media_download_burst_count": "87M",
|
"remote_media_download_burst_count": "87M",
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@patch(
|
@patch(
|
||||||
@@ -1229,7 +1170,7 @@ class RemoteDownloadLimiterTestCase(unittest.HomeserverTestCase):
|
|||||||
)
|
)
|
||||||
assert channel2.code == 429
|
assert channel2.code == 429
|
||||||
|
|
||||||
@override_config({"max_upload_size": "29M", "enable_authenticated_media": False})
|
@override_config({"max_upload_size": "29M"})
|
||||||
@patch(
|
@patch(
|
||||||
"synapse.http.matrixfederationclient.read_body_with_max_size",
|
"synapse.http.matrixfederationclient.read_body_with_max_size",
|
||||||
read_body_with_max_size_30MiB,
|
read_body_with_max_size_30MiB,
|
||||||
@@ -1320,11 +1261,6 @@ class MediaHashesTestCase(unittest.HomeserverTestCase):
|
|||||||
store_media_b.sha256,
|
store_media_b.sha256,
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_config(
|
|
||||||
{
|
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# mock actually reading file body
|
# mock actually reading file body
|
||||||
@patch(
|
@patch(
|
||||||
"synapse.http.matrixfederationclient.read_body_with_max_size",
|
"synapse.http.matrixfederationclient.read_body_with_max_size",
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class MediaRepoShardTestCase(BaseMultiWorkerStreamTestCase):
|
|||||||
self.reactor,
|
self.reactor,
|
||||||
self._hs_to_site[hs],
|
self._hs_to_site[hs],
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/r0/download/{target}/{media_id}",
|
f"/_matrix/client/v1/media/download/{target}/{media_id}",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
access_token=self.access_token,
|
access_token=self.access_token,
|
||||||
await_result=False,
|
await_result=False,
|
||||||
@@ -149,7 +149,6 @@ class MediaRepoShardTestCase(BaseMultiWorkerStreamTestCase):
|
|||||||
|
|
||||||
return channel, request
|
return channel, request
|
||||||
|
|
||||||
@override_config({"enable_authenticated_media": False})
|
|
||||||
def test_basic(self) -> None:
|
def test_basic(self) -> None:
|
||||||
"""Test basic fetching of remote media from a single worker."""
|
"""Test basic fetching of remote media from a single worker."""
|
||||||
hs1 = self.make_worker_hs("synapse.app.generic_worker")
|
hs1 = self.make_worker_hs("synapse.app.generic_worker")
|
||||||
@@ -166,7 +165,6 @@ class MediaRepoShardTestCase(BaseMultiWorkerStreamTestCase):
|
|||||||
self.assertEqual(channel.code, 200)
|
self.assertEqual(channel.code, 200)
|
||||||
self.assertEqual(channel.result["body"], b"Hello!")
|
self.assertEqual(channel.result["body"], b"Hello!")
|
||||||
|
|
||||||
@override_config({"enable_authenticated_media": False})
|
|
||||||
def test_download_simple_file_race(self) -> None:
|
def test_download_simple_file_race(self) -> None:
|
||||||
"""Test that fetching remote media from two different processes at the
|
"""Test that fetching remote media from two different processes at the
|
||||||
same time works.
|
same time works.
|
||||||
@@ -206,7 +204,6 @@ class MediaRepoShardTestCase(BaseMultiWorkerStreamTestCase):
|
|||||||
# We expect only one new file to have been persisted.
|
# We expect only one new file to have been persisted.
|
||||||
self.assertEqual(start_count + 1, self._count_remote_media())
|
self.assertEqual(start_count + 1, self._count_remote_media())
|
||||||
|
|
||||||
@override_config({"enable_authenticated_media": False})
|
|
||||||
def test_download_image_race(self) -> None:
|
def test_download_image_race(self) -> None:
|
||||||
"""Test that fetching remote *images* from two different processes at
|
"""Test that fetching remote *images* from two different processes at
|
||||||
the same time works.
|
the same time works.
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ class DeleteMediaByIDTestCase(_AdminMediaTests):
|
|||||||
self.assertEqual(400, channel.code, msg=channel.json_body)
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual("Can only delete local media", channel.json_body["error"])
|
self.assertEqual("Can only delete local media", channel.json_body["error"])
|
||||||
|
|
||||||
@override_config({"enable_authenticated_media": False})
|
|
||||||
def test_delete_media(self) -> None:
|
def test_delete_media(self) -> None:
|
||||||
"""
|
"""
|
||||||
Tests that delete a media is successfully
|
Tests that delete a media is successfully
|
||||||
@@ -148,7 +147,7 @@ class DeleteMediaByIDTestCase(_AdminMediaTests):
|
|||||||
# Attempt to access media
|
# Attempt to access media
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/v3/download/{server_and_media_id}",
|
f"/_matrix/client/v1/media/download/{server_and_media_id}",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
@@ -185,7 +184,7 @@ class DeleteMediaByIDTestCase(_AdminMediaTests):
|
|||||||
# Attempt to access media
|
# Attempt to access media
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/v3/download/{server_and_media_id}",
|
f"/_matrix/client/v1/media/download/{server_and_media_id}",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
@@ -373,7 +372,6 @@ class DeleteMediaByDateSizeTestCase(_AdminMediaTests):
|
|||||||
|
|
||||||
self._access_media(server_and_media_id, False)
|
self._access_media(server_and_media_id, False)
|
||||||
|
|
||||||
@override_config({"enable_authenticated_media": False})
|
|
||||||
def test_keep_media_by_date(self) -> None:
|
def test_keep_media_by_date(self) -> None:
|
||||||
"""
|
"""
|
||||||
Tests that media is not deleted if it is newer than `before_ts`
|
Tests that media is not deleted if it is newer than `before_ts`
|
||||||
@@ -411,7 +409,6 @@ class DeleteMediaByDateSizeTestCase(_AdminMediaTests):
|
|||||||
|
|
||||||
self._access_media(server_and_media_id, False)
|
self._access_media(server_and_media_id, False)
|
||||||
|
|
||||||
@override_config({"enable_authenticated_media": False})
|
|
||||||
def test_keep_media_by_size(self) -> None:
|
def test_keep_media_by_size(self) -> None:
|
||||||
"""
|
"""
|
||||||
Tests that media is not deleted if its size is smaller than or equal
|
Tests that media is not deleted if its size is smaller than or equal
|
||||||
@@ -447,7 +444,6 @@ class DeleteMediaByDateSizeTestCase(_AdminMediaTests):
|
|||||||
|
|
||||||
self._access_media(server_and_media_id, False)
|
self._access_media(server_and_media_id, False)
|
||||||
|
|
||||||
@override_config({"enable_authenticated_media": False})
|
|
||||||
def test_keep_media_by_user_avatar(self) -> None:
|
def test_keep_media_by_user_avatar(self) -> None:
|
||||||
"""
|
"""
|
||||||
Tests that we do not delete media if is used as a user avatar
|
Tests that we do not delete media if is used as a user avatar
|
||||||
@@ -492,7 +488,6 @@ class DeleteMediaByDateSizeTestCase(_AdminMediaTests):
|
|||||||
|
|
||||||
self._access_media(server_and_media_id, False)
|
self._access_media(server_and_media_id, False)
|
||||||
|
|
||||||
@override_config({"enable_authenticated_media": False})
|
|
||||||
def test_keep_media_by_room_avatar(self) -> None:
|
def test_keep_media_by_room_avatar(self) -> None:
|
||||||
"""
|
"""
|
||||||
Tests that we do not delete media if it is used as a room avatar
|
Tests that we do not delete media if it is used as a room avatar
|
||||||
@@ -568,7 +563,7 @@ class DeleteMediaByDateSizeTestCase(_AdminMediaTests):
|
|||||||
|
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/v3/download/{server_and_media_id}",
|
f"/_matrix/client/v1/media/download/{server_and_media_id}",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2552,7 +2552,6 @@ class AuthenticatedMediaTestCase(unittest.HomeserverTestCase):
|
|||||||
os.mkdir(self.storage_path)
|
os.mkdir(self.storage_path)
|
||||||
os.mkdir(self.media_store_path)
|
os.mkdir(self.media_store_path)
|
||||||
config["media_store_path"] = self.media_store_path
|
config["media_store_path"] = self.media_store_path
|
||||||
config["enable_authenticated_media"] = True
|
|
||||||
|
|
||||||
provider_config = {
|
provider_config = {
|
||||||
"module": "synapse.media.storage_provider.FileStorageProviderBackend",
|
"module": "synapse.media.storage_provider.FileStorageProviderBackend",
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ class MediaDomainBlockingTests(unittest.HomeserverTestCase):
|
|||||||
# Disable downloads from a domain we won't be requesting downloads from.
|
# Disable downloads from a domain we won't be requesting downloads from.
|
||||||
# This proves we haven't broken anything.
|
# This proves we haven't broken anything.
|
||||||
"prevent_media_downloads_from": ["not-listed.com"],
|
"prevent_media_downloads_from": ["not-listed.com"],
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def test_remote_media_normally_unblocked(self) -> None:
|
def test_remote_media_normally_unblocked(self) -> None:
|
||||||
@@ -101,10 +100,14 @@ class MediaDomainBlockingTests(unittest.HomeserverTestCase):
|
|||||||
Tests to ensure that remote media is normally able to be downloaded
|
Tests to ensure that remote media is normally able to be downloaded
|
||||||
when no domain block is in place.
|
when no domain block is in place.
|
||||||
"""
|
"""
|
||||||
|
self.register_user("user", "password")
|
||||||
|
access_token = self.login("user", "password")
|
||||||
|
|
||||||
response = self.make_request(
|
response = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/v3/download/evil.com/{self.remote_media_id}",
|
f"/_matrix/client/v1/media/download/evil.com/{self.remote_media_id}",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
|
access_token=access_token,
|
||||||
)
|
)
|
||||||
self.assertEqual(response.code, 200)
|
self.assertEqual(response.code, 200)
|
||||||
|
|
||||||
@@ -134,16 +137,19 @@ class MediaDomainBlockingTests(unittest.HomeserverTestCase):
|
|||||||
# This proves we haven't broken anything.
|
# This proves we haven't broken anything.
|
||||||
"prevent_media_downloads_from": ["not-listed.com"],
|
"prevent_media_downloads_from": ["not-listed.com"],
|
||||||
"dynamic_thumbnails": True,
|
"dynamic_thumbnails": True,
|
||||||
"enable_authenticated_media": False,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def test_remote_media_thumbnail_normally_unblocked(self) -> None:
|
def test_remote_media_thumbnail_normally_unblocked(self) -> None:
|
||||||
"""
|
"""
|
||||||
Same test as test_remote_media_normally_unblocked but for thumbnails.
|
Same test as test_remote_media_normally_unblocked but for thumbnails.
|
||||||
"""
|
"""
|
||||||
|
self.register_user("user", "password")
|
||||||
|
access_token = self.login("user", "password")
|
||||||
|
|
||||||
response = self.make_request(
|
response = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/v3/thumbnail/evil.com/{self.remote_media_id}?width=100&height=100",
|
f"/_matrix/client/v1/media/thumbnail/evil.com/{self.remote_media_id}?width=100&height=100",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
|
access_token=access_token,
|
||||||
)
|
)
|
||||||
self.assertEqual(response.code, 200)
|
self.assertEqual(response.code, 200)
|
||||||
|
|||||||
@@ -1260,11 +1260,13 @@ class URLPreviewTests(unittest.HomeserverTestCase):
|
|||||||
self.assertIsNone(_port)
|
self.assertIsNone(_port)
|
||||||
return host, media_id
|
return host, media_id
|
||||||
|
|
||||||
@override_config({"enable_authenticated_media": False})
|
|
||||||
def test_storage_providers_exclude_files(self) -> None:
|
def test_storage_providers_exclude_files(self) -> None:
|
||||||
"""Test that files are not stored in or fetched from storage providers."""
|
"""Test that files are not stored in or fetched from storage providers."""
|
||||||
host, media_id = self._download_image()
|
host, media_id = self._download_image()
|
||||||
|
|
||||||
|
self.register_user("user", "password")
|
||||||
|
access_token = self.login("user", "password")
|
||||||
|
|
||||||
rel_file_path = self.media_repo.filepaths.url_cache_filepath_rel(media_id)
|
rel_file_path = self.media_repo.filepaths.url_cache_filepath_rel(media_id)
|
||||||
media_store_path = os.path.join(self.media_store_path, rel_file_path)
|
media_store_path = os.path.join(self.media_store_path, rel_file_path)
|
||||||
storage_provider_path = os.path.join(self.storage_path, rel_file_path)
|
storage_provider_path = os.path.join(self.storage_path, rel_file_path)
|
||||||
@@ -1279,9 +1281,10 @@ class URLPreviewTests(unittest.HomeserverTestCase):
|
|||||||
# Check fetching
|
# Check fetching
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/v3/download/{host}/{media_id}",
|
f"/_matrix/client/v1/media/download/{host}/{media_id}",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
await_result=False,
|
await_result=False,
|
||||||
|
access_token=access_token,
|
||||||
)
|
)
|
||||||
self.pump()
|
self.pump()
|
||||||
self.assertEqual(channel.code, 200)
|
self.assertEqual(channel.code, 200)
|
||||||
@@ -1292,9 +1295,10 @@ class URLPreviewTests(unittest.HomeserverTestCase):
|
|||||||
|
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/v3/download/{host}/{media_id}",
|
f"/_matrix/client/v1/media/download/{host}/{media_id}",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
await_result=False,
|
await_result=False,
|
||||||
|
access_token=access_token,
|
||||||
)
|
)
|
||||||
self.pump()
|
self.pump()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -1303,11 +1307,13 @@ class URLPreviewTests(unittest.HomeserverTestCase):
|
|||||||
"URL cache file was unexpectedly retrieved from a storage provider",
|
"URL cache file was unexpectedly retrieved from a storage provider",
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_config({"enable_authenticated_media": False})
|
|
||||||
def test_storage_providers_exclude_thumbnails(self) -> None:
|
def test_storage_providers_exclude_thumbnails(self) -> None:
|
||||||
"""Test that thumbnails are not stored in or fetched from storage providers."""
|
"""Test that thumbnails are not stored in or fetched from storage providers."""
|
||||||
host, media_id = self._download_image()
|
host, media_id = self._download_image()
|
||||||
|
|
||||||
|
self.register_user("user", "password")
|
||||||
|
access_token = self.login("user", "password")
|
||||||
|
|
||||||
rel_thumbnail_path = (
|
rel_thumbnail_path = (
|
||||||
self.media_repo.filepaths.url_cache_thumbnail_directory_rel(media_id)
|
self.media_repo.filepaths.url_cache_thumbnail_directory_rel(media_id)
|
||||||
)
|
)
|
||||||
@@ -1328,9 +1334,10 @@ class URLPreviewTests(unittest.HomeserverTestCase):
|
|||||||
# Check fetching
|
# Check fetching
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/v3/thumbnail/{host}/{media_id}?width=32&height=32&method=scale",
|
f"/_matrix/client/v1/media/thumbnail/{host}/{media_id}?width=32&height=32&method=scale",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
await_result=False,
|
await_result=False,
|
||||||
|
access_token=access_token,
|
||||||
)
|
)
|
||||||
self.pump()
|
self.pump()
|
||||||
self.assertEqual(channel.code, 200)
|
self.assertEqual(channel.code, 200)
|
||||||
@@ -1346,9 +1353,10 @@ class URLPreviewTests(unittest.HomeserverTestCase):
|
|||||||
|
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
f"/_matrix/media/v3/thumbnail/{host}/{media_id}?width=32&height=32&method=scale",
|
f"/_matrix/client/v1/media/thumbnail/{host}/{media_id}?width=32&height=32&method=scale",
|
||||||
shorthand=False,
|
shorthand=False,
|
||||||
await_result=False,
|
await_result=False,
|
||||||
|
access_token=access_token,
|
||||||
)
|
)
|
||||||
self.pump()
|
self.pump()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
Reference in New Issue
Block a user