diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c6c5315..deca765fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ Changelog All notable changes to this project will be documented in this file. +## [3.4.4] - 2021-11-26 +### Fixed + +- Fix error when suspending user with an already blocked canonical email ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17036)) +- Fix overflow of long profile fields in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17010)) +- Fix confusing error when WebFinger request returns empty document ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16986)) +- Fix upload of remote media with OpenStack Swift sometimes failing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16998)) +- Fix logout link not working in Safari ([noellabo](https://github.com/mastodon/mastodon/pull/16574)) +- Fix “open” link of media modal not closing modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16524)) +- Fix replying from modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16516)) +- Fix `mastodon:setup` command crashing in some circumstances ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16976)) + +### Security + +- Fix filtering DMs from non-followed users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17042)) +- Fix handling of recursive toots in WebUI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17041)) + ## [3.4.3] - 2021-11-06 ### Fixed diff --git a/app/javascript/mastodon/components/modal_root.js b/app/javascript/mastodon/components/modal_root.js index 8c9409d01..755c46fd6 100644 --- a/app/javascript/mastodon/components/modal_root.js +++ b/app/javascript/mastodon/components/modal_root.js @@ -6,6 +6,10 @@ import { multiply } from 'color-blend'; export default class ModalRoot extends React.PureComponent { + static contextTypes = { + router: PropTypes.object, + }; + static propTypes = { children: PropTypes.node, onClose: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/compose/containers/navigation_container.js b/app/javascript/mastodon/features/compose/containers/navigation_container.js index 8606a642e..654c14df9 100644 --- a/app/javascript/mastodon/features/compose/containers/navigation_container.js +++ b/app/javascript/mastodon/features/compose/containers/navigation_container.js @@ -21,6 +21,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(openModal('CONFIRM', { message: intl.formatMessage(messages.logoutMessage), confirm: intl.formatMessage(messages.logoutConfirm), + closeWhenConfirm: false, onConfirm: () => logOut(), })); }, diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index e7e0fb423..e47772bd4 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -86,6 +86,7 @@ class Compose extends React.PureComponent { dispatch(openModal('CONFIRM', { message: intl.formatMessage(messages.logoutMessage), confirm: intl.formatMessage(messages.logoutConfirm), + closeWhenConfirm: false, onConfirm: () => logOut(), })); diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js index c89b86ffe..1d78c741a 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js @@ -118,7 +118,11 @@ class Footer extends ImmutablePureComponent { return; } - const { status } = this.props; + const { status, onClose } = this.props; + + if (onClose) { + onClose(); + } router.history.push(`/statuses/${status.get('id')}`); } diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index fc597c05a..86eff2a32 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -88,7 +88,7 @@ const makeMapStateToProps = () => { ancestorsIds = ancestorsIds.withMutations(mutable => { let id = statusId; - while (id) { + while (id && !mutable.includes(id)) { mutable.unshift(id); id = inReplyTos.get(id); } @@ -106,7 +106,7 @@ const makeMapStateToProps = () => { const ids = [statusId]; while (ids.length > 0) { - let id = ids.shift(); + let id = ids.pop(); const replies = contextReplies.get(id); if (statusId !== id) { @@ -115,7 +115,7 @@ const makeMapStateToProps = () => { if (replies) { replies.reverse().forEach(reply => { - ids.unshift(reply); + if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply); }); } } diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modal.js b/app/javascript/mastodon/features/ui/components/confirmation_modal.js index 1227fa453..65d97ca16 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modal.js +++ b/app/javascript/mastodon/features/ui/components/confirmation_modal.js @@ -13,15 +13,22 @@ class ConfirmationModal extends React.PureComponent { onConfirm: PropTypes.func.isRequired, secondary: PropTypes.string, onSecondary: PropTypes.func, + closeWhenConfirm: PropTypes.bool, intl: PropTypes.object.isRequired, }; + static defaultProps = { + closeWhenConfirm: true, + }; + componentDidMount() { this.button.focus(); } handleClick = () => { - this.props.onClose(); + if (this.props.closeWhenConfirm) { + this.props.onClose(); + } this.props.onConfirm(); } diff --git a/app/javascript/mastodon/features/ui/components/link_footer.js b/app/javascript/mastodon/features/ui/components/link_footer.js index 432fd5f14..12a983eb7 100644 --- a/app/javascript/mastodon/features/ui/components/link_footer.js +++ b/app/javascript/mastodon/features/ui/components/link_footer.js @@ -17,6 +17,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(openModal('CONFIRM', { message: intl.formatMessage(messages.logoutMessage), confirm: intl.formatMessage(messages.logoutConfirm), + closeWhenConfirm: false, onConfirm: () => logOut(), })); }, diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 25ebc19b0..2744c6c6c 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -829,6 +829,7 @@ a.name-tag, padding: 0 5px; margin-bottom: 10px; flex: 1 0 50%; + max-width: 100%; } .account__header__fields, diff --git a/app/lib/webfinger.rb b/app/lib/webfinger.rb index e0e022cea..1ffb5b4bf 100644 --- a/app/lib/webfinger.rb +++ b/app/lib/webfinger.rb @@ -46,7 +46,9 @@ class Webfinger def body_from_webfinger(url = standard_url, use_fallback = true) webfinger_request(url).perform do |res| if res.code == 200 - res.body_with_limit + body = res.body_with_limit + raise Webfinger::Error, "Request for #{@uri} returned empty response" if body.empty? + body elsif res.code == 404 && use_fallback body_from_host_meta elsif res.code == 410 diff --git a/app/models/canonical_email_block.rb b/app/models/canonical_email_block.rb index a8546d65a..be8c45bfe 100644 --- a/app/models/canonical_email_block.rb +++ b/app/models/canonical_email_block.rb @@ -15,7 +15,7 @@ class CanonicalEmailBlock < ApplicationRecord belongs_to :reference_account, class_name: 'Account' - validates :canonical_email_hash, presence: true + validates :canonical_email_hash, presence: true, uniqueness: true def email=(email) self.canonical_email_hash = email_to_canonical_email_hash(email) diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index fc187db40..e78c74d1e 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -67,8 +67,49 @@ class NotifyService < BaseService message? && @notification.target_status.direct_visibility? end + # Returns true if the sender has been mentionned by the recipient up the thread def response_to_recipient? - @notification.target_status.in_reply_to_account_id == @recipient.id && @notification.target_status.thread&.direct_visibility? + return false if @notification.target_status.in_reply_to_id.nil? + + # Using an SQL CTE to avoid unneeded back-and-forth with SQL server in case of long threads + !Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @notification.from_account.id]).zero? + WITH RECURSIVE ancestors(id, in_reply_to_id, replying_to_sender) AS ( + SELECT + s.id, s.in_reply_to_id, (CASE + WHEN s.account_id = :recipient_id THEN + EXISTS ( + SELECT * + FROM mentions m + WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id + ) + ELSE + FALSE + END) + FROM statuses s + WHERE s.id = :id + UNION ALL + SELECT + s.id, + s.in_reply_to_id, + (CASE + WHEN s.account_id = :recipient_id THEN + EXISTS ( + SELECT * + FROM mentions m + WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id + ) + ELSE + FALSE + END) + FROM ancestors st + JOIN statuses s ON s.id = st.in_reply_to_id + WHERE st.replying_to_sender IS FALSE + ) + SELECT COUNT(*) + FROM ancestors st + JOIN statuses s ON s.id = st.id + WHERE st.replying_to_sender IS TRUE AND s.visibility = 3 + SQL end def from_staff? diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 37f08ad93..493479977 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 3 + 4 end def flags diff --git a/lib/paperclip/response_with_limit_adapter.rb b/lib/paperclip/response_with_limit_adapter.rb index 17a2abd25..deb89717a 100644 --- a/lib/paperclip/response_with_limit_adapter.rb +++ b/lib/paperclip/response_with_limit_adapter.rb @@ -17,9 +17,9 @@ module Paperclip def cache_current_values @original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data' - @size = @target.response.content_length @tempfile = copy_to_tempfile(@target) @content_type = ContentTypeDetector.new(@tempfile.path).detect + @size = File.size(@tempfile) end def copy_to_tempfile(source) diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 9146f78e1..d905a07da 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -350,11 +350,11 @@ namespace :mastodon do end end.join("\n") - generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n" + generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n".dup if incompatible_syntax - generated_header << "Some variables in this file will be interpreted differently whether you are\n" - generated_header << "using docker-compose or not.\n\n" + generated_header << "# Some variables in this file will be interpreted differently whether you are\n" + generated_header << "# using docker-compose or not.\n\n" end File.write(Rails.root.join('.env.production'), "#{generated_header}#{env_contents}\n") diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 03d6f5fb0..65e6714c0 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -5,6 +5,37 @@ RSpec.describe Account, type: :model do let(:bob) { Fabricate(:account, username: 'bob') } subject { Fabricate(:account) } + describe '#suspend!' do + it 'marks the account as suspended' do + subject.suspend! + expect(subject.suspended?).to be true + end + + it 'creates a deletion request' do + subject.suspend! + expect(AccountDeletionRequest.where(account: subject).exists?).to be true + end + + context 'when the account is of a local user' do + let!(:subject) { Fabricate(:account, user: Fabricate(:user, email: 'foo+bar@domain.org')) } + + it 'creates a canonical domain block' do + subject.suspend! + expect(CanonicalEmailBlock.block?(subject.user_email)).to be true + end + + context 'when a canonical domain block already exists for that email' do + before do + Fabricate(:canonical_email_block, email: subject.user_email) + end + + it 'does not raise an error' do + expect { subject.suspend! }.not_to raise_error + end + end + end + end + describe '#follow!' do it 'creates a follow' do follow = subject.follow!(bob) diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb index f2cb22c5e..7433866b7 100644 --- a/spec/services/notify_service_spec.rb +++ b/spec/services/notify_service_spec.rb @@ -64,8 +64,9 @@ RSpec.describe NotifyService, type: :service do is_expected.to_not change(Notification, :count) end - context 'if the message chain initiated by recipient, but is not direct message' do + context 'if the message chain is initiated by recipient, but is not direct message' do let(:reply_to) { Fabricate(:status, account: recipient) } + let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) } let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) } it 'does not notify' do @@ -73,8 +74,20 @@ RSpec.describe NotifyService, type: :service do end end - context 'if the message chain initiated by recipient and is direct message' do + context 'if the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do + let(:reply_to) { Fabricate(:status, account: recipient) } + let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) } + let(:dummy_reply) { Fabricate(:status, account: sender, visibility: :direct, thread: reply_to) } + let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: dummy_reply)) } + + it 'does not notify' do + is_expected.to_not change(Notification, :count) + end + end + + context 'if the message chain is initiated by the recipient with a mention to the sender' do let(:reply_to) { Fabricate(:status, account: recipient, visibility: :direct) } + let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) } let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) } it 'does notify' do