Merge commit 'a3a90ee03' into anoa/dinsic_release_1_21_x
* commit 'a3a90ee03': Show a confirmation page during user password reset (#8004) Do not error when thumbnailing invalid files (#8236) Remove some unused distributor signals (#8216) Fixup pusher pool notifications (#8287) Revert "Fixup pusher pool notifications" Fixup pusher pool notifications
This commit is contained in:
21
UPGRADE.rst
21
UPGRADE.rst
@@ -152,6 +152,27 @@ modules are expected to make use of the `http_client` property on the `ModuleApi
|
||||
Modules are now passed a `module_api` argument during initialisation, which is an instance of
|
||||
`ModuleApi`.
|
||||
|
||||
New HTML templates
|
||||
------------------
|
||||
|
||||
A new HTML template,
|
||||
`password_reset_confirmation.html <https://github.com/matrix-org/synapse/blob/develop/synapse/res/templates/password_reset_confirmation.html>`_,
|
||||
has been added to the ``synapse/res/templates`` directory. If you are using a
|
||||
custom template directory, you may want to copy the template over and modify it.
|
||||
|
||||
Note that as of v1.20.0, templates do not need to be included in custom template
|
||||
directories for Synapse to start. The default templates will be used if a custom
|
||||
template cannot be found.
|
||||
|
||||
This page will appear to the user after clicking a password reset link that has
|
||||
been emailed to them.
|
||||
|
||||
To complete password reset, the page must include a way to make a `POST`
|
||||
request to
|
||||
``/_synapse/client/password_reset/{medium}/submit_token``
|
||||
with the query parameters from the original link, presented as a URL-encoded form. See the file
|
||||
itself for more details.
|
||||
|
||||
Upgrading to v1.18.0
|
||||
====================
|
||||
|
||||
|
||||
1
changelog.d/8004.feature
Normal file
1
changelog.d/8004.feature
Normal file
@@ -0,0 +1 @@
|
||||
Require the user to confirm that their password should be reset after clicking the email confirmation link.
|
||||
1
changelog.d/8216.misc
Normal file
1
changelog.d/8216.misc
Normal file
@@ -0,0 +1 @@
|
||||
Simplify the distributor code to avoid unnecessary work.
|
||||
1
changelog.d/8236.bugfix
Normal file
1
changelog.d/8236.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix a longstanding bug where files that could not be thumbnailed would result in an Internal Server Error.
|
||||
1
changelog.d/8287.bugfix
Normal file
1
changelog.d/8287.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix edge case where push could get delayed for a user until a later event was pushed.
|
||||
@@ -2214,9 +2214,13 @@ email:
|
||||
# * The contents of password reset emails sent by the homeserver:
|
||||
# 'password_reset.html' and 'password_reset.txt'
|
||||
#
|
||||
# * HTML pages for success and failure that a user will see when they follow
|
||||
# the link in the password reset email: 'password_reset_success.html' and
|
||||
# 'password_reset_failure.html'
|
||||
# * An HTML page that a user will see when they follow the link in the password
|
||||
# reset email. The user will be asked to confirm the action before their
|
||||
# password is reset: 'password_reset_confirmation.html'
|
||||
#
|
||||
# * HTML pages for success and failure that a user will see when they confirm
|
||||
# the password reset flow using the page above: 'password_reset_success.html'
|
||||
# and 'password_reset_failure.html'
|
||||
#
|
||||
# * The contents of address verification emails sent during registration:
|
||||
# 'registration.html' and 'registration.txt'
|
||||
|
||||
@@ -21,6 +21,7 @@ from urllib.parse import urlencode
|
||||
|
||||
from synapse.config import ConfigError
|
||||
|
||||
SYNAPSE_CLIENT_API_PREFIX = "/_synapse/client"
|
||||
CLIENT_API_PREFIX = "/_matrix/client"
|
||||
FEDERATION_PREFIX = "/_matrix/federation"
|
||||
FEDERATION_V1_PREFIX = FEDERATION_PREFIX + "/v1"
|
||||
|
||||
@@ -48,6 +48,7 @@ from synapse.api.urls import (
|
||||
from synapse.app import _base
|
||||
from synapse.app._base import listen_ssl, listen_tcp, quit_with_error
|
||||
from synapse.config._base import ConfigError
|
||||
from synapse.config.emailconfig import ThreepidBehaviour
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.config.server import ListenerConfig
|
||||
from synapse.federation.transport.server import TransportLayerServer
|
||||
@@ -209,6 +210,15 @@ class SynapseHomeServer(HomeServer):
|
||||
|
||||
resources["/_matrix/saml2"] = SAML2Resource(self)
|
||||
|
||||
if self.get_config().threepid_behaviour_email == ThreepidBehaviour.LOCAL:
|
||||
from synapse.rest.synapse.client.password_reset import (
|
||||
PasswordResetSubmitTokenResource,
|
||||
)
|
||||
|
||||
resources[
|
||||
"/_synapse/client/password_reset/email/submit_token"
|
||||
] = PasswordResetSubmitTokenResource(self)
|
||||
|
||||
if name == "consent":
|
||||
from synapse.rest.consent.consent_resource import ConsentResource
|
||||
|
||||
|
||||
@@ -228,6 +228,7 @@ class EmailConfig(Config):
|
||||
self.email_registration_template_text,
|
||||
self.email_add_threepid_template_html,
|
||||
self.email_add_threepid_template_text,
|
||||
self.email_password_reset_template_confirmation_html,
|
||||
self.email_password_reset_template_failure_html,
|
||||
self.email_registration_template_failure_html,
|
||||
self.email_add_threepid_template_failure_html,
|
||||
@@ -242,6 +243,7 @@ class EmailConfig(Config):
|
||||
registration_template_text,
|
||||
add_threepid_template_html,
|
||||
add_threepid_template_text,
|
||||
"password_reset_confirmation.html",
|
||||
password_reset_template_failure_html,
|
||||
registration_template_failure_html,
|
||||
add_threepid_template_failure_html,
|
||||
@@ -404,9 +406,13 @@ class EmailConfig(Config):
|
||||
# * The contents of password reset emails sent by the homeserver:
|
||||
# 'password_reset.html' and 'password_reset.txt'
|
||||
#
|
||||
# * HTML pages for success and failure that a user will see when they follow
|
||||
# the link in the password reset email: 'password_reset_success.html' and
|
||||
# 'password_reset_failure.html'
|
||||
# * An HTML page that a user will see when they follow the link in the password
|
||||
# reset email. The user will be asked to confirm the action before their
|
||||
# password is reset: 'password_reset_confirmation.html'
|
||||
#
|
||||
# * HTML pages for success and failure that a user will see when they confirm
|
||||
# the password reset flow using the page above: 'password_reset_success.html'
|
||||
# and 'password_reset_failure.html'
|
||||
#
|
||||
# * The contents of address verification emails sent during registration:
|
||||
# 'registration.html' and 'registration.txt'
|
||||
|
||||
@@ -39,10 +39,6 @@ class EventStreamHandler(BaseHandler):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super(EventStreamHandler, self).__init__(hs)
|
||||
|
||||
self.distributor = hs.get_distributor()
|
||||
self.distributor.declare("started_user_eventstream")
|
||||
self.distributor.declare("stopped_user_eventstream")
|
||||
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
self.notifier = hs.get_notifier()
|
||||
|
||||
@@ -69,7 +69,6 @@ from synapse.replication.http.federation import (
|
||||
ReplicationFederationSendEventsRestServlet,
|
||||
ReplicationStoreRoomOnInviteRestServlet,
|
||||
)
|
||||
from synapse.replication.http.membership import ReplicationUserJoinedLeftRoomRestServlet
|
||||
from synapse.state import StateResolutionStore, resolve_events_with_store
|
||||
from synapse.storage.databases.main.events_worker import EventRedactBehaviour
|
||||
from synapse.types import (
|
||||
@@ -80,7 +79,6 @@ from synapse.types import (
|
||||
get_domain_from_id,
|
||||
)
|
||||
from synapse.util.async_helpers import Linearizer, concurrently_execute
|
||||
from synapse.util.distributor import user_joined_room
|
||||
from synapse.util.retryutils import NotRetryingDestination
|
||||
from synapse.util.stringutils import shortstr
|
||||
from synapse.visibility import filter_events_for_server
|
||||
@@ -141,9 +139,6 @@ class FederationHandler(BaseHandler):
|
||||
self._replication = hs.get_replication_data_handler()
|
||||
|
||||
self._send_events = ReplicationFederationSendEventsRestServlet.make_client(hs)
|
||||
self._notify_user_membership_change = ReplicationUserJoinedLeftRoomRestServlet.make_client(
|
||||
hs
|
||||
)
|
||||
self._clean_room_for_join_client = ReplicationCleanRoomRestServlet.make_client(
|
||||
hs
|
||||
)
|
||||
@@ -707,31 +702,10 @@ class FederationHandler(BaseHandler):
|
||||
logger.debug("[%s %s] Processing event: %s", room_id, event_id, event)
|
||||
|
||||
try:
|
||||
context = await self._handle_new_event(origin, event, state=state)
|
||||
await self._handle_new_event(origin, event, state=state)
|
||||
except AuthError as e:
|
||||
raise FederationError("ERROR", e.code, e.msg, affected=event.event_id)
|
||||
|
||||
if event.type == EventTypes.Member:
|
||||
if event.membership == Membership.JOIN:
|
||||
# Only fire user_joined_room if the user has acutally
|
||||
# joined the room. Don't bother if the user is just
|
||||
# changing their profile info.
|
||||
newly_joined = True
|
||||
|
||||
prev_state_ids = await context.get_prev_state_ids()
|
||||
|
||||
prev_state_id = prev_state_ids.get((event.type, event.state_key))
|
||||
if prev_state_id:
|
||||
prev_state = await self.store.get_event(
|
||||
prev_state_id, allow_none=True
|
||||
)
|
||||
if prev_state and prev_state.membership == Membership.JOIN:
|
||||
newly_joined = False
|
||||
|
||||
if newly_joined:
|
||||
user = UserID.from_string(event.state_key)
|
||||
await self.user_joined_room(user, room_id)
|
||||
|
||||
# For encrypted messages we check that we know about the sending device,
|
||||
# if we don't then we mark the device cache for that user as stale.
|
||||
if event.type == EventTypes.Encrypted:
|
||||
@@ -1553,11 +1527,6 @@ class FederationHandler(BaseHandler):
|
||||
event.signatures,
|
||||
)
|
||||
|
||||
if event.type == EventTypes.Member:
|
||||
if event.content["membership"] == Membership.JOIN:
|
||||
user = UserID.from_string(event.state_key)
|
||||
await self.user_joined_room(user, event.room_id)
|
||||
|
||||
prev_state_ids = await context.get_prev_state_ids()
|
||||
|
||||
state_ids = list(prev_state_ids.values())
|
||||
@@ -2980,7 +2949,7 @@ class FederationHandler(BaseHandler):
|
||||
event, event_stream_id, max_stream_id, extra_users=extra_users
|
||||
)
|
||||
|
||||
await self.pusher_pool.on_new_notifications(event_stream_id, max_stream_id)
|
||||
await self.pusher_pool.on_new_notifications(max_stream_id)
|
||||
|
||||
async def _clean_room_for_join(self, room_id: str) -> None:
|
||||
"""Called to clean up any data in DB for a given room, ready for the
|
||||
@@ -2994,16 +2963,6 @@ class FederationHandler(BaseHandler):
|
||||
else:
|
||||
await self.store.clean_room_for_join(room_id)
|
||||
|
||||
async def user_joined_room(self, user: UserID, room_id: str) -> None:
|
||||
"""Called when a new user has joined the room
|
||||
"""
|
||||
if self.config.worker_app:
|
||||
await self._notify_user_membership_change(
|
||||
room_id=room_id, user_id=user.to_string(), change="joined"
|
||||
)
|
||||
else:
|
||||
user_joined_room(self.distributor, user, room_id)
|
||||
|
||||
async def get_room_complexity(
|
||||
self, remote_room_hosts: List[str], room_id: str
|
||||
) -> Optional[dict]:
|
||||
|
||||
@@ -1148,7 +1148,7 @@ class EventCreationHandler:
|
||||
# If there's an expiry timestamp on the event, schedule its expiry.
|
||||
self._message_handler.maybe_schedule_expiry(event)
|
||||
|
||||
await self.pusher_pool.on_new_notifications(event_stream_id, max_stream_id)
|
||||
await self.pusher_pool.on_new_notifications(max_stream_id)
|
||||
|
||||
def _notify():
|
||||
try:
|
||||
|
||||
@@ -40,7 +40,7 @@ from synapse.events.validator import EventValidator
|
||||
from synapse.storage.roommember import RoomsForUser
|
||||
from synapse.types import JsonDict, Requester, RoomAlias, RoomID, StateMap, UserID
|
||||
from synapse.util.async_helpers import Linearizer
|
||||
from synapse.util.distributor import user_joined_room, user_left_room
|
||||
from synapse.util.distributor import user_left_room
|
||||
|
||||
from ._base import BaseHandler
|
||||
|
||||
@@ -149,17 +149,6 @@ class RoomMemberHandler:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _user_joined_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Notifies distributor on master process that the user has joined the
|
||||
room.
|
||||
|
||||
Args:
|
||||
target
|
||||
room_id
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _user_left_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Notifies distributor on master process that the user has left the
|
||||
@@ -222,7 +211,6 @@ class RoomMemberHandler:
|
||||
|
||||
prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None)
|
||||
|
||||
newly_joined = False
|
||||
if event.membership == Membership.JOIN:
|
||||
newly_joined = True
|
||||
if prev_member_event_id:
|
||||
@@ -247,12 +235,7 @@ class RoomMemberHandler:
|
||||
requester, event, context, extra_users=[target], ratelimit=ratelimit,
|
||||
)
|
||||
|
||||
if event.membership == Membership.JOIN and newly_joined:
|
||||
# Only fire user_joined_room if the user has actually joined the
|
||||
# room. Don't bother if the user is just changing their profile
|
||||
# info.
|
||||
await self._user_joined_room(target, room_id)
|
||||
elif event.membership == Membership.LEAVE:
|
||||
if event.membership == Membership.LEAVE:
|
||||
if prev_member_event_id:
|
||||
prev_member_event = await self.store.get_event(prev_member_event_id)
|
||||
if prev_member_event.membership == Membership.JOIN:
|
||||
@@ -756,17 +739,7 @@ class RoomMemberHandler:
|
||||
(EventTypes.Member, event.state_key), None
|
||||
)
|
||||
|
||||
if event.membership == Membership.JOIN:
|
||||
# Only fire user_joined_room if the user has actually joined the
|
||||
# room. Don't bother if the user is just changing their profile
|
||||
# info.
|
||||
newly_joined = True
|
||||
if prev_member_event_id:
|
||||
prev_member_event = await self.store.get_event(prev_member_event_id)
|
||||
newly_joined = prev_member_event.membership != Membership.JOIN
|
||||
if newly_joined:
|
||||
await self._user_joined_room(target_user, room_id)
|
||||
elif event.membership == Membership.LEAVE:
|
||||
if event.membership == Membership.LEAVE:
|
||||
if prev_member_event_id:
|
||||
prev_member_event = await self.store.get_event(prev_member_event_id)
|
||||
if prev_member_event.membership == Membership.JOIN:
|
||||
@@ -1056,10 +1029,9 @@ class RoomMemberHandler:
|
||||
|
||||
class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
def __init__(self, hs):
|
||||
super(RoomMemberMasterHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.distributor = hs.get_distributor()
|
||||
self.distributor.declare("user_joined_room")
|
||||
self.distributor.declare("user_left_room")
|
||||
|
||||
async def _is_remote_room_too_complex(
|
||||
@@ -1139,7 +1111,6 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
event_id, stream_id = await self.federation_handler.do_invite_join(
|
||||
remote_room_hosts, room_id, user.to_string(), content
|
||||
)
|
||||
await self._user_joined_room(user, room_id)
|
||||
|
||||
# Check the room we just joined wasn't too large, if we didn't fetch the
|
||||
# complexity of it before.
|
||||
@@ -1282,11 +1253,6 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
)
|
||||
return event.event_id, stream_id
|
||||
|
||||
async def _user_joined_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Implements RoomMemberHandler._user_joined_room
|
||||
"""
|
||||
user_joined_room(self.distributor, target, room_id)
|
||||
|
||||
async def _user_left_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Implements RoomMemberHandler._user_left_room
|
||||
"""
|
||||
|
||||
@@ -57,8 +57,6 @@ class RoomMemberWorkerHandler(RoomMemberHandler):
|
||||
content=content,
|
||||
)
|
||||
|
||||
await self._user_joined_room(user, room_id)
|
||||
|
||||
return ret["event_id"], ret["stream_id"]
|
||||
|
||||
async def remote_reject_invite(
|
||||
@@ -81,13 +79,6 @@ class RoomMemberWorkerHandler(RoomMemberHandler):
|
||||
)
|
||||
return ret["event_id"], ret["stream_id"]
|
||||
|
||||
async def _user_joined_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Implements RoomMemberHandler._user_joined_room
|
||||
"""
|
||||
await self._notify_change_client(
|
||||
user_id=target.to_string(), room_id=room_id, change="joined"
|
||||
)
|
||||
|
||||
async def _user_left_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Implements RoomMemberHandler._user_left_room
|
||||
"""
|
||||
|
||||
@@ -91,7 +91,7 @@ class EmailPusher:
|
||||
pass
|
||||
self.timed_call = None
|
||||
|
||||
def on_new_notifications(self, min_stream_ordering, max_stream_ordering):
|
||||
def on_new_notifications(self, max_stream_ordering):
|
||||
if self.max_stream_ordering:
|
||||
self.max_stream_ordering = max(
|
||||
max_stream_ordering, self.max_stream_ordering
|
||||
|
||||
@@ -114,7 +114,7 @@ class HttpPusher:
|
||||
if should_check_for_notifs:
|
||||
self._start_processing()
|
||||
|
||||
def on_new_notifications(self, min_stream_ordering, max_stream_ordering):
|
||||
def on_new_notifications(self, max_stream_ordering):
|
||||
self.max_stream_ordering = max(
|
||||
max_stream_ordering, self.max_stream_ordering or 0
|
||||
)
|
||||
|
||||
@@ -123,7 +123,7 @@ class Mailer:
|
||||
params = {"token": token, "client_secret": client_secret, "sid": sid}
|
||||
link = (
|
||||
self.hs.config.public_baseurl
|
||||
+ "_matrix/client/unstable/password_reset/email/submit_token?%s"
|
||||
+ "_synapse/client/password_reset/email/submit_token?%s"
|
||||
% urllib.parse.urlencode(params)
|
||||
)
|
||||
|
||||
|
||||
@@ -64,7 +64,11 @@ class PusherPool:
|
||||
self._pusher_shard_config = hs.config.push.pusher_shard_config
|
||||
self._instance_name = hs.get_instance_name()
|
||||
|
||||
self._account_validity = hs.config.account_validity
|
||||
# Record the last stream ID that we were poked about so we can get
|
||||
# changes since then. We set this to the current max stream ID on
|
||||
# startup as every individual pusher will have checked for changes on
|
||||
# startup.
|
||||
self._last_room_stream_id_seen = self.store.get_room_max_stream_ordering()
|
||||
|
||||
# map from user id to app_id:pushkey to pusher
|
||||
self.pushers = {} # type: Dict[str, Dict[str, Union[HttpPusher, EmailPusher]]]
|
||||
@@ -180,20 +184,27 @@ class PusherPool:
|
||||
)
|
||||
await self.remove_pusher(p["app_id"], p["pushkey"], p["user_name"])
|
||||
|
||||
async def on_new_notifications(self, min_stream_id, max_stream_id):
|
||||
async def on_new_notifications(self, max_stream_id):
|
||||
if not self.pushers:
|
||||
# nothing to do here.
|
||||
return
|
||||
|
||||
if max_stream_id < self._last_room_stream_id_seen:
|
||||
# Nothing to do
|
||||
return
|
||||
|
||||
prev_stream_id = self._last_room_stream_id_seen
|
||||
self._last_room_stream_id_seen = max_stream_id
|
||||
|
||||
try:
|
||||
users_affected = await self.store.get_push_action_users_in_range(
|
||||
min_stream_id, max_stream_id
|
||||
prev_stream_id, max_stream_id
|
||||
)
|
||||
|
||||
for u in users_affected:
|
||||
if u in self.pushers:
|
||||
for p in self.pushers[u].values():
|
||||
p.on_new_notifications(min_stream_id, max_stream_id)
|
||||
p.on_new_notifications(max_stream_id)
|
||||
|
||||
except Exception:
|
||||
logger.exception("Exception in pusher on_new_notifications")
|
||||
|
||||
@@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from synapse.http.servlet import parse_json_object_from_request
|
||||
from synapse.replication.http._base import ReplicationEndpoint
|
||||
from synapse.types import JsonDict, Requester, UserID
|
||||
from synapse.util.distributor import user_joined_room, user_left_room
|
||||
from synapse.util.distributor import user_left_room
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
@@ -181,9 +181,9 @@ class ReplicationUserJoinedLeftRoomRestServlet(ReplicationEndpoint):
|
||||
Args:
|
||||
room_id (str)
|
||||
user_id (str)
|
||||
change (str): Either "joined" or "left"
|
||||
change (str): "left"
|
||||
"""
|
||||
assert change in ("joined", "left")
|
||||
assert change == "left"
|
||||
|
||||
return {}
|
||||
|
||||
@@ -192,9 +192,7 @@ class ReplicationUserJoinedLeftRoomRestServlet(ReplicationEndpoint):
|
||||
|
||||
user = UserID.from_string(user_id)
|
||||
|
||||
if change == "joined":
|
||||
user_joined_room(self.distributor, user, room_id)
|
||||
elif change == "left":
|
||||
if change == "left":
|
||||
user_left_room(self.distributor, user, room_id)
|
||||
else:
|
||||
raise Exception("Unrecognized change: %r", change)
|
||||
|
||||
@@ -154,7 +154,8 @@ class ReplicationDataHandler:
|
||||
max_token = self.store.get_room_max_stream_ordering()
|
||||
self.notifier.on_new_room_event(event, token, max_token, extra_users)
|
||||
|
||||
await self.pusher_pool.on_new_notifications(token, token)
|
||||
max_token = self.store.get_room_max_stream_ordering()
|
||||
await self.pusher_pool.on_new_notifications(max_token)
|
||||
|
||||
# Notify any waiting deferreds. The list is ordered by position so we
|
||||
# just iterate through the list until we reach a position that is
|
||||
|
||||
16
synapse/res/templates/password_reset_confirmation.html
Normal file
16
synapse/res/templates/password_reset_confirmation.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<!--Use a hidden form to resubmit the information necessary to reset the password-->
|
||||
<form method="post">
|
||||
<input type="hidden" name="sid" value="{{ sid }}">
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
<input type="hidden" name="client_secret" value="{{ client_secret }}">
|
||||
|
||||
<p>You have requested to <strong>reset your Matrix account password</strong>. Click the link below to confirm this action. <br /><br />
|
||||
If you did not mean to do this, please close this page and your password will not be changed.</p>
|
||||
<p><button type="submit">Confirm changing my password</button></p>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import synapse.rest.admin
|
||||
from synapse.http.server import JsonResource
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import versions
|
||||
from synapse.rest.client.v1 import (
|
||||
directory,
|
||||
@@ -124,9 +124,7 @@ class ClientRestResource(JsonResource):
|
||||
password_policy.register_servlets(hs, client_resource)
|
||||
|
||||
# moving to /_synapse/admin
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource(
|
||||
hs, client_resource
|
||||
)
|
||||
admin.register_servlets_for_client_rest_resource(hs, client_resource)
|
||||
|
||||
# unstable
|
||||
shared_rooms.register_servlets(hs, client_resource)
|
||||
|
||||
@@ -155,81 +155,6 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
|
||||
return 200, ret
|
||||
|
||||
|
||||
class PasswordResetSubmitTokenServlet(RestServlet):
|
||||
"""Handles 3PID validation token submission"""
|
||||
|
||||
PATTERNS = client_patterns(
|
||||
"/password_reset/(?P<medium>[^/]*)/submit_token$", releases=(), unstable=True
|
||||
)
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
super(PasswordResetSubmitTokenServlet, self).__init__()
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.config = hs.config
|
||||
self.clock = hs.get_clock()
|
||||
self.store = hs.get_datastore()
|
||||
if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
|
||||
self._failure_email_template = (
|
||||
self.config.email_password_reset_template_failure_html
|
||||
)
|
||||
|
||||
async def on_GET(self, request, medium):
|
||||
# We currently only handle threepid token submissions for email
|
||||
if medium != "email":
|
||||
raise SynapseError(
|
||||
400, "This medium is currently not supported for password resets"
|
||||
)
|
||||
if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
|
||||
if self.config.local_threepid_handling_disabled_due_to_email_config:
|
||||
logger.warning(
|
||||
"Password reset emails have been disabled due to lack of an email config"
|
||||
)
|
||||
raise SynapseError(
|
||||
400, "Email-based password resets are disabled on this server"
|
||||
)
|
||||
|
||||
sid = parse_string(request, "sid", required=True)
|
||||
token = parse_string(request, "token", required=True)
|
||||
client_secret = parse_string(request, "client_secret", required=True)
|
||||
assert_valid_client_secret(client_secret)
|
||||
|
||||
# Attempt to validate a 3PID session
|
||||
try:
|
||||
# Mark the session as valid
|
||||
next_link = await self.store.validate_threepid_session(
|
||||
sid, client_secret, token, self.clock.time_msec()
|
||||
)
|
||||
|
||||
# Perform a 302 redirect if next_link is set
|
||||
if next_link:
|
||||
if next_link.startswith("file:///"):
|
||||
logger.warning(
|
||||
"Not redirecting to next_link as it is a local file: address"
|
||||
)
|
||||
else:
|
||||
request.setResponseCode(302)
|
||||
request.setHeader("Location", next_link)
|
||||
finish_request(request)
|
||||
return None
|
||||
|
||||
# Otherwise show the success template
|
||||
html = self.config.email_password_reset_template_success_html_content
|
||||
status_code = 200
|
||||
except ThreepidValidationError as e:
|
||||
status_code = e.code
|
||||
|
||||
# Show a failure page with a reason
|
||||
template_vars = {"failure_reason": e.msg}
|
||||
html = self._failure_email_template.render(**template_vars)
|
||||
|
||||
respond_with_html(request, status_code, html)
|
||||
|
||||
|
||||
class PasswordRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/account/password$")
|
||||
|
||||
@@ -1112,7 +1037,6 @@ class WhoamiRestServlet(RestServlet):
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
EmailPasswordRequestTokenRestServlet(hs).register(http_server)
|
||||
PasswordResetSubmitTokenServlet(hs).register(http_server)
|
||||
PasswordRestServlet(hs).register(http_server)
|
||||
DeactivateAccountRestServlet(hs).register(http_server)
|
||||
EmailThreepidRequestTokenRestServlet(hs).register(http_server)
|
||||
|
||||
@@ -53,7 +53,7 @@ from .media_storage import MediaStorage
|
||||
from .preview_url_resource import PreviewUrlResource
|
||||
from .storage_provider import StorageProviderWrapper
|
||||
from .thumbnail_resource import ThumbnailResource
|
||||
from .thumbnailer import Thumbnailer
|
||||
from .thumbnailer import Thumbnailer, ThumbnailError
|
||||
from .upload_resource import UploadResource
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -460,13 +460,30 @@ class MediaRepository:
|
||||
return t_byte_source
|
||||
|
||||
async def generate_local_exact_thumbnail(
|
||||
self, media_id, t_width, t_height, t_method, t_type, url_cache
|
||||
):
|
||||
self,
|
||||
media_id: str,
|
||||
t_width: int,
|
||||
t_height: int,
|
||||
t_method: str,
|
||||
t_type: str,
|
||||
url_cache: str,
|
||||
) -> Optional[str]:
|
||||
input_path = await self.media_storage.ensure_media_is_in_local_cache(
|
||||
FileInfo(None, media_id, url_cache=url_cache)
|
||||
)
|
||||
|
||||
thumbnailer = Thumbnailer(input_path)
|
||||
try:
|
||||
thumbnailer = Thumbnailer(input_path)
|
||||
except ThumbnailError as e:
|
||||
logger.warning(
|
||||
"Unable to generate a thumbnail for local media %s using a method of %s and type of %s: %s",
|
||||
media_id,
|
||||
t_method,
|
||||
t_type,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
|
||||
t_byte_source = await defer_to_thread(
|
||||
self.hs.get_reactor(),
|
||||
self._generate_thumbnail,
|
||||
@@ -506,14 +523,36 @@ class MediaRepository:
|
||||
|
||||
return output_path
|
||||
|
||||
# Could not generate thumbnail.
|
||||
return None
|
||||
|
||||
async def generate_remote_exact_thumbnail(
|
||||
self, server_name, file_id, media_id, t_width, t_height, t_method, t_type
|
||||
):
|
||||
self,
|
||||
server_name: str,
|
||||
file_id: str,
|
||||
media_id: str,
|
||||
t_width: int,
|
||||
t_height: int,
|
||||
t_method: str,
|
||||
t_type: str,
|
||||
) -> Optional[str]:
|
||||
input_path = await self.media_storage.ensure_media_is_in_local_cache(
|
||||
FileInfo(server_name, file_id, url_cache=False)
|
||||
)
|
||||
|
||||
thumbnailer = Thumbnailer(input_path)
|
||||
try:
|
||||
thumbnailer = Thumbnailer(input_path)
|
||||
except ThumbnailError as e:
|
||||
logger.warning(
|
||||
"Unable to generate a thumbnail for remote media %s from %s using a method of %s and type of %s: %s",
|
||||
media_id,
|
||||
server_name,
|
||||
t_method,
|
||||
t_type,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
|
||||
t_byte_source = await defer_to_thread(
|
||||
self.hs.get_reactor(),
|
||||
self._generate_thumbnail,
|
||||
@@ -559,6 +598,9 @@ class MediaRepository:
|
||||
|
||||
return output_path
|
||||
|
||||
# Could not generate thumbnail.
|
||||
return None
|
||||
|
||||
async def _generate_thumbnails(
|
||||
self,
|
||||
server_name: Optional[str],
|
||||
@@ -590,7 +632,18 @@ class MediaRepository:
|
||||
FileInfo(server_name, file_id, url_cache=url_cache)
|
||||
)
|
||||
|
||||
thumbnailer = Thumbnailer(input_path)
|
||||
try:
|
||||
thumbnailer = Thumbnailer(input_path)
|
||||
except ThumbnailError as e:
|
||||
logger.warning(
|
||||
"Unable to generate thumbnails for remote media %s from %s using a method of %s and type of %s: %s",
|
||||
media_id,
|
||||
server_name,
|
||||
media_type,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
|
||||
m_width = thumbnailer.width
|
||||
m_height = thumbnailer.height
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.http.server import DirectServeJsonResource, set_cors_headers
|
||||
from synapse.http.servlet import parse_integer, parse_string
|
||||
|
||||
@@ -173,7 +174,7 @@ class ThumbnailResource(DirectServeJsonResource):
|
||||
await respond_with_file(request, desired_type, file_path)
|
||||
else:
|
||||
logger.warning("Failed to generate thumbnail")
|
||||
respond_404(request)
|
||||
raise SynapseError(400, "Failed to generate thumbnail.")
|
||||
|
||||
async def _select_or_generate_remote_thumbnail(
|
||||
self,
|
||||
@@ -235,7 +236,7 @@ class ThumbnailResource(DirectServeJsonResource):
|
||||
await respond_with_file(request, desired_type, file_path)
|
||||
else:
|
||||
logger.warning("Failed to generate thumbnail")
|
||||
respond_404(request)
|
||||
raise SynapseError(400, "Failed to generate thumbnail.")
|
||||
|
||||
async def _respond_remote_thumbnail(
|
||||
self, request, server_name, media_id, width, height, method, m_type
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import logging
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image as Image
|
||||
from PIL import Image
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,12 +31,22 @@ EXIF_TRANSPOSE_MAPPINGS = {
|
||||
}
|
||||
|
||||
|
||||
class ThumbnailError(Exception):
|
||||
"""An error occurred generating a thumbnail."""
|
||||
|
||||
|
||||
class Thumbnailer:
|
||||
|
||||
FORMATS = {"image/jpeg": "JPEG", "image/png": "PNG"}
|
||||
|
||||
def __init__(self, input_path):
|
||||
self.image = Image.open(input_path)
|
||||
try:
|
||||
self.image = Image.open(input_path)
|
||||
except OSError as e:
|
||||
# If an error occurs opening the image, a thumbnail won't be able to
|
||||
# be generated.
|
||||
raise ThumbnailError from e
|
||||
|
||||
self.width, self.height = self.image.size
|
||||
self.transpose_method = None
|
||||
try:
|
||||
|
||||
14
synapse/rest/synapse/__init__.py
Normal file
14
synapse/rest/synapse/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
14
synapse/rest/synapse/client/__init__.py
Normal file
14
synapse/rest/synapse/client/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
127
synapse/rest/synapse/client/password_reset.py
Normal file
127
synapse/rest/synapse/client/password_reset.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
from twisted.web.http import Request
|
||||
|
||||
from synapse.api.errors import ThreepidValidationError
|
||||
from synapse.config.emailconfig import ThreepidBehaviour
|
||||
from synapse.http.server import DirectServeHtmlResource
|
||||
from synapse.http.servlet import parse_string
|
||||
from synapse.util.stringutils import assert_valid_client_secret
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PasswordResetSubmitTokenResource(DirectServeHtmlResource):
|
||||
"""Handles 3PID validation token submission
|
||||
|
||||
This resource gets mounted under /_synapse/client/password_reset/email/submit_token
|
||||
"""
|
||||
|
||||
isLeaf = 1
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
"""
|
||||
Args:
|
||||
hs: server
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.clock = hs.get_clock()
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
self._local_threepid_handling_disabled_due_to_email_config = (
|
||||
hs.config.local_threepid_handling_disabled_due_to_email_config
|
||||
)
|
||||
self._confirmation_email_template = (
|
||||
hs.config.email_password_reset_template_confirmation_html
|
||||
)
|
||||
self._email_password_reset_template_success_html = (
|
||||
hs.config.email_password_reset_template_success_html_content
|
||||
)
|
||||
self._failure_email_template = (
|
||||
hs.config.email_password_reset_template_failure_html
|
||||
)
|
||||
|
||||
# This resource should not be mounted if threepid behaviour is not LOCAL
|
||||
assert hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL
|
||||
|
||||
async def _async_render_GET(self, request: Request) -> Tuple[int, bytes]:
|
||||
sid = parse_string(request, "sid", required=True)
|
||||
token = parse_string(request, "token", required=True)
|
||||
client_secret = parse_string(request, "client_secret", required=True)
|
||||
assert_valid_client_secret(client_secret)
|
||||
|
||||
# Show a confirmation page, just in case someone accidentally clicked this link when
|
||||
# they didn't mean to
|
||||
template_vars = {
|
||||
"sid": sid,
|
||||
"token": token,
|
||||
"client_secret": client_secret,
|
||||
}
|
||||
return (
|
||||
200,
|
||||
self._confirmation_email_template.render(**template_vars).encode("utf-8"),
|
||||
)
|
||||
|
||||
async def _async_render_POST(self, request: Request) -> Tuple[int, bytes]:
|
||||
sid = parse_string(request, "sid", required=True)
|
||||
token = parse_string(request, "token", required=True)
|
||||
client_secret = parse_string(request, "client_secret", required=True)
|
||||
|
||||
# Attempt to validate a 3PID session
|
||||
try:
|
||||
# Mark the session as valid
|
||||
next_link = await self.store.validate_threepid_session(
|
||||
sid, client_secret, token, self.clock.time_msec()
|
||||
)
|
||||
|
||||
# Perform a 302 redirect if next_link is set
|
||||
if next_link:
|
||||
if next_link.startswith("file:///"):
|
||||
logger.warning(
|
||||
"Not redirecting to next_link as it is a local file: address"
|
||||
)
|
||||
else:
|
||||
next_link_bytes = next_link.encode("utf-8")
|
||||
request.setHeader("Location", next_link_bytes)
|
||||
return (
|
||||
302,
|
||||
(
|
||||
b'You are being redirected to <a src="%s">%s</a>.'
|
||||
% (next_link_bytes, next_link_bytes)
|
||||
),
|
||||
)
|
||||
|
||||
# Otherwise show the success template
|
||||
html_bytes = self._email_password_reset_template_success_html.encode(
|
||||
"utf-8"
|
||||
)
|
||||
status_code = 200
|
||||
except ThreepidValidationError as e:
|
||||
status_code = e.code
|
||||
|
||||
# Show a failure page with a reason
|
||||
template_vars = {"failure_reason": e.msg}
|
||||
html_bytes = self._failure_email_template.render(**template_vars).encode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
return status_code, html_bytes
|
||||
@@ -16,8 +16,6 @@ import inspect
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet.defer import Deferred, fail, succeed
|
||||
from twisted.python import failure
|
||||
|
||||
from synapse.logging.context import make_deferred_yieldable, run_in_background
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
@@ -29,11 +27,6 @@ def user_left_room(distributor, user, room_id):
|
||||
distributor.fire("user_left_room", user=user, room_id=room_id)
|
||||
|
||||
|
||||
# XXX: this is no longer used. We should probably kill it.
|
||||
def user_joined_room(distributor, user, room_id):
|
||||
distributor.fire("user_joined_room", user=user, room_id=room_id)
|
||||
|
||||
|
||||
class Distributor:
|
||||
"""A central dispatch point for loosely-connected pieces of code to
|
||||
register, observe, and fire signals.
|
||||
@@ -81,28 +74,6 @@ class Distributor:
|
||||
run_as_background_process(name, self.signals[name].fire, *args, **kwargs)
|
||||
|
||||
|
||||
def maybeAwaitableDeferred(f, *args, **kw):
|
||||
"""
|
||||
Invoke a function that may or may not return a Deferred or an Awaitable.
|
||||
|
||||
This is a modified version of twisted.internet.defer.maybeDeferred.
|
||||
"""
|
||||
try:
|
||||
result = f(*args, **kw)
|
||||
except Exception:
|
||||
return fail(failure.Failure(captureVars=Deferred.debug))
|
||||
|
||||
if isinstance(result, Deferred):
|
||||
return result
|
||||
# Handle the additional case of an awaitable being returned.
|
||||
elif inspect.isawaitable(result):
|
||||
return defer.ensureDeferred(result)
|
||||
elif isinstance(result, failure.Failure):
|
||||
return fail(result)
|
||||
else:
|
||||
return succeed(result)
|
||||
|
||||
|
||||
class Signal:
|
||||
"""A Signal is a dispatch point that stores a list of callables as
|
||||
observers of it.
|
||||
@@ -132,22 +103,17 @@ class Signal:
|
||||
Returns a Deferred that will complete when all the observers have
|
||||
completed."""
|
||||
|
||||
def do(observer):
|
||||
def eb(failure):
|
||||
async def do(observer):
|
||||
try:
|
||||
result = observer(*args, **kwargs)
|
||||
if inspect.isawaitable(result):
|
||||
result = await result
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"%s signal observer %s failed: %r",
|
||||
self.name,
|
||||
observer,
|
||||
failure,
|
||||
exc_info=(
|
||||
failure.type,
|
||||
failure.value,
|
||||
failure.getTracebackObject(),
|
||||
),
|
||||
"%s signal observer %s failed: %r", self.name, observer, e,
|
||||
)
|
||||
|
||||
return maybeAwaitableDeferred(observer, *args, **kwargs).addErrback(eb)
|
||||
|
||||
deferreds = [run_in_background(do, o) for o in self.observers]
|
||||
|
||||
return make_deferred_yieldable(
|
||||
|
||||
@@ -80,6 +80,7 @@ class TypingNotificationsTestCase(unittest.HomeserverTestCase):
|
||||
"get_user_directory_stream_pos",
|
||||
"get_current_state_deltas",
|
||||
"get_device_updates_by_remote",
|
||||
"get_room_max_stream_ordering",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import os
|
||||
import re
|
||||
from email.parser import Parser
|
||||
from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import pkg_resources
|
||||
|
||||
@@ -27,6 +28,7 @@ from synapse.api.constants import LoginType, Membership
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.rest.client.v1 import login, room
|
||||
from synapse.rest.client.v2_alpha import account, register
|
||||
from synapse.rest.synapse.client.password_reset import PasswordResetSubmitTokenResource
|
||||
|
||||
from tests import unittest
|
||||
from tests.unittest import override_config
|
||||
@@ -70,6 +72,7 @@ class PasswordResetTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
self.store = hs.get_datastore()
|
||||
self.submit_token_resource = PasswordResetSubmitTokenResource(hs)
|
||||
|
||||
def test_basic_password_reset(self):
|
||||
"""Test basic password reset flow
|
||||
@@ -251,8 +254,32 @@ class PasswordResetTestCase(unittest.HomeserverTestCase):
|
||||
# Remove the host
|
||||
path = link.replace("https://example.com", "")
|
||||
|
||||
# Load the password reset confirmation page
|
||||
request, channel = self.make_request("GET", path, shorthand=False)
|
||||
self.render(request)
|
||||
request.render(self.submit_token_resource)
|
||||
self.pump()
|
||||
self.assertEquals(200, channel.code, channel.result)
|
||||
|
||||
# Now POST to the same endpoint, mimicking the same behaviour as clicking the
|
||||
# password reset confirm button
|
||||
|
||||
# Send arguments as url-encoded form data, matching the template's behaviour
|
||||
form_args = []
|
||||
for key, value_list in request.args.items():
|
||||
for value in value_list:
|
||||
arg = (key, value)
|
||||
form_args.append(arg)
|
||||
|
||||
# Confirm the password reset
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
path,
|
||||
content=urlencode(form_args).encode("utf8"),
|
||||
shorthand=False,
|
||||
content_is_form=True,
|
||||
)
|
||||
request.render(self.submit_token_resource)
|
||||
self.pump()
|
||||
self.assertEquals(200, channel.code, channel.result)
|
||||
|
||||
def _get_link_from_email(self):
|
||||
|
||||
@@ -120,12 +120,13 @@ class _TestImage:
|
||||
extension = attr.ib(type=bytes)
|
||||
expected_cropped = attr.ib(type=Optional[bytes])
|
||||
expected_scaled = attr.ib(type=Optional[bytes])
|
||||
expected_found = attr.ib(default=True, type=bool)
|
||||
|
||||
|
||||
@parameterized_class(
|
||||
("test_image",),
|
||||
[
|
||||
# smol png
|
||||
# smoll png
|
||||
(
|
||||
_TestImage(
|
||||
unhexlify(
|
||||
@@ -161,6 +162,8 @@ class _TestImage:
|
||||
None,
|
||||
),
|
||||
),
|
||||
# an empty file
|
||||
(_TestImage(b"", b"image/gif", b".gif", None, None, False,),),
|
||||
],
|
||||
)
|
||||
class MediaRepoTests(unittest.HomeserverTestCase):
|
||||
@@ -303,12 +306,16 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
||||
self.assertEqual(headers.getRawHeaders(b"Content-Disposition"), None)
|
||||
|
||||
def test_thumbnail_crop(self):
|
||||
self._test_thumbnail("crop", self.test_image.expected_cropped)
|
||||
self._test_thumbnail(
|
||||
"crop", self.test_image.expected_cropped, self.test_image.expected_found
|
||||
)
|
||||
|
||||
def test_thumbnail_scale(self):
|
||||
self._test_thumbnail("scale", self.test_image.expected_scaled)
|
||||
self._test_thumbnail(
|
||||
"scale", self.test_image.expected_scaled, self.test_image.expected_found
|
||||
)
|
||||
|
||||
def _test_thumbnail(self, method, expected_body):
|
||||
def _test_thumbnail(self, method, expected_body, expected_found):
|
||||
params = "?width=32&height=32&method=" + method
|
||||
request, channel = self.make_request(
|
||||
"GET", self.media_id + params, shorthand=False
|
||||
@@ -325,11 +332,23 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
||||
)
|
||||
self.pump()
|
||||
|
||||
self.assertEqual(channel.code, 200)
|
||||
if expected_body is not None:
|
||||
self.assertEqual(
|
||||
channel.result["body"], expected_body, channel.result["body"]
|
||||
)
|
||||
if expected_found:
|
||||
self.assertEqual(channel.code, 200)
|
||||
if expected_body is not None:
|
||||
self.assertEqual(
|
||||
channel.result["body"], expected_body, channel.result["body"]
|
||||
)
|
||||
else:
|
||||
# ensure that the result is at least some valid image
|
||||
Image.open(BytesIO(channel.result["body"]))
|
||||
else:
|
||||
# ensure that the result is at least some valid image
|
||||
Image.open(BytesIO(channel.result["body"]))
|
||||
# A 404 with a JSON body.
|
||||
self.assertEqual(channel.code, 404)
|
||||
self.assertEqual(
|
||||
channel.json_body,
|
||||
{
|
||||
"errcode": "M_NOT_FOUND",
|
||||
"error": "Not found [b'example.com', b'12345?width=32&height=32&method=%s']"
|
||||
% method,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from io import SEEK_END, BytesIO
|
||||
|
||||
import attr
|
||||
from zope.interface import implementer
|
||||
@@ -135,6 +135,7 @@ def make_request(
|
||||
request=SynapseRequest,
|
||||
shorthand=True,
|
||||
federation_auth_origin=None,
|
||||
content_is_form=False,
|
||||
):
|
||||
"""
|
||||
Make a web request using the given method and path, feed it the
|
||||
@@ -150,6 +151,8 @@ def make_request(
|
||||
with the usual REST API path, if it doesn't contain it.
|
||||
federation_auth_origin (bytes|None): if set to not-None, we will add a fake
|
||||
Authorization header pretenting to be the given server name.
|
||||
content_is_form: Whether the content is URL encoded form data. Adds the
|
||||
'Content-Type': 'application/x-www-form-urlencoded' header.
|
||||
|
||||
Returns:
|
||||
Tuple[synapse.http.site.SynapseRequest, channel]
|
||||
@@ -181,6 +184,8 @@ def make_request(
|
||||
req = request(channel)
|
||||
req.process = lambda: b""
|
||||
req.content = BytesIO(content)
|
||||
# Twisted expects to be at the end of the content when parsing the request.
|
||||
req.content.seek(SEEK_END)
|
||||
req.postpath = list(map(unquote, path[1:].split(b"/")))
|
||||
|
||||
if access_token:
|
||||
@@ -195,7 +200,13 @@ def make_request(
|
||||
)
|
||||
|
||||
if content:
|
||||
req.requestHeaders.addRawHeader(b"Content-Type", b"application/json")
|
||||
if content_is_form:
|
||||
req.requestHeaders.addRawHeader(
|
||||
b"Content-Type", b"application/x-www-form-urlencoded"
|
||||
)
|
||||
else:
|
||||
# Assume the body is JSON
|
||||
req.requestHeaders.addRawHeader(b"Content-Type", b"application/json")
|
||||
|
||||
req.requestReceived(method, path, b"1.1")
|
||||
|
||||
|
||||
@@ -353,6 +353,7 @@ class HomeserverTestCase(TestCase):
|
||||
request: Type[T] = SynapseRequest,
|
||||
shorthand: bool = True,
|
||||
federation_auth_origin: str = None,
|
||||
content_is_form: bool = False,
|
||||
) -> Tuple[T, FakeChannel]:
|
||||
"""
|
||||
Create a SynapseRequest at the path using the method and containing the
|
||||
@@ -368,6 +369,8 @@ class HomeserverTestCase(TestCase):
|
||||
with the usual REST API path, if it doesn't contain it.
|
||||
federation_auth_origin (bytes|None): if set to not-None, we will add a fake
|
||||
Authorization header pretenting to be the given server name.
|
||||
content_is_form: Whether the content is URL encoded form data. Adds the
|
||||
'Content-Type': 'application/x-www-form-urlencoded' header.
|
||||
|
||||
Returns:
|
||||
Tuple[synapse.http.site.SynapseRequest, channel]
|
||||
@@ -384,6 +387,7 @@ class HomeserverTestCase(TestCase):
|
||||
request,
|
||||
shorthand,
|
||||
federation_auth_origin,
|
||||
content_is_form,
|
||||
)
|
||||
|
||||
def render(self, request):
|
||||
|
||||
Reference in New Issue
Block a user