Merge tag 'v4.4.0-rc.1' into chinwag-next

This commit is contained in:
Mike Barnes 2025-09-14 11:47:14 +10:00
commit fbbcaf4efd
2660 changed files with 83548 additions and 52192 deletions

View file

@ -49,7 +49,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_collection_url(@account, params[:id]),
id: ActivityPub::TagManager.instance.collection_uri_for(@account, params[:id]),
type: @type,
size: @size,
items: @items

View file

@ -41,12 +41,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
end
end
def outbox_url(**kwargs)
if params[:account_username].present?
account_outbox_url(@account, **kwargs)
else
instance_actor_outbox_url(**kwargs)
end
def outbox_url(...)
ActivityPub::TagManager.instance.outbox_uri_for(@account, ...)
end
def next_page

View file

@ -34,7 +34,8 @@ module Admin
end
def resource_params
params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses)
params
.expect(admin_account_action: [:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses])
end
end
end

View file

@ -29,10 +29,8 @@ module Admin
private
def resource_params
params.require(:account_moderation_note).permit(
:content,
:target_account_id
)
params
.expect(account_moderation_note: [:content, :target_account_id])
end
def set_account_moderation_note

View file

@ -158,7 +158,8 @@ module Admin
end
def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
params
.expect(form_account_batch: [:action, account_ids: []])
end
def action_from_button

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class Admin::Announcements::DistributionsController < Admin::BaseController
before_action :set_announcement
def create
authorize @announcement, :distribute?
@announcement.touch(:notification_sent_at)
Admin::DistributeAnnouncementNotificationWorker.perform_async(@announcement.id)
redirect_to admin_announcements_path
end
private
def set_announcement
@announcement = Announcement.find(params[:announcement_id])
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Admin::Announcements::PreviewsController < Admin::BaseController
before_action :set_announcement
def show
authorize @announcement, :distribute?
@user_count = @announcement.scope_for_notification.count
end
private
def set_announcement
@announcement = Announcement.find(params[:announcement_id])
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Admin::Announcements::TestsController < Admin::BaseController
before_action :set_announcement
def create
authorize @announcement, :distribute?
UserMailer.announcement_published(current_user, @announcement).deliver_later!
redirect_to admin_announcements_path
end
private
def set_announcement
@announcement = Announcement.find(params[:announcement_id])
end
end

View file

@ -84,6 +84,7 @@ class Admin::AnnouncementsController < Admin::BaseController
end
def resource_params
params.require(:announcement).permit(:text, :scheduled_at, :starts_at, :ends_at, :all_day)
params
.expect(announcement: [:text, :scheduled_at, :starts_at, :ends_at, :all_day])
end
end

View file

@ -7,14 +7,14 @@ module Admin
layout 'admin'
before_action :set_cache_headers
before_action :set_referrer_policy_header
after_action :verify_authorized
private
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
def set_referrer_policy_header
response.headers['Referrer-Policy'] = 'same-origin'
end
def set_user

View file

@ -41,9 +41,8 @@ module Admin
end
def resource_params
params.require(:user).permit(
:unconfirmed_email
)
params
.expect(user: [:unconfirmed_email])
end
end
end

View file

@ -44,7 +44,8 @@ module Admin
private
def resource_params
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
params
.expect(custom_emoji: [:shortcode, :image, :visible_in_picker])
end
def filtered_custom_emojis
@ -74,7 +75,8 @@ module Admin
end
def form_custom_emoji_batch_params
params.require(:form_custom_emoji_batch).permit(:action, :category_id, :category_name, custom_emoji_ids: [])
params
.expect(form_custom_emoji_batch: [:action, :category_id, :category_name, custom_emoji_ids: []])
end
end
end

View file

@ -37,6 +37,7 @@ class Admin::DomainAllowsController < Admin::BaseController
end
def resource_params
params.require(:domain_allow).permit(:domain)
params
.expect(domain_allow: [:domain])
end
end

View file

@ -25,7 +25,9 @@ module Admin
rescue Mastodon::NotPermittedError
flash[:alert] = I18n.t('admin.domain_blocks.not_permitted')
else
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
flash[:notice] = I18n.t('admin.domain_blocks.created_msg')
ensure
redirect_to admin_instances_path(limited: '1')
end
def new
@ -114,7 +116,12 @@ module Admin
end
def form_domain_block_batch_params
params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate])
params
.expect(
form_domain_block_batch: [
domain_blocks_attributes: [[:enabled, :domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate]],
]
)
end
def action_from_button

View file

@ -5,7 +5,7 @@ module Admin
def index
authorize :email_domain_block, :index?
@email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page])
@email_domain_blocks = EmailDomainBlock.parents.includes(:children).order(id: :desc).page(params[:page])
@form = Form::EmailDomainBlockBatch.new
end
@ -58,18 +58,17 @@ module Admin
private
def set_resolved_records
Resolv::DNS.open do |dns|
dns.timeouts = 5
@resolved_records = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a
end
@resolved_records = DomainResource.new(@email_domain_block.domain).mx
end
def resource_params
params.require(:email_domain_block).permit(:domain, :allow_with_approval, other_domains: [])
params
.expect(email_domain_block: [:domain, :allow_with_approval, other_domains: []])
end
def form_email_domain_block_batch_params
params.require(:form_email_domain_block_batch).permit(email_domain_block_ids: [])
params
.expect(form_email_domain_block_batch: [email_domain_block_ids: []])
end
def action_from_button

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
class Admin::Fasp::Debug::CallbacksController < Admin::BaseController
def index
authorize [:admin, :fasp, :provider], :update?
@callbacks = Fasp::DebugCallback
.includes(:fasp_provider)
.order(created_at: :desc)
end
def destroy
authorize [:admin, :fasp, :provider], :update?
callback = Fasp::DebugCallback.find(params[:id])
callback.destroy
redirect_to admin_fasp_debug_callbacks_path
end
end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
class Admin::Fasp::DebugCallsController < Admin::BaseController
before_action :set_provider
def create
authorize [:admin, @provider], :update?
@provider.perform_debug_call
redirect_to admin_fasp_providers_path
end
private
def set_provider
@provider = Fasp::Provider.find(params[:provider_id])
end
end

