diff --git a/CHANGELOG.md b/CHANGELOG.md index cfbc450d74..c6c0ff20d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to this project will be documented in this file. +## [4.5.7] - 2026-02-24 + +### Security + +- Reject unconfirmed FASPs (#37926 by @oneiros, [GHSA-qgmm-vr4c-ggjg](https://github.com/mastodon/mastodon/security/advisories/GHSA-qgmm-vr4c-ggjg)) +- Re-use custom socket class for FASP requests (#37925 by @oneiros, [GHSA-46w6-g98f-wxqm](https://github.com/mastodon/mastodon/security/advisories/GHSA-46w6-g98f-wxqm)) + +### Added + +- Add `--suspended-only` option to `tootctl emoji purge` (#37828 and #37861 by @ClearlyClaire and @mjankowski) + +### Fixed + +- Fix emoji data not being properly cached (#37858 by @ChaosExAnima) +- Fix delete & redraft of pending posts (#37839 by @ClearlyClaire) +- Fix processing separate key documents without the ActivityStreams context (#37826 by @ClearlyClaire) +- Fix custom emojis not being purged on domain suspension (#37808 by @ClearlyClaire) +- Fix users without special permissions being able to stream disabled timelines (#37791 by @ClearlyClaire) +- Fix processing of object updates with duplicate hashtags (#37756 by @ClearlyClaire) + ## [4.5.6] - 2026-02-03 ### Security diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index 3e2cf0e3cd..196d0ef3a7 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -1,11 +1,41 @@ # frozen_string_literal: true class Api::V1::ProfilesController < Api::BaseController - before_action -> { doorkeeper_authorize! :profile, :read, :'read:accounts' } + before_action -> { doorkeeper_authorize! :profile, :read, :'read:accounts' }, except: [:update] + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update] before_action :require_user! def show @account = current_account render json: @account, serializer: REST::ProfileSerializer end + + def update + @account = current_account + UpdateAccountService.new.call(@account, account_params, raise_error: true) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + + render json: @account, serializer: REST::ProfileSerializer + rescue ActiveRecord::RecordInvalid => e + render json: ValidationErrorFormatter.new(e).as_json, status: 422 + end + + def account_params + params.permit( + :display_name, + :note, + :avatar, + :header, + :locked, + :bot, + :discoverable, + :hide_collections, + :indexable, + :show_media, + :show_media_replies, + :show_featured, + attribution_domains: [], + fields_attributes: [:name, :value] + ) + end end diff --git a/app/models/account.rb b/app/models/account.rb index 6538df90ab..1b2da6ce0e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -323,16 +323,16 @@ class Account < ApplicationRecord old_fields = self[:fields] || [] old_fields = [] if old_fields.is_a?(Hash) - if attributes.is_a?(Hash) - attributes.each_value do |attr| - next if attr[:name].blank? && attr[:value].blank? + attributes = attributes.values if attributes.is_a?(Hash) - previous = old_fields.find { |item| item['value'] == attr[:value] } + attributes.each do |attr| + next if attr[:name].blank? && attr[:value].blank? - attr[:verified_at] = previous['verified_at'] if previous && previous['verified_at'].present? + previous = old_fields.find { |item| item['value'] == attr[:value] } - fields << attr - end + attr[:verified_at] = previous['verified_at'] if previous && previous['verified_at'].present? + + fields << attr end self[:fields] = fields diff --git a/config/routes/api.rb b/config/routes/api.rb index 6042afc205..828029a01b 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -113,7 +113,7 @@ namespace :api, format: false do resources :endorsements, only: [:index] resources :markers, only: [:index, :create] - resource :profile, only: [:show] do + resource :profile, only: [:show, :update] do scope module: :profile do resource :avatar, only: :destroy resource :header, only: :destroy diff --git a/docker-compose.yml b/docker-compose.yml index cf0e198222..b189fecf63 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/glitch-soc/mastodon:v4.5.6 + image: ghcr.io/glitch-soc/mastodon:v4.5.7 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/glitch-soc/mastodon-streaming:v4.5.6 + image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.7 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/glitch-soc/mastodon:v4.5.6 + image: ghcr.io/glitch-soc/mastodon:v4.5.7 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 5a64867efd..5231e44799 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -17,7 +17,7 @@ module Mastodon end def default_prerelease - 'alpha.4' + 'alpha.5' end def prerelease diff --git a/spec/requests/api/v1/profiles_spec.rb b/spec/requests/api/v1/profiles_spec.rb index d9c2ffaef5..fa4e8d9a57 100644 --- a/spec/requests/api/v1/profiles_spec.rb +++ b/spec/requests/api/v1/profiles_spec.rb @@ -53,6 +53,80 @@ RSpec.describe 'Profile API' do end end + describe 'PATCH /api/v1/profile' do + subject do + patch '/api/v1/profile', headers: headers, params: params + end + + let(:params) do + { + avatar: fixture_file_upload('avatar.gif', 'image/gif'), + discoverable: true, + display_name: "Alice Isn't Dead", + header: fixture_file_upload('attachment.jpg', 'image/jpeg'), + indexable: true, + locked: false, + note: 'Hello!', + attribution_domains: ['example.com'], + fields_attributes: [ + { name: 'pronouns', value: 'she/her' }, + { name: 'foo', value: 'bar' }, + ], + } + end + + it_behaves_like 'forbidden for wrong scope', 'read read:accounts' + + describe 'with invalid data' do + let(:params) { { note: 'a' * 2 * Account::NOTE_LENGTH_LIMIT } } + + it 'returns http unprocessable entity' do + subject + expect(response).to have_http_status(422) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body) + .to include( + error: /Validation failed/, + details: include(note: contain_exactly(include(error: 'ERR_TOO_LONG', description: /character limit/))) + ) + end + end + + it 'returns http success with updated JSON attributes' do + subject + + expect(response) + .to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body) + .to include({ + locked: false, + }) + expect(user.account.reload) + .to have_attributes( + display_name: eq("Alice Isn't Dead"), + note: 'Hello!', + avatar: exist, + header: exist, + attribution_domains: ['example.com'], + fields: contain_exactly( + have_attributes( + name: 'pronouns', + value: 'she/her' + ), + have_attributes( + name: 'foo', + value: 'bar' + ) + ) + ) + expect(ActivityPub::UpdateDistributionWorker) + .to have_enqueued_sidekiq_job(user.account_id) + end + end + describe 'DELETE /api/v1/profile/avatar' do context 'with wrong scope' do before do