Merge commit '8f1847df31f5195abb427854fa1e2bf1e8ba23a0' into glitch-soc/stable-4.6

This commit is contained in:
Claire committed 2026-09-15 15:29:20 +02:00
commit 222dfdf233
17 files changed
+76 -19

No files matched your search

+15 -1
View File
@@ -2,9 +2,23 @@
All notable changes to this project will be documented in this file.
## [4.6.8] - 2026-09-15
### Security
- Temporarily disable HEIF support
### Fixes
- Fix relative privacy policy links in subscription emails (#40486 by @crafkaz)
- Fix canonical email blocks interfering with freezing or approving users (#40463 by @ClearlyClaire)
- Fix 500 error when submitting a status twice (#40439 by @ClearlyClaire)
- Fix account deletion not deleting generated annual reports (#40394 by @ClearlyClaire)
- Fix notifications not being cleaned up when notification requests are deleted in bulk (#40393 by @ClearlyClaire)
## [4.6.7] - 2026-09-01
## Security
### Security
- Update dependencies
- Fix password authentication bypass in 2FA auth for LDAP/PAM/SSO accounts ([GHSA-vx32-x96w-qq65](https://github.com/mastodon/mastodon/security/advisories/GHSA-vx32-x96w-qq65))
@@ -45,6 +45,7 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
end
def dismiss_bulk
FilteredNotificationCleanupWorker.perform_async(current_account.id, @requests.map(&:from_account_id))
@requests.each(&:destroy!)
render_empty
end
@@ -40,6 +40,7 @@ module Account::Associations
has_many :statuses
has_many :keypairs
has_many :email_subscriptions
has_many :generated_annual_reports
has_one :deletion_request, class_name: 'AccountDeletionRequest'
has_one :follow_recommendation_suppression
+1 -1
View File
@@ -90,7 +90,7 @@ class User < ApplicationRecord
validates :email, presence: true, email_address: true, length: { maximum: 320 }
validates :email, email_mx: { attempt_ip: :sign_up_ip }, if: :validate_email_dns?
validates_with UserEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
validates_with UserEmailValidator, if: -> { (ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed?) && will_save_change_to_email? }
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
# Honeypot/anti-spam fields
+2
View File
@@ -28,6 +28,7 @@ class DeleteAccountService < BaseService
scheduled_statuses
status_pins
tag_follows
generated_annual_reports
).freeze
# The following associations have no important side-effects
@@ -53,6 +54,7 @@ class DeleteAccountService < BaseService
scheduled_statuses
status_pins
tag_follows
generated_annual_reports
).freeze
ASSOCIATIONS_ON_DESTROY = %w(
+13 -3
View File
@@ -20,6 +20,15 @@ class PostStatusService < BaseService
end
end
class IdempotencyError < StandardError
attr_reader :status
def initialize(status)
super()
@status = status
end
end
# Post a text status update, fetch and notify remote users mentioned
# @param [Account] account Account from which to post
# @param [Hash] options
@@ -46,9 +55,10 @@ class PostStatusService < BaseService
@in_reply_to = @options[:thread]
@quoted_status = @options[:quoted_status]
preprocess_attributes!
with_idempotency do
validate_media!
preprocess_attributes!
if scheduled?
schedule_status!
@@ -63,7 +73,7 @@ class PostStatusService < BaseService
end
@status
rescue Antispam::SilentlyDrop => e
rescue Antispam::SilentlyDrop, IdempotencyError => e
e.status
end
@@ -253,7 +263,7 @@ class PostStatusService < BaseService
return yield unless idempotency_given?
with_redis_lock("idempotency:lock:status:#{@account.id}:#{@options[:idempotency]}") do
return idempotency_duplicate if idempotency_duplicate?
raise IdempotencyError, idempotency_duplicate if idempotency_duplicate?
yield
@@ -17,6 +17,6 @@
- content_for :footer do
%p.email-footer-p= t('email_subscription_mailer.notification.footer.reason_for_email_html', name: display_name(@subscription.account), unsubscribe_path: @unsubscribe_url)
%p.email-footer-p= t('email_subscription_mailer.notification.footer.privacy_html', domain: @instance, privacy_policy_path: privacy_policy_path)
%p.email-footer-p= t('email_subscription_mailer.notification.footer.privacy_html', domain: @instance, privacy_policy_path: privacy_policy_url)
- if Setting.email_footer_text.present?
%p.email-footer-p= Setting.email_footer_text
@@ -33,6 +33,6 @@
- content_for :footer do
%p.email-footer-p= t('.footer.reason_for_email_html', name: display_name(@subscription.account), unsubscribe_path: @unsubscribe_url)
%p.email-footer-p= t('.footer.privacy_html', domain: @instance, privacy_policy_path: privacy_policy_path)
%p.email-footer-p= t('.footer.privacy_html', domain: @instance, privacy_policy_path: privacy_policy_url)
- if Setting.email_footer_text.present?
%p.email-footer-p= Setting.email_footer_text
@@ -3,7 +3,7 @@
class FilteredNotificationCleanupWorker
include Sidekiq::Worker
def perform(account_id, from_account_id)
Notification.where(account_id: account_id, from_account_id: from_account_id, filtered: true).in_batches(order: :desc).delete_all
def perform(account_id, from_account_ids)
Notification.where(account_id: account_id, from_account_id: from_account_ids, filtered: true).in_batches(order: :desc).delete_all
end
end
-1
View File
@@ -17,7 +17,6 @@ Vips.block('VipsForeign', true)
VipsForeignLoadJpeg
VipsForeignLoadPng
VipsForeignLoadWebp
VipsForeignLoadHeif
VipsForeignSavePng
VipsForeignSaveSpng
VipsForeignSaveJpeg
+3 -3
View File
@@ -60,7 +60,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/glitch-soc/mastodon:v4.6.7
image: ghcr.io/glitch-soc/mastodon:v4.6.8
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
@@ -84,7 +84,7 @@ services:
# build:
# dockerfile: ./streaming/Dockerfile
# context: .
image: ghcr.io/glitch-soc/mastodon-streaming:v4.6.7
image: ghcr.io/glitch-soc/mastodon-streaming:v4.6.8
restart: always
env_file: .env.production
command: node ./streaming/index.js
@@ -103,7 +103,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/glitch-soc/mastodon:v4.6.7
image: ghcr.io/glitch-soc/mastodon:v4.6.8
restart: always
env_file: .env.production
command: bundle exec sidekiq
+1 -1
View File
@@ -13,7 +13,7 @@ module Mastodon
end
def patch
7
8
end
def default_prerelease
@@ -3,6 +3,13 @@
require 'rails_helper'
RSpec.describe EmailSubscriptionMailer do
shared_examples 'an absolute privacy policy link' do
it 'links to the privacy policy with an absolute URL' do
expect(mail.html_part.body.decoded)
.to include(%(href="#{privacy_policy_url}"))
end
end
describe '.confirmation' do
let(:email_subscription) { Fabricate(:email_subscription) }
let(:mail) { described_class.with(subscription: email_subscription).confirmation }
@@ -15,6 +22,8 @@ RSpec.describe EmailSubscriptionMailer do
subject: I18n.t('email_subscription_mailer.confirmation.subject')
)
end
it_behaves_like 'an absolute privacy policy link'
end
describe '.notification' do
@@ -33,6 +42,8 @@ RSpec.describe EmailSubscriptionMailer do
subject: I18n.t('email_subscription_mailer.notification.subject.singular', name: email_subscription.account.display_name, excerpt: statuses.first.text.truncate(17))
)
end
it_behaves_like 'an absolute privacy policy link'
end
context 'with multiple statuses' do
@@ -122,7 +122,9 @@ RSpec.describe 'Requests' do
it_behaves_like 'forbidden for wrong scope', 'read read:notifications'
it 'returns http success and destroys the notification request', :aggregate_failures do
expect { subject }.to change(NotificationRequest, :count).by(-1)
expect { subject }
.to change(NotificationRequest, :count).by(-1)
.and enqueue_sidekiq_job(FilteredNotificationCleanupWorker).with(user.account_id, be_a(Array))
expect(response).to have_http_status(200)
expect(response.content_type)
@@ -26,6 +26,7 @@ RSpec.describe DeleteAccountService do
let!(:follow_notification) { Fabricate(:notification, account: local_follower, activity: active_relationship, type: :follow) }
let!(:account_note) { Fabricate(:account_note, account: account) }
let!(:generated_annual_report) { Fabricate(:generated_annual_report, account: account) }
it 'deletes associated owned and target records and target notifications' do
subject
@@ -48,6 +49,7 @@ RSpec.describe DeleteAccountService do
expect { poll_vote.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { account_note.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { collection.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { generated_annual_report.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
def expect_deletion_of_associated_target_records
+4 -4
View File
@@ -57,8 +57,8 @@ RSpec.describe PostStatusService do
it 'returns existing status when used twice with idempotency key' do
account = Fabricate(:account)
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep', scheduled_at: future)
status2 = subject.call(account, text: 'test', idempotency: 'meepmeep', scheduled_at: future)
status1 = described_class.new.call(account, text: 'test', idempotency: 'meepmeep', scheduled_at: future)
status2 = described_class.new.call(account, text: 'test', idempotency: 'meepmeep', scheduled_at: future)
expect(status2.id).to eq status1.id
end
@@ -341,8 +341,8 @@ RSpec.describe PostStatusService do
it 'returns existing status when used twice with idempotency key' do
account = Fabricate(:account)
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep')
status2 = subject.call(account, text: 'test', idempotency: 'meepmeep')
status1 = described_class.new.call(account, text: 'test', idempotency: 'meepmeep')
status2 = described_class.new.call(account, text: 'test', idempotency: 'meepmeep')
expect(status2.id).to eq status1.id
end
@@ -20,5 +20,20 @@ RSpec.describe FilteredNotificationCleanupWorker do
.to change { recipient.notifications.where(from_account: sender).count }.from(2).to(0)
.and(not_change { recipient.notifications.where(from_account: bystander).count })
end
context 'when given an array of IDs as parameter' do
let(:other_sender) { Fabricate(:account) }
before do
Fabricate(:notification, account: recipient, activity: Fabricate(:favourite, account: other_sender), filtered: true)
end
it 'deletes all filtered notifications to the account' do
expect { described_class.new.perform(recipient.id, [sender.id, other_sender.id]) }
.to change { recipient.notifications.where(from_account: sender).count }.from(2).to(0)
.and change { recipient.notifications.where(from_account: other_sender).count }.from(1).to(0)
.and(not_change { recipient.notifications.where(from_account: bystander).count })
end
end
end
end