diff --git a/CHANGELOG.md b/CHANGELOG.md index cfbc450d7..39e975479 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,25 +2,6 @@ All notable changes to this project will be documented in this file. -## [4.5.6] - 2026-02-03 - -### Security - -- Fix ActivityPub collection caching logic for pinned posts and featured tags not checking blocked accounts ([GHSA-ccpr-m53r-mfwr](https://github.com/mastodon/mastodon/security/advisories/GHSA-ccpr-m53r-mfwr)) - -### Changed - -- Shorten caching of quote posts pending approval (#37570 and #37592 by @ClearlyClaire) - -### Fixed - -- Fix relationship cache not being cleared when handling account migrations (#37664 by @ClearlyClaire) -- Fix quote cancel button not appearing after edit then delete-and-redraft (#37066 by @PGrayCS) -- Fix followers with profile subscription (bell icon) being notified of post edits (#37646 by @ClearlyClaire) -- Fix error when encountering invalid tag in updated object (#37635 by @ClearlyClaire) -- Fix cross-server conversation tracking (#37559 by @ClearlyClaire) -- Fix recycled connections not being immediately closed (#37335 and #37674 by @ClearlyClaire and @shleeable) - ## [4.5.5] - 2026-01-20 ### Security diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 752b843c8..c80db3500 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -4,31 +4,17 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController vary_by -> { 'Signature' if authorized_fetch_mode? } before_action :require_account_signature!, if: :authorized_fetch_mode? - before_action :check_authorization before_action :set_items before_action :set_size before_action :set_type def show expires_in 3.minutes, public: public_fetch_mode? - - if @unauthorized - render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter - else - render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter - end + render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter end private - def check_authorization - # Because in public fetch mode we cache the response, there would be no - # benefit from performing the check below, since a blocked account or domain - # would likely be served the cache from the reverse proxy anyway - - @unauthorized = authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain))) - end - def set_items case params[:id] when 'featured' @@ -71,7 +57,11 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController end def for_signed_account - if @unauthorized + # Because in public fetch mode we cache the response, there would be no + # benefit from performing the check below, since a blocked account or domain + # would likely be served the cache from the reverse proxy anyway + + if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain))) [] else yield diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 65db807d1..e673faca0 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -29,7 +29,7 @@ class StatusesController < ApplicationController end format.json do - expires_in @status.quote&.pending? ? 5.seconds : 3.minutes, public: true if @status.distributable? && public_fetch_mode? + expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index aac58b31f..2a8c9bfb2 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -145,7 +145,6 @@ class Status extends ImmutablePureComponent { 'hidden', 'unread', 'pictureInPicture', - 'onQuoteCancel', ]; state = { diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index a7d2be35e..43c7bb1fe 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -379,7 +379,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def conversation_from_uri(uri) return nil if uri.nil? return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri) - return ActivityPub::TagManager.instance.uri_to_resource(uri, Conversation) if ActivityPub::TagManager.instance.local_uri?(uri) begin Conversation.find_or_create_by!(uri: uri) diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 40adb5737..3174d1792 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -241,6 +241,12 @@ class ActivityPub::TagManager !host.nil? && (::TagManager.instance.local_domain?(host) || ::TagManager.instance.web_domain?(host)) end + def uri_to_local_id(uri, param = :id) + path_params = Rails.application.routes.recognize_path(uri) + path_params[:username] = Rails.configuration.x.local_domain if path_params[:controller] == 'instance_actors' + path_params[param] + end + def uris_to_local_accounts(uris) usernames = [] ids = [] @@ -258,14 +264,6 @@ class ActivityPub::TagManager uri_to_resource(uri, Account) end - def uri_to_local_conversation(uri) - path_params = Rails.application.routes.recognize_path(uri) - return unless path_params[:controller] == 'activitypub/contexts' - - account_id, conversation_id = path_params[:id].split('-') - Conversation.find_by(parent_account_id: account_id, id: conversation_id) - end - def uri_to_resource(uri, klass) return if uri.nil? @@ -273,8 +271,6 @@ class ActivityPub::TagManager case klass.name when 'Account' uris_to_local_accounts([uri]).first - when 'Conversation' - uri_to_local_conversation(uri) else StatusFinder.new(uri).status end diff --git a/app/lib/connection_pool/shared_timed_stack.rb b/app/lib/connection_pool/shared_timed_stack.rb index 8a13f4473..14a5285c4 100644 --- a/app/lib/connection_pool/shared_timed_stack.rb +++ b/app/lib/connection_pool/shared_timed_stack.rb @@ -70,7 +70,6 @@ class ConnectionPool::SharedTimedStack if @created == @max && !@queue.empty? throw_away_connection = @queue.pop @tagged_queue[throw_away_connection.site].delete(throw_away_connection) - throw_away_connection.close @create_block.call(preferred_tag) elsif @created != @max connection = @create_block.call(preferred_tag) diff --git a/app/lib/ostatus/tag_manager.rb b/app/lib/ostatus/tag_manager.rb index 7d0f23c4d..cb0c9f896 100644 --- a/app/lib/ostatus/tag_manager.rb +++ b/app/lib/ostatus/tag_manager.rb @@ -11,12 +11,16 @@ class OStatus::TagManager def unique_tag_to_local_id(tag, expected_type) return nil unless local_id?(tag) - matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag) - matches[1] unless matches.nil? + if ActivityPub::TagManager.instance.local_uri?(tag) + ActivityPub::TagManager.instance.uri_to_local_id(tag) + else + matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag) + matches[1] unless matches.nil? + end end def local_id?(id) - id.start_with?("tag:#{Rails.configuration.x.local_domain}") + id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id) end def uri_for(target) diff --git a/app/models/quote.rb b/app/models/quote.rb index e4f3b823f..4ad393e3a 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -47,8 +47,6 @@ class Quote < ApplicationRecord def accept! update!(state: :accepted) - - reset_parent_cache! if attribute_previously_changed?(:state) end def reject! @@ -77,15 +75,6 @@ class Quote < ApplicationRecord private - def reset_parent_cache! - return if status_id.nil? - - Rails.cache.delete("v3:statuses/#{status_id}") - - # This clears the web cache for the ActivityPub representation - Rails.cache.delete("statuses/show:v3:statuses/#{status_id}") - end - def set_accounts self.account = status.account self.quoted_account = quoted_status&.account diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index c45454fc7..1cdf0b483 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -204,11 +204,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService def update_tags! previous_tags = @status.tags.to_a - current_tags = @status.tags = @raw_tags.flat_map do |tag| - Tag.find_or_create_by_names([tag]).filter(&:valid?) - rescue ActiveRecord::RecordInvalid - [] - end + current_tags = @status.tags = Tag.find_or_create_by_names(@raw_tags) return unless @status.distributable? diff --git a/app/workers/feed_insert_worker.rb b/app/workers/feed_insert_worker.rb index b0c9c9181..e883daf3e 100644 --- a/app/workers/feed_insert_worker.rb +++ b/app/workers/feed_insert_worker.rb @@ -53,7 +53,7 @@ class FeedInsertWorker def notify?(filter_result) return false if @type != :home || @status.reblog? || (@status.reply? && @status.in_reply_to_account_id != @status.account_id) || - update? || filter_result == :filter + filter_result == :filter Follow.find_by(account: @follower, target_account: @status.account)&.notify? end diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index d0103dc66..1a5745a86 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -64,16 +64,6 @@ class MoveWorker .in_batches do |follows| ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id) num_moved += follows.update_all(target_account_id: @target_account.id) - - # Clear any relationship cache, since callbacks are not called - Rails.cache.delete_multi(follows.flat_map do |follow| - [ - ['relationship', follow.account_id, follow.target_account_id], - ['relationship', follow.target_account_id, follow.account_id], - ['relationship', follow.account_id, @target_account.id], - ['relationship', @target_account.id, follow.account_id], - ] - end) end num_moved diff --git a/docker-compose.yml b/docker-compose.yml index caff99be1..52d2a83f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: web: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.5.6 + image: ghcr.io/mastodon/mastodon:v4.5.5 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb @@ -83,7 +83,7 @@ services: # build: # dockerfile: ./streaming/Dockerfile # context: . - image: ghcr.io/mastodon/mastodon-streaming:v4.5.6 + image: ghcr.io/mastodon/mastodon-streaming:v4.5.5 restart: always env_file: .env.production command: node ./streaming/index.js @@ -102,7 +102,7 @@ services: sidekiq: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.5.6 + image: ghcr.io/mastodon/mastodon:v4.5.5 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 9a7415aa8..8fa37f225 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 6 + 5 end def default_prerelease diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 19b6014af..1e8a2a29d 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -471,7 +471,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'with a reply without explicitly setting a conversation' do + context 'with a reply' do let(:original_status) { Fabricate(:status) } let(:object_json) do @@ -493,30 +493,6 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'with a reply explicitly setting a conversation' do - let(:original_status) { Fabricate(:status) } - - let(:object_json) do - build_object( - inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status), - conversation: ActivityPub::TagManager.instance.uri_for(original_status.conversation), - context: ActivityPub::TagManager.instance.uri_for(original_status.conversation) - ) - end - - it 'creates status' do - expect { subject.perform }.to change(sender.statuses, :count).by(1) - - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.thread).to eq original_status - expect(status.reply?).to be true - expect(status.in_reply_to_account).to eq original_status.account - expect(status.conversation).to eq original_status.conversation - end - end - context 'with mentions' do let(:recipient) { Fabricate(:account) } diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index 7571d03f0..6cbb58055 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -612,6 +612,14 @@ RSpec.describe ActivityPub::TagManager do end end + describe '#uri_to_local_id' do + let(:account) { Fabricate(:account, id_scheme: :username_ap_id) } + + it 'returns the local ID' do + expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username + end + end + describe '#uris_to_local_accounts' do it 'returns the expected local accounts' do account = Fabricate(:account) diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 9dc1ece33..9d63c5f1f 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -258,7 +258,6 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do tag: [ { type: 'Hashtag', name: 'foo' }, { type: 'Hashtag', name: 'bar' }, - { type: 'Hashtag', name: '#2024' }, ], } end