View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
class Admin::Fasp::ProvidersController < Admin::BaseController
before_action :set_provider, only: [:show, :edit, :update, :destroy]
def index
authorize [:admin, :fasp, :provider], :index?
@providers = Fasp::Provider.order(confirmed: :asc, created_at: :desc)
end
def show
authorize [:admin, @provider], :show?
end
def edit
authorize [:admin, @provider], :update?
end
def update
authorize [:admin, @provider], :update?
if @provider.update(provider_params)
redirect_to admin_fasp_providers_path
else
render :edit
end
end
def destroy
authorize [:admin, @provider], :destroy?
@provider.destroy
redirect_to admin_fasp_providers_path
end
private
def provider_params
params.expect(fasp_provider: [capabilities_attributes: {}])
end
def set_provider
@provider = Fasp::Provider.find(params[:id])
end
end

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class Admin::Fasp::RegistrationsController < Admin::BaseController
before_action :set_provider
def new
authorize [:admin, @provider], :create?
end
def create
authorize [:admin, @provider], :create?
@provider.update_info!(confirm: true)
redirect_to edit_admin_fasp_provider_path(@provider)
end
private
def set_provider
@provider = Fasp::Provider.find(params[:provider_id])
end
end

View file

@ -37,7 +37,8 @@ module Admin
end
def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
params
.expect(form_account_batch: [:action, account_ids: []])
end
def filter_params

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
class Admin::Instances::ModerationNotesController < Admin::BaseController
before_action :set_instance, only: [:create]
before_action :set_instance_note, only: [:destroy]
def create
authorize :instance_moderation_note, :create?
@instance_moderation_note = current_account.instance_moderation_notes.new(content: resource_params[:content], domain: @instance.domain)
if @instance_moderation_note.save
redirect_to admin_instance_path(@instance.domain, anchor: helpers.dom_id(@instance_moderation_note)), notice: I18n.t('admin.instances.moderation_notes.created_msg')
else
@instance_moderation_notes = @instance.moderation_notes.includes(:account).chronological
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(5)
render 'admin/instances/show'
end
end
def destroy
authorize @instance_moderation_note, :destroy?
@instance_moderation_note.destroy!
redirect_to admin_instance_path(@instance_moderation_note.domain, anchor: 'instance-notes'), notice: I18n.t('admin.instances.moderation_notes.destroyed_msg')
end
private
def resource_params
params
.expect(instance_moderation_note: [:content])
end
def set_instance
domain = params[:instance_id]&.strip
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(domain))
end
def set_instance_note
@instance_moderation_note = InstanceModerationNote.find(params[:id])
end
end

View file

@ -5,6 +5,8 @@ module Admin
before_action :set_instances, only: :index
before_action :set_instance, except: :index
LOGS_LIMIT = 5
def index
authorize :instance, :index?
preload_delivery_failures!
@ -12,8 +14,11 @@ module Admin
def show
authorize :instance, :show?
@instance_moderation_note = @instance.moderation_notes.new
@instance_moderation_notes = @instance.moderation_notes.includes(:account).chronological
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(5)
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT)
end
def destroy
@ -50,7 +55,8 @@ module Admin
private
def set_instance
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(params[:id]&.strip))
domain = params[:id]&.strip
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(domain))
end
def set_instances

View file

@ -32,14 +32,15 @@ module Admin
def deactivate_all
authorize :invite, :deactivate_all?
Invite.available.in_batches.update_all(expires_at: Time.now.utc)
Invite.available.in_batches.touch_all(:expires_at)
redirect_to admin_invites_path
end
private
def resource_params
params.require(:invite).permit(:max_uses, :expires_in)
params
.expect(invite: [:max_uses, :expires_in])
end
def filtered_invites

View file

@ -44,7 +44,8 @@ module Admin
private
def resource_params
params.require(:ip_block).permit(:ip, :severity, :comment, :expires_in)
params
.expect(ip_block: [:ip, :severity, :comment, :expires_in])
end
def action_from_button
@ -52,7 +53,8 @@ module Admin
end
def form_ip_block_batch_params
params.require(:form_ip_block_batch).permit(ip_block_ids: [])
params
.expect(form_ip_block_batch: [ip_block_ids: []])
end
end
end

View file

@ -21,6 +21,7 @@ module Admin
@relay = Relay.new(resource_params)
if @relay.save
log_action :create, @relay
@relay.enable!
redirect_to admin_relays_path
else
@ -31,18 +32,21 @@ module Admin
def destroy
authorize :relay, :update?
@relay.destroy
log_action :destroy, @relay
redirect_to admin_relays_path
end
def enable
authorize :relay, :update?
@relay.enable!
log_action :enable, @relay
redirect_to admin_relays_path
end
def disable
authorize :relay, :update?
@relay.disable!
log_action :disable, @relay
redirect_to admin_relays_path
end
@ -53,7 +57,8 @@ module Admin
end
def resource_params
params.require(:relay).permit(:inbox_url)
params
.expect(relay: [:inbox_url])
end
def warn_signatures_not_enabled!

View file

@ -47,10 +47,8 @@ module Admin
end
def resource_params
params.require(:report_note).permit(
:content,
:report_id
)
params
.expect(report_note: [:content, :report_id])
end
def set_report_note

View file

@ -61,7 +61,8 @@ module Admin
end
def resource_params
params.require(:user_role).permit(:name, :color, :highlighted, :position, permissions_as_keys: [])
params
.expect(user_role: [:name, :color, :highlighted, :position, permissions_as_keys: []])
end
end
end

View file

@ -2,17 +2,24 @@
module Admin
class RulesController < BaseController
before_action :set_rule, except: [:index, :create]
before_action :set_rule, except: [:index, :new, :create]
def index
authorize :rule, :index?
@rules = Rule.ordered
@rule = Rule.new
@rules = Rule.ordered.includes(:translations)
end
def new
authorize :rule, :create?
@rule = Rule.new
end
def edit
authorize @rule, :update?
missing_languages = RuleTranslation.languages - @rule.translations.pluck(:language)
missing_languages.each { |lang| @rule.translations.build(language: lang) }
end
def create
@ -23,8 +30,7 @@ module Admin
if @rule.save
redirect_to admin_rules_path
else
@rules = Rule.ordered
render :index
render :new
end
end
@ -46,6 +52,22 @@ module Admin
redirect_to admin_rules_path
end
def move_up
authorize @rule, :update?
@rule.move!(-1)
redirect_to admin_rules_path
end
def move_down
authorize @rule, :update?
@rule.move!(+1)
redirect_to admin_rules_path
end
private
def set_rule
@ -53,7 +75,8 @@ module Admin
end
def resource_params
params.require(:rule).permit(:text, :hint, :priority)
params
.expect(rule: [:text, :hint, :priority, translations_attributes: [[:id, :language, :text, :hint, :_destroy]]])
end
end
end

View file

@ -28,7 +28,8 @@ module Admin
end
def settings_params
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
params
.expect(form_admin_settings: [*Form::AdminSettings::KEYS])
end
end
end

View file

@ -6,7 +6,7 @@ module Admin
def index
authorize :software_update, :index?
@software_updates = SoftwareUpdate.all.sort_by(&:gem_version)
@software_updates = SoftwareUpdate.by_version.filter(&:pending?)
end
private

View file

@ -16,6 +16,8 @@ module Admin
def show
authorize [:admin, @status], :show?
@status_batch_action = Admin::StatusBatchAction.new
end
def batch
@ -37,7 +39,8 @@ module Admin
helper_method :batched_ordered_status_edits
def admin_status_batch_action_params
params.require(:admin_status_batch_action).permit(status_ids: [])
params
.expect(admin_status_batch_action: [status_ids: []])
end
def after_create_redirect_path

View file

@ -37,7 +37,8 @@ module Admin
end
def tag_params
params.require(:tag).permit(:name, :display_name, :trendable, :usable, :listable)
params
.expect(tag: [:name, :display_name, :trendable, :usable, :listable])
end
def filtered_tags

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class Admin::TermsOfService::DistributionsController < Admin::BaseController
before_action :set_terms_of_service
def create
authorize @terms_of_service, :distribute?
@terms_of_service.touch(:notification_sent_at)
Admin::DistributeTermsOfServiceNotificationWorker.perform_async(@terms_of_service.id)
redirect_to admin_terms_of_service_index_path
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View file

@ -0,0 +1,37 @@
# frozen_string_literal: true
class Admin::TermsOfService::DraftsController < Admin::BaseController
before_action :set_terms_of_service
def show
authorize :terms_of_service, :create?
end
def update
authorize @terms_of_service, :update?
@terms_of_service.published_at = Time.now.utc if params[:action_type] == 'publish'
if @terms_of_service.update(resource_params)
log_action(:publish, @terms_of_service) if @terms_of_service.published?
redirect_to @terms_of_service.published? ? admin_terms_of_service_index_path : admin_terms_of_service_draft_path
else
render :show
end
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text, effective_date: 10.days.from_now)
end
def current_terms_of_service
TermsOfService.live.first
end
def resource_params
params
.expect(terms_of_service: [:text, :changelog, :effective_date])
end
end

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
class Admin::TermsOfService::GeneratesController < Admin::BaseController
before_action :set_instance_presenter
def show
authorize :terms_of_service, :create?
@generator = TermsOfService::Generator.new(
domain: @instance_presenter.domain,
admin_email: @instance_presenter.contact.email
)
end
def create
authorize :terms_of_service, :create?
@generator = TermsOfService::Generator.new(resource_params)
if @generator.valid?
TermsOfService.create!(text: @generator.render)
redirect_to admin_terms_of_service_draft_path
else
render :show
end
end
private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def resource_params
params
.expect(terms_of_service_generator: [*TermsOfService::Generator::VARIABLES])
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Admin::TermsOfService::HistoriesController < Admin::BaseController
def show
authorize :terms_of_service, :index?
@terms_of_service = TermsOfService.published.all
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Admin::TermsOfService::PreviewsController < Admin::BaseController
before_action :set_terms_of_service
def show
authorize @terms_of_service, :distribute?
@user_count = @terms_of_service.scope_for_notification.count
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Admin::TermsOfService::TestsController < Admin::BaseController
before_action :set_terms_of_service
def create
authorize @terms_of_service, :distribute?
UserMailer.terms_of_service_changed(current_user, @terms_of_service).deliver_later!
redirect_to admin_terms_of_service_preview_path(@terms_of_service)
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Admin::TermsOfServiceController < Admin::BaseController
def index
authorize :terms_of_service, :index?
@terms_of_service = TermsOfService.published.first
end
end

View file

@ -31,7 +31,8 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll
end
def trends_preview_card_provider_batch_params
params.require(:trends_preview_card_provider_batch).permit(:action, preview_card_provider_ids: [])
params
.expect(trends_preview_card_provider_batch: [:action, preview_card_provider_ids: []])
end
def action_from_button

View file

@ -4,7 +4,7 @@ class Admin::Trends::LinksController < Admin::BaseController
def index
authorize :preview_card, :review?
@locales = PreviewCardTrend.pluck('distinct language')
@locales = PreviewCardTrend.locales
@preview_cards = filtered_preview_cards.page(params[:page])
@form = Trends::PreviewCardBatch.new
end
@ -31,7 +31,8 @@ class Admin::Trends::LinksController < Admin::BaseController
end
def trends_preview_card_batch_params
params.require(:trends_preview_card_batch).permit(:action, preview_card_ids: [])
params
.expect(trends_preview_card_batch: [:action, preview_card_ids: []])
end
def action_from_button

View file

@ -4,7 +4,7 @@ class Admin::Trends::StatusesController < Admin::BaseController
def index
authorize [:admin, :status], :review?
@locales = StatusTrend.pluck('distinct language')
@locales = StatusTrend.locales
@statuses = filtered_statuses.page(params[:page])
@form = Trends::StatusBatch.new
end
@ -31,7 +31,8 @@ class Admin::Trends::StatusesController < Admin::BaseController
end
def trends_status_batch_params
params.require(:trends_status_batch).permit(:action, status_ids: [])
params
.expect(trends_status_batch: [:action, status_ids: []])
end
def action_from_button

View file

@ -4,7 +4,7 @@ class Admin::Trends::TagsController < Admin::BaseController
def index
authorize :tag, :review?
@pending_tags_count = Tag.pending_review.async_count
@pending_tags_count = pending_tags.async_count
@tags = filtered_tags.page(params[:page])
@form = Trends::TagBatch.new
end
@ -22,6 +22,10 @@ class Admin::Trends::TagsController < Admin::BaseController
private
def pending_tags
Trends::TagFilter.new(status: :pending_review).results
end
def filtered_tags
Trends::TagFilter.new(filter_params).results
end
@ -31,7 +35,8 @@ class Admin::Trends::TagsController < Admin::BaseController
end
def trends_tag_batch_params
params.require(:trends_tag_batch).permit(:action, tag_ids: [])
params
.expect(trends_tag_batch: [:action, tag_ids: []])
end
def action_from_button

View file

@ -28,7 +28,8 @@ module Admin
end
def resource_params
params.require(:user).permit(:role_id)
params
.expect(user: [:role_id])
end
end
end

View file

@ -52,7 +52,8 @@ module Admin
end
def warning_preset_params
params.require(:account_warning_preset).permit(:title, :text)
params
.expect(account_warning_preset: [:title, :text])
end
end
end

View file

@ -74,7 +74,8 @@ module Admin
end
def resource_params
params.require(:webhook).permit(:url, :template, events: [])
params
.expect(webhook: [:url, :template, events: []])
end
end
end

View file

@ -50,6 +50,10 @@ class Api::BaseController < ApplicationController
nil
end
def require_client_credentials!
render json: { error: 'This method requires an client credentials authentication' }, status: 403 if doorkeeper_token.resource_owner_id.present?
end
def require_authenticated_user!
render json: { error: 'This method requires an authenticated user' }, status: 401 unless current_user
end
@ -72,6 +76,13 @@ class Api::BaseController < ApplicationController
end
end
# Redefine `require_functional!` to properly output JSON instead of HTML redirects
def require_functional!
return if current_user.functional?
require_user!
end
def render_empty
render json: {}, status: 200
end
@ -81,7 +92,7 @@ class Api::BaseController < ApplicationController
end
def disallow_unauthenticated_api_access?
ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.limited_federation_mode
ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.mastodon.limited_federation_mode
end
private

View file

@ -0,0 +1,66 @@
# frozen_string_literal: true
class Api::Fasp::BaseController < ApplicationController
class Error < ::StandardError; end
DIGEST_PATTERN = /sha-256=:(.*?):/
KEYID_PATTERN = /keyid="(.*?)"/
attr_reader :current_provider
skip_forgery_protection
before_action :check_fasp_enabled
before_action :require_authentication
after_action :sign_response
private
def require_authentication
validate_content_digest!
validate_signature!
rescue Error, Linzer::Error, ActiveRecord::RecordNotFound => e
logger.debug("FASP Authentication error: #{e}")
authentication_error
end
def authentication_error
respond_to do |format|
format.json { head 401 }
end
end
def validate_content_digest!
content_digest_header = request.headers['content-digest']
raise Error, 'content-digest missing' if content_digest_header.blank?
digest_received = content_digest_header.match(DIGEST_PATTERN)[1]
digest_computed = OpenSSL::Digest.base64digest('sha256', request.body&.string || '')
raise Error, 'content-digest does not match' if digest_received != digest_computed
end
def validate_signature!
raise Error, 'signature-input is missing' if request.headers['signature-input'].blank?
provider = nil
Linzer.verify!(request.rack_request, no_older_than: 5.minutes) do |keyid|
provider = Fasp::Provider.find(keyid)
Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid)
end
@current_provider = provider
end
def sign_response
response.headers['content-digest'] = "sha-256=:#{OpenSSL::Digest.base64digest('sha256', response.body || '')}:"
key = Linzer.new_ed25519_key(current_provider.server_private_key_pem)
Linzer.sign!(response, key:, components: %w(@status content-digest))
end
def check_fasp_enabled
raise ActionController::RoutingError unless Mastodon::Feature.fasp_enabled?
end
end

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
class Api::Fasp::DataSharing::V0::BackfillRequestsController < Api::Fasp::BaseController
def create
backfill_request = current_provider.fasp_backfill_requests.new(backfill_request_params)
respond_to do |format|
format.json do
if backfill_request.save
render json: { backfillRequest: { id: backfill_request.id } }, status: 201
else
head 422
end
end
end
end
private
def backfill_request_params
params
.permit(:category, :maxCount)
.to_unsafe_h
.transform_keys { |k| k.to_s.underscore }
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
class Api::Fasp::DataSharing::V0::ContinuationsController < Api::Fasp::BaseController
def create
backfill_request = current_provider.fasp_backfill_requests.find(params[:backfill_request_id])
Fasp::BackfillWorker.perform_async(backfill_request.id)
head 204
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
class Api::Fasp::DataSharing::V0::EventSubscriptionsController < Api::Fasp::BaseController
def create
subscription = current_provider.fasp_subscriptions.create!(subscription_params)
render json: { subscription: { id: subscription.id } }, status: 201
end
def destroy
subscription = current_provider.fasp_subscriptions.find(params[:id])
subscription.destroy
head 204
end
private
def subscription_params
params
.permit(:category, :subscriptionType, :maxBatchSize, threshold: {})
.to_unsafe_h
.transform_keys { |k| k.to_s.underscore }
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class Api::Fasp::Debug::V0::Callback::ResponsesController < Api::Fasp::BaseController
def create
Fasp::DebugCallback.create(
fasp_provider: current_provider,
ip: request.remote_ip,
request_body: request.raw_post
)
respond_to do |format|
format.json { head 201 }
end
end
end

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
class Api::Fasp::RegistrationsController < Api::Fasp::BaseController
skip_before_action :require_authentication
def create
@current_provider = Fasp::Provider.create!(
name: params[:name],
base_url: params[:baseUrl],
remote_identifier: params[:serverId],
provider_public_key_base64: params[:publicKey]
)
render json: registration_confirmation
end
private
def registration_confirmation
{
faspId: current_provider.id.to_s,
publicKey: current_provider.server_public_key_base64,
registrationCompletionUri: new_admin_fasp_provider_registration_url(current_provider),
}
end
end

View file

@ -14,7 +14,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
@account = current_account
UpdateAccountService.new.call(@account, account_params, raise_error: true)
current_user.update(user_params) if user_params
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
rescue ActiveRecord::RecordInvalid => e
render json: ValidationErrorFormatter.new(e).as_json, status: 422
@ -33,6 +33,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
:discoverable,
:hide_collections,
:indexable,
attribution_domains: [],
fields_attributes: [:name, :value]
)
end

View file

@ -0,0 +1,66 @@
# frozen_string_literal: true
class Api::V1::Accounts::EndorsementsController < Api::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
before_action :require_user!, except: :index
before_action :set_account
before_action :set_endorsed_accounts, only: :index
after_action :insert_pagination_headers, only: :index
def index
cache_if_unauthenticated!
render json: @endorsed_accounts, each_serializer: REST::AccountSerializer
end
def create
AccountPin.find_or_create_by!(account: current_account, target_account: @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
end
def destroy
pin = AccountPin.find_by(account: current_account, target_account: @account)
pin&.destroy!
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
end
private
def set_account
@account = Account.find(params[:account_id])
end
def set_endorsed_accounts
@endorsed_accounts = @account.unavailable? ? [] : paginated_endorsed_accounts
end
def paginated_endorsed_accounts
@account.endorsed_accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
def relationships_presenter
AccountRelationshipsPresenter.new([@account], current_user.account_id)
end
def next_path
api_v1_account_endorsements_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v1_account_endorsements_url pagination_params(since_id: pagination_since_id) unless @endorsed_accounts.empty?
end
def pagination_collection
@endorsed_accounts
end
def records_continue?
@endorsed_accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
end

View file

@ -12,7 +12,7 @@ class Api::V1::Accounts::FamiliarFollowersController < Api::BaseController
private
def set_accounts
@accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections')
@accounts = Account.without_suspended.where(id: account_ids).select(:id, :hide_collections)
end
def familiar_followers

View file

@ -17,6 +17,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
end
def set_featured_tags
@featured_tags = @account.suspended? ? [] : @account.featured_tags
@featured_tags = @account.unavailable? ? [] : @account.featured_tags
end
end

View file

@ -1,6 +1,10 @@
# frozen_string_literal: true
class Api::V1::Accounts::IdentityProofsController < Api::BaseController
include DeprecationConcern
deprecate_api '2022-03-30'
before_action :require_user!
before_action :set_account

View file

@ -1,30 +0,0 @@
# frozen_string_literal: true
class Api::V1::Accounts::PinsController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user!
before_action :set_account
def create
AccountPin.find_or_create_by!(account: current_account, target_account: @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
end
def destroy
pin = AccountPin.find_by(account: current_account, target_account: @account)
pin&.destroy!
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
end
private
def set_account
@account = Account.find(params[:account_id])
end
def relationships_presenter
AccountRelationshipsPresenter.new([@account], current_user.account_id)
end
end

View file

@ -10,6 +10,7 @@ class Api::V1::AccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
before_action :require_user!, except: [:index, :show, :create]
before_action :require_client_credentials!, only: [:create]
before_action :set_account, except: [:index, :create]
before_action :set_accounts, only: [:index]
before_action :check_account_approval, except: [:index, :create]
@ -106,8 +107,8 @@ class Api::V1::AccountsController < Api::BaseController
render json: { error: I18n.t('accounts.self_follow_error') }, status: 403 if current_user.account.id == @account.id
end
def relationships(**options)
AccountRelationshipsPresenter.new([@account], current_user.account_id, **options)
def relationships(**)
AccountRelationshipsPresenter.new([@account], current_user.account_id, **)
end
def account_ids
@ -119,7 +120,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def account_params
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code)
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code, :date_of_birth)
end
def invite

View file

@ -17,6 +17,17 @@ class Api::V1::AnnualReportsController < Api::BaseController
relationships: @relationships
end
def show
with_read_replica do
@presenter = AnnualReportsPresenter.new([@annual_report])
@relationships = StatusRelationshipsPresenter.new(@presenter.statuses, current_account.id)
end
render json: @presenter,
serializer: REST::AnnualReportsSerializer,
relationships: @relationships
end
def read
@annual_report.view!
render_empty

View file

@ -5,6 +5,8 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
before_action :require_user!
before_action :set_recently_used_tags, only: :index
RECENT_TAGS_LIMIT = 10
def index
render json: @recently_used_tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@recently_used_tags, current_user&.account_id)
end
@ -12,6 +14,6 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
private
def set_recently_used_tags
@recently_used_tags = Tag.suggestions_for_account(current_account).limit(10)
@recently_used_tags = Tag.suggestions_for_account(current_account).limit(RECENT_TAGS_LIMIT)
end
end

View file

@ -18,7 +18,7 @@ class Api::V1::FeaturedTagsController < Api::BaseController
end
def destroy
RemoveFeaturedTagWorker.perform_async(current_account.id, @featured_tag.id)
RemoveFeaturedTagService.new.call(current_account, @featured_tag)
render_empty
end

View file

@ -1,6 +1,10 @@
# frozen_string_literal: true
class Api::V1::FiltersController < Api::BaseController
include DeprecationConcern
deprecate_api '2022-11-14'
before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
before_action :require_user!
@ -28,7 +32,7 @@ class Api::V1::FiltersController < Api::BaseController
ApplicationRecord.transaction do
@filter.update!(keyword_params)
@filter.custom_filter.assign_attributes(filter_params)
raise Mastodon::ValidationError, I18n.t('filters.errors.deprecated_api_multiple_keywords') if @filter.custom_filter.changed? && @filter.custom_filter.keywords.count > 1
raise Mastodon::ValidationError, I18n.t('filters.errors.deprecated_api_multiple_keywords') if @filter.custom_filter.changed? && @filter.custom_filter.keywords.many?
@filter.custom_filter.save!
end

View file

@ -28,8 +28,8 @@ class Api::V1::FollowRequestsController < Api::BaseController
@account ||= Account.find(params[:id])
end
def relationships(**options)
AccountRelationshipsPresenter.new([account], current_user.account_id, **options)
def relationships(**)
AccountRelationshipsPresenter.new([account], current_user.account_id, **)
end
def load_accounts

View file

@ -18,6 +18,6 @@ class Api::V1::Instances::RulesController < Api::V1::Instances::BaseController
private
def set_rules
@rules = Rule.ordered
@rules = Rule.ordered.includes(:translations)
end
end

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseController
before_action :set_terms_of_service
def show
cache_even_if_authenticated!
render json: @terms_of_service, serializer: REST::TermsOfServiceSerializer
end
private
def set_terms_of_service
@terms_of_service = begin
if params[:date].present?
TermsOfService.published.find_by!(effective_date: params[:date])
else
TermsOfService.current
end
end
not_found if @terms_of_service.nil?
end
end

View file

@ -1,15 +1,9 @@
# frozen_string_literal: true
class Api::V1::InstancesController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
skip_around_action :set_locale
class Api::V1::InstancesController < Api::V2::InstancesController
include DeprecationConcern
vary_by ''
# Override `current_user` to avoid reading session cookies unless in limited federation mode
def current_user
super if limited_federation_mode?
end
deprecate_api '2022-11-14'
def show
cache_even_if_authenticated!

View file

@ -15,17 +15,12 @@ class Api::V1::Lists::AccountsController < Api::BaseController
end
def create
ApplicationRecord.transaction do
list_accounts.each do |account|
@list.accounts << account
end
end
AddAccountsToListService.new.call(@list, Account.find(account_ids))
render_empty
end
def destroy
ListAccount.where(list: @list, account_id: account_ids).destroy_all
RemoveAccountsFromListService.new.call(@list, Account.where(id: account_ids))
render_empty
end
@ -43,10 +38,6 @@ class Api::V1::Lists::AccountsController < Api::BaseController
end
end
def list_accounts
Account.find(account_ids)
end
def account_ids
Array(resource_params[:account_ids])
end

View file

@ -7,10 +7,6 @@ class Api::V1::ListsController < Api::BaseController
before_action :require_user!
before_action :set_list, except: [:index, :create]
rescue_from ArgumentError do |e|
render json: { error: e.to_s }, status: 422
end
def index
@lists = List.where(account: current_account).all
render json: @lists, each_serializer: REST::ListSerializer

View file

@ -3,8 +3,8 @@
class Api::V1::MediaController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:media' }
before_action :require_user!
before_action :set_media_attachment, except: [:create]
before_action :check_processing, except: [:create]
before_action :set_media_attachment, except: [:create, :destroy]
before_action :check_processing, except: [:create, :destroy]
def show
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
@ -25,6 +25,15 @@ class Api::V1::MediaController < Api::BaseController
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
end
def destroy
@media_attachment = current_account.media_attachments.find(params[:id])
return render json: in_usage_error, status: 422 unless @media_attachment.status_id.nil?
@media_attachment.destroy
render_empty
end
private
def status_code_for_media_attachment
@ -54,4 +63,8 @@ class Api::V1::MediaController < Api::BaseController
def processing_error
{ error: 'Error processing thumbnail for uploaded media' }
end
def in_usage_error
{ error: 'Media attachment is currently used by a status' }
end
end

View file

@ -15,7 +15,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
private
def set_poll
@poll = Poll.attached.find(params[:poll_id])
@poll = Poll.find(params[:poll_id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
not_found

View file

@ -15,7 +15,7 @@ class Api::V1::PollsController < Api::BaseController
private
def set_poll
@poll = Poll.attached.find(params[:id])
@poll = Poll.find(params[:id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
not_found

View file

@ -7,7 +7,7 @@ class Api::V1::Profile::AvatarsController < Api::BaseController
def destroy
@account = current_account
UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
end

View file

@ -7,7 +7,7 @@ class Api::V1::Profile::HeadersController < Api::BaseController
def destroy
@account = current_account
UpdateAccountService.new.call(@account, { header: nil }, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
end

View file

@ -21,6 +21,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh],
key_auth: subscription_params[:keys][:auth],
standard: subscription_params[:standard] || false,
data: data_params,
user_id: current_user.id,
access_token_id: doorkeeper_token.id
@ -55,12 +56,12 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
end
def subscription_params
params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]])
end
def data_params
return {} if params[:data].blank?
params.require(:data).permit(:policy, alerts: Notification::TYPES)
params.expect(data: [:policy, alerts: Notification::TYPES])
end
end

View file

@ -58,6 +58,8 @@ class Api::V1::StatusesController < Api::BaseController
statuses = [@status] + @context.ancestors + @context.descendants
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) if !current_account.nil? && @status.should_fetch_replies?
end
def create
@ -111,7 +113,7 @@ class Api::V1::StatusesController < Api::BaseController
@status.account.statuses_count = @status.account.statuses_count - 1
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
RemovalWorker.perform_async(@status.id, { 'redraft' => true })
RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) })
render json: json
end

View file

@ -2,6 +2,9 @@
class Api::V1::SuggestionsController < Api::BaseController
include Authorization
include DeprecationConcern
deprecate_api '2021-05-16', only: [:index]
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index

View file

@ -1,7 +1,8 @@
# frozen_string_literal: true
class Api::V1::TagsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, except: :show
before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, only: [:follow, :unfollow]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:feature, :unfeature]
before_action :require_user!, except: :show
before_action :set_or_create_tag
@ -23,6 +24,16 @@ class Api::V1::TagsController < Api::BaseController
render json: @tag, serializer: REST::TagSerializer
end
def feature
CreateFeaturedTagService.new.call(current_account, @tag)
render json: @tag, serializer: REST::TagSerializer
end
def unfeature
RemoveFeaturedTagService.new.call(current_account, @tag)
render json: @tag, serializer: REST::TagSerializer
end
private
def set_or_create_tag

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController
include AsyncRefreshesConcern
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show]
before_action :require_user!, only: [:show]
@ -12,6 +14,8 @@ class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController
@relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
add_async_refresh_header(account_home_feed.async_refresh, retry_seconds: 5)
render json: @statuses,
each_serializer: REST::StatusSerializer,
relationships: @relationships,

View file

@ -1,12 +1,16 @@
# frozen_string_literal: true
class Api::V1::Trends::TagsController < Api::BaseController
include DeprecationConcern
before_action :set_tags
after_action :insert_pagination_headers
DEFAULT_TAGS_LIMIT = 10
deprecate_api '2022-03-30', only: :index, if: -> { request.path == '/api/v1/trends' }
def index
cache_if_unauthenticated!
render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id)
@ -27,7 +31,9 @@ class Api::V1::Trends::TagsController < Api::BaseController
end
def tags_from_trends
Trends.tags.query.allowed
scope = Trends.tags.query.allowed.in_locale(content_locale)
scope = scope.filtered_for(current_account) if user_signed_in?
scope
end
def next_path

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Api::V1Alpha::AsyncRefreshesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
def show
async_refresh = AsyncRefresh.find(params[:id])
if async_refresh
render json: async_refresh
else
not_found
end
end
end

View file

@ -1,6 +1,16 @@
# frozen_string_literal: true
class Api::V2::InstancesController < Api::V1::InstancesController
class Api::V2::InstancesController < Api::BaseController
skip_before_action :require_authenticated_user!
skip_around_action :set_locale
vary_by ''
# Override `current_user` to avoid reading session cookies unless in limited federation mode
def current_user
super if limited_federation_mode?
end
def show
cache_even_if_authenticated!
render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance'

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true
class Api::V2::SearchController < Api::BaseController
include AsyncRefreshesConcern
include Authorization
RESULTS_LIMIT = 20
@ -13,6 +14,7 @@ class Api::V2::SearchController < Api::BaseController
before_action :remote_resolve_error, if: :remote_resolve_requested?
end
before_action :require_valid_pagination_options!
before_action :handle_fasp_requests
def index
@search = Search.new(search_results)
@ -37,6 +39,21 @@ class Api::V2::SearchController < Api::BaseController
render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401
end
def handle_fasp_requests
return unless Mastodon::Feature.fasp_enabled?
return if params[:q].blank?
# Do not schedule a new retrieval if the request is a follow-up
# to an earlier retrieval
return if request.headers['Mastodon-Async-Refresh-Id'].present?
refresh_key = "fasp:account_search:#{Digest::MD5.base64digest(params[:q])}"
return if AsyncRefresh.new(refresh_key).running?
add_async_refresh_header(AsyncRefresh.create(refresh_key))
@query_fasp = true
end
def remote_resolve_requested?
truthy_param?(:resolve)
end
@ -58,7 +75,8 @@ class Api::V2::SearchController < Api::BaseController
search_params.merge(
resolve: truthy_param?(:resolve),
exclude_unreviewed: truthy_param?(:exclude_unreviewed),
following: truthy_param?(:following)
following: truthy_param?(:following),
query_fasp: @query_fasp
)
end

View file

@ -2,11 +2,13 @@
class Api::V2::SuggestionsController < Api::BaseController
include Authorization
include AsyncRefreshesConcern
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
before_action :require_user!
before_action :set_suggestions
before_action :schedule_fasp_retrieval
def index
render json: @suggestions.get(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:offset].to_i), each_serializer: REST::SuggestionSerializer
@ -22,4 +24,18 @@ class Api::V2::SuggestionsController < Api::BaseController
def set_suggestions
@suggestions = AccountSuggestions.new(current_account)
end
def schedule_fasp_retrieval
return unless Mastodon::Feature.fasp_enabled?
# Do not schedule a new retrieval if the request is a follow-up
# to an earlier retrieval
return if request.headers['Mastodon-Async-Refresh-Id'].present?
refresh_key = "fasp:follow_recommendation:#{current_account.id}"
return if AsyncRefresh.new(refresh_key).running?
add_async_refresh_header(AsyncRefresh.create(refresh_key))
Fasp::FollowRecommendationWorker.perform_async(current_account.id)
end
end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
before_action :require_user!
before_action :require_user!, except: :destroy
before_action :set_push_subscription, only: :update
before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions?
after_action :update_session_with_subscription, only: :create
@ -17,6 +17,13 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
def destroy
push_subscription = ::Web::PushSubscription.find_by_token_for(:unsubscribe, params[:id])
push_subscription&.destroy
head 200
end
private
def active_session
@ -59,7 +66,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end
def subscription_params
@subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
@subscription_params ||= params.expect(subscription: [:standard, :endpoint, keys: [:auth, :p256dh]])
end
def web_push_subscription_params
@ -69,11 +76,12 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
endpoint: subscription_params[:endpoint],
key_auth: subscription_params[:keys][:auth],
key_p256dh: subscription_params[:keys][:p256dh],
standard: subscription_params[:standard] || false,
user_id: active_session.user_id,
}
end
def data_params
@data_params ||= params.require(:data).permit(:policy, alerts: Notification::TYPES)
@data_params ||= params.expect(data: [:policy, alerts: Notification::TYPES])
end
end

View file

@ -22,7 +22,6 @@ class ApplicationController < ActionController::Base
helper_method :use_seamless_external_login?
helper_method :sso_account_settings
helper_method :limited_federation_mode?
helper_method :body_class_string
helper_method :skip_csrf_meta_tags?
rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
@ -32,7 +31,7 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error)
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable
rescue_from Seahorse::Client::NetworkingError do |e|
@ -71,7 +70,27 @@ class ApplicationController < ActionController::Base
end
def require_functional!
redirect_to edit_user_registration_path unless current_user.functional?
return if current_user.functional?
respond_to do |format|
format.any do
if current_user.confirmed?
redirect_to edit_user_registration_path
else
redirect_to auth_setup_path
end
end
format.json do
if !current_user.confirmed?
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
elsif !current_user.approved?
render json: { error: 'Your login is currently pending approval' }, status: 403
elsif !current_user.functional?
render json: { error: 'Your login is currently disabled' }, status: 403
end
end
end
end
def skip_csrf_meta_tags?
@ -79,7 +98,7 @@ class ApplicationController < ActionController::Base
end
def after_sign_out_path_for(_resource_or_scope)
if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true'
if ENV['OMNIAUTH_ONLY'] == 'true' && Rails.configuration.x.omniauth.oidc_enabled?
'/auth/auth/openid_connect/logout'
else
new_user_session_path
@ -158,10 +177,6 @@ class ApplicationController < ActionController::Base
current_user.setting_theme
end
def body_class_string
@body_classes || ''
end
def respond_with_error(code)
respond_to do |format|
format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] }

View file

@ -12,7 +12,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :set_sessions, only: [:edit, :update]
before_action :set_strikes, only: [:edit, :update]
before_action :require_not_suspended!, only: [:update]
before_action :set_cache_headers, only: [:edit, :update]
before_action :set_rules, only: :new
before_action :require_rules_acceptance!, only: :new
before_action :set_registration_form_time, only: :new
@ -63,7 +62,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up) do |user_params|
user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password, :date_of_birth)
end
end
@ -127,7 +126,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def set_rules
@rules = Rule.ordered
@rules = Rule.ordered.includes(:translations)
end
def require_rules_acceptance!
@ -139,7 +138,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController
set_locale { render :rules }
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
def is_flashing_format? # rubocop:disable Naming/PredicatePrefix
if params[:action] == 'create'
false # Disable flash messages for sign-up
else
super
end
end
end

View file

@ -73,7 +73,7 @@ class Auth::SessionsController < Devise::SessionsController
end
def user_params
params.require(:user).permit(:email, :password, :otp_attempt, credential: {})
params.expect(user: [:email, :password, :otp_attempt, credential: {}])
end
def after_sign_in_path_for(resource)
@ -177,9 +177,7 @@ class Auth::SessionsController < Devise::SessionsController
)
# Only send a notification email every hour at most
return if redis.get("2fa_failure_notification:#{user.id}").present?
redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour)
return if redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour, get: true).present?
UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later!
end

View file

@ -18,7 +18,7 @@ class Auth::SetupController < ApplicationController
if @user.update(user_params)
@user.resend_confirmation_instructions unless @user.confirmed?
redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent')
redirect_to auth_setup_path, notice: t('auth.setup.new_confirmation_instructions_sent')
else
render :show
end
@ -35,6 +35,6 @@ class Auth::SetupController < ApplicationController
end
def user_params
params.require(:user).permit(:email)
params.expect(user: [:email])
end
end

View file

@ -9,19 +9,10 @@ class BackupsController < ApplicationController
before_action :authenticate_user!
before_action :set_backup
BACKUP_LINK_TIMEOUT = 1.hour.freeze
def download
case Paperclip::Attachment.default_options[:storage]
when :s3, :azure
redirect_to @backup.dump.expiring_url(10), allow_other_host: true
when :fog
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true
else
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
end
when :filesystem
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
end
redirect_to expiring_asset_url(@backup.dump, BACKUP_LINK_TIMEOUT), allow_other_host: true
end
private

View file

@ -24,6 +24,6 @@ module Admin::ExportControllerConcern
end
def import_params
params.require(:admin_import).permit(:data)
params.expect(admin_import: [:data])
end
end

View file

@ -20,7 +20,7 @@ module Api::ErrorHandling
render json: { error: 'Record not found' }, status: 404
end
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, Mastodon::UnexpectedResponseError) do
render json: { error: 'Remote data could not be fetched' }, status: 503
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module AsyncRefreshesConcern
private
def add_async_refresh_header(async_refresh, retry_seconds: 3)
return unless async_refresh.running?
response.headers['Mastodon-Async-Refresh'] = "id=\"#{async_refresh.id}\", retry=#{retry_seconds}"
end
end

View file

@ -10,7 +10,7 @@ module Auth::CaptchaConcern
end
def captcha_available?
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present?
end
def captcha_enabled?

View file

@ -28,7 +28,7 @@ module CacheConcern
def render_with_cache(**options)
raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given?
key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':')
key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields]&.join(',')].compact.join(':')
expires_in = options.delete(:expires_in) || 3.minutes
body = Rails.cache.read(key, raw: true)

View file

@ -58,6 +58,6 @@ module ChallengableConcern
end
def challenge_params
params.require(:form_challenge).permit(:current_password, :return_to)
params.expect(form_challenge: [:current_password, :return_to])
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
module DeprecationConcern
extend ActiveSupport::Concern
class_methods do
def deprecate_api(date, sunset: nil, **kwargs)
deprecation_timestamp = "@#{date.to_datetime.to_i}"
sunset = sunset&.to_date&.httpdate
before_action(**kwargs) do
response.headers['Deprecation'] = deprecation_timestamp
response.headers['Sunset'] = sunset if sunset
end
end
end
end

View file

@ -16,7 +16,7 @@ module Localized
def requested_locale
requested_locale_name = available_locale_or_nil(params[:lang])
requested_locale_name ||= available_locale_or_nil(current_user.locale) if respond_to?(:user_signed_in?) && user_signed_in?
requested_locale_name ||= http_accept_language if ENV['DEFAULT_LOCALE'].blank?
requested_locale_name ||= http_accept_language unless ENV['FORCE_DEFAULT_LOCALE'] == 'true'
requested_locale_name
end
@ -25,7 +25,7 @@ module Localized
end
def available_locale_or_nil(locale_name)
locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
locale_name.to_sym if locale_name.respond_to?(:to_sym) && I18n.available_locales.include?(locale_name.to_sym)
end
def content_locale

View file

@ -10,8 +10,6 @@ module SignatureVerification
EXPIRATION_WINDOW_LIMIT = 12.hours
CLOCK_SKEW_MARGIN = 1.hour
class SignatureVerificationError < StandardError; end
def require_account_signature!
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
end
@ -24,6 +22,18 @@ module SignatureVerification
request.headers['Signature'].present?
end
def signature_key_id
signed_request.key_id
rescue Mastodon::SignatureVerificationError
nil
end
private
def signed_request
@signed_request ||= SignedRequest.new(request) if signed_request?
end
def signature_verification_failure_reason
@signature_verification_failure_reason
end
@ -32,12 +42,6 @@ module SignatureVerification
@signature_verification_failure_code || 401
end
def signature_key_id
signature_params['keyId']
rescue SignatureVerificationError
nil
end
def signed_request_account
signed_request_actor.is_a?(Account) ? signed_request_actor : nil
end
@ -45,42 +49,24 @@ module SignatureVerification
def signed_request_actor
return @signed_request_actor if defined?(@signed_request_actor)
raise SignatureVerificationError, 'Request not signed' unless signed_request?
raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
raise SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm)
raise SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window?
raise Mastodon::SignatureVerificationError, 'Request not signed' unless signed_request?
verify_signature_strength!
verify_body_digest!
actor = actor_from_key_id
actor = actor_from_key_id(signature_params['keyId'])
raise Mastodon::SignatureVerificationError, "Public key not found for key #{signature_key_id}" if actor.nil?
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string(include_query_string: true)
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
# Compatibility quirk with older Mastodon versions
compare_signed_string = build_signed_string(include_query_string: false)
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
return (@signed_request_actor = actor) if signed_request.verified?(actor)
actor = stoplight_wrapper.run { actor_refresh_key!(actor) }
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
raise Mastodon::SignatureVerificationError, "Could not refresh public key #{signature_key_id}" if actor.nil?
compare_signed_string = build_signed_string(include_query_string: true)
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
return (@signed_request_actor = actor) if signed_request.verified?(actor)
# Compatibility quirk with older Mastodon versions
compare_signed_string = build_signed_string(include_query_string: false)
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
rescue SignatureVerificationError => e
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri}"
rescue Mastodon::SignatureVerificationError => e
fail_with! e.message
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
fail_with! "Failed to fetch remote data: #{e.message}"
rescue Mastodon::UnexpectedResponseError
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
@ -88,12 +74,6 @@ module SignatureVerification
fail_with! 'Fetching attempt skipped because of recent connection failure'
end
def request_body
@request_body ||= request.raw_post
end
private
def fail_with!(message, **options)
Rails.logger.debug { "Signature verification failed: #{message}" }
@ -101,123 +81,8 @@ module SignatureVerification
@signed_request_actor = nil
end
def signature_params
@signature_params ||= SignatureParser.parse(request.headers['Signature'])
rescue SignatureParser::ParsingError
raise SignatureVerificationError, 'Error parsing signature parameters'
end
def signature_algorithm
signature_params.fetch('algorithm', 'hs2019')
end
def signed_headers
signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split
end
def verify_signature_strength!
raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest')
raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
end
def verify_body_digest!
return unless signed_headers.include?('digest')
raise SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest')
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
sha256 = digests.assoc('sha-256')
raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil?
return if body_digest == sha256[1]
digest_size = begin
Base64.strict_decode64(sha256[1].strip).length
rescue ArgumentError
raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}"
end
raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32
raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}"
end
def verify_signature(actor, signature, compare_signed_string)
if actor.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string)
@signed_request_actor = actor
@signed_request_actor
end
rescue OpenSSL::PKey::RSAError
nil
end
def build_signed_string(include_query_string: true)
signed_headers.map do |signed_header|
case signed_header
when HttpSignatureDraft::REQUEST_TARGET
if include_query_string
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
else
# Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header.
# Therefore, temporarily support such incorrect signatures for compatibility.
# TODO: remove eventually some time after release of the fixed version
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
end
when '(created)'
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
"(created): #{signature_params['created']}"
when '(expires)'
raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
"(expires): #{signature_params['expires']}"
else
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
end
end.join("\n")
end
def matches_time_window?
created_time = nil
expires_time = nil
begin
if signature_algorithm == 'hs2019' && signature_params['created'].present?
created_time = Time.at(signature_params['created'].to_i).utc
elsif request.headers['Date'].present?
created_time = Time.httpdate(request.headers['Date']).utc
end
expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present?
rescue ArgumentError => e
raise SignatureVerificationError, "Invalid Date header: #{e.message}"
end
expires_time ||= created_time + 5.minutes unless created_time.nil?
expires_time = [expires_time, created_time + EXPIRATION_WINDOW_LIMIT].min unless created_time.nil?
return false if created_time.present? && created_time > Time.now.utc + CLOCK_SKEW_MARGIN
return false if expires_time.present? && Time.now.utc > expires_time + CLOCK_SKEW_MARGIN
true
end
def body_digest
@body_digest ||= Digest::SHA256.base64digest(request_body)
end
def to_header_name(name)
name.split('-').map(&:capitalize).join('-')
end
def missing_required_signature_parameters?
signature_params['keyId'].blank? || signature_params['signature'].blank?
end
def actor_from_key_id(key_id)
def actor_from_key_id
key_id = signed_request.key_id
domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id
if domain_not_allowed?(domain)
@ -233,9 +98,9 @@ module SignatureVerification
account
end
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
raise Mastodon::SignatureVerificationError, e.message
end
def stoplight_wrapper
@ -251,8 +116,8 @@ module SignatureVerification
ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false)
rescue Mastodon::PrivateNetworkAddressError => e
raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})"
rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e
raise SignatureVerificationError, e.message
raise Mastodon::SignatureVerificationError, e.message
end
end

Some files were not shown because too many files have changed in this diff Show more