Merge commit '4a22e72b9b1b8f14792efcc649b0db8bc27f0df2' into glitch-soc/merge-upstream

local
Claire 1 year ago
commit 2e02d03524
  1. 10
      .github/workflows/test-migrations-one-step.yml
  2. 10
      .github/workflows/test-migrations-two-step.yml
  3. 38
      .rubocop_todo.yml
  4. 2
      Dockerfile
  5. 73
      Gemfile
  6. 5
      Gemfile.lock
  7. 2
      app/controllers/api/v1/admin/domain_allows_controller.rb
  8. 6
      app/controllers/api/v1/statuses/reblogs_controller.rb
  9. 2
      app/controllers/auth/registrations_controller.rb
  10. 12
      app/controllers/oauth/authorized_applications_controller.rb
  11. 2
      app/javascript/mastodon/utils/__tests__/html-test.js
  12. 5
      app/javascript/types/image.d.ts
  13. 6
      app/lib/activitypub/activity/flag.rb
  14. 4
      app/lib/activitypub/tag_manager.rb
  15. 4
      app/lib/application_extension.rb
  16. 2
      app/lib/link_details_extractor.rb
  17. 9
      app/models/report.rb
  18. 4
      app/services/backup_service.rb
  19. 6
      app/validators/vote_validator.rb
  20. 4
      app/views/oauth/authorized_applications/index.html.haml
  21. 2
      app/workers/post_process_media_worker.rb
  22. 2
      config/initializers/ffmpeg.rb
  23. 2
      config/initializers/omniauth.rb
  24. 8
      config/initializers/paperclip.rb
  25. 2
      config/initializers/webauthn.rb
  26. 1
      config/locales/en.yml
  27. 1
      jest.config.js
  28. 2
      lib/mastodon/media_cli.rb
  29. 2
      lib/tasks/tests.rake
  30. 4
      package.json
  31. 2
      spec/controllers/admin/confirmations_controller_spec.rb
  32. 295
      spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb
  33. 8
      spec/controllers/api/v1/admin/domain_allows_controller_spec.rb
  34. 267
      spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb
  35. 294
      spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb
  36. 2
      spec/controllers/concerns/signature_verification_spec.rb
  37. 141
      spec/controllers/statuses_controller_spec.rb
  38. 2
      spec/fabricators/canonical_email_block_fabricator.rb
  39. 31
      spec/lib/activitypub/activity/flag_spec.rb
  40. 34
      spec/lib/mastodon/migration_warning_spec.rb
  41. 2
      spec/models/account_migration_spec.rb
  42. 2
      spec/models/account_spec.rb
  43. 11
      spec/models/report_spec.rb
  44. 2
      spec/models/user_settings/setting_spec.rb
  45. 2
      spec/services/activitypub/process_account_service_spec.rb
  46. 21
      spec/services/backup_service_spec.rb
  47. 12
      spec/services/report_service_spec.rb
  48. 20
      yarn.lock

@ -23,9 +23,17 @@ jobs:
needs: pre_job needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true' if: needs.pre_job.outputs.should_skip != 'true'
strategy:
fail-fast: false
matrix:
postgres:
- 14-alpine
- 15-alpine
services: services:
postgres: postgres:
image: postgres:14-alpine image: postgres:${{ matrix.postgres}}
env: env:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres

@ -23,9 +23,17 @@ jobs:
needs: pre_job needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true' if: needs.pre_job.outputs.should_skip != 'true'
strategy:
fail-fast: false
matrix:
postgres:
- 14-alpine
- 15-alpine
services: services:
postgres: postgres:
image: postgres:14-alpine image: postgres:${{ matrix.postgres}}
env: env:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres

@ -21,12 +21,6 @@ Layout/ArgumentAlignment:
- 'config/initializers/cors.rb' - 'config/initializers/cors.rb'
- 'config/initializers/session_store.rb' - 'config/initializers/session_store.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
Layout/ExtraSpacing:
Exclude:
- 'config/initializers/omniauth.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
# SupportedHashRocketStyles: key, separator, table # SupportedHashRocketStyles: key, separator, table
@ -39,12 +33,6 @@ Layout/HashAlignment:
- 'config/initializers/rack_attack.rb' - 'config/initializers/rack_attack.rb'
- 'config/routes.rb' - 'config/routes.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Width, AllowedPatterns.
Layout/IndentationWidth:
Exclude:
- 'config/initializers/ffmpeg.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment. # Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment.
Layout/LeadingCommentSpace: Layout/LeadingCommentSpace:
@ -52,14 +40,6 @@ Layout/LeadingCommentSpace:
- 'config/application.rb' - 'config/application.rb'
- 'config/initializers/omniauth.rb' - 'config/initializers/omniauth.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
# SupportedStyles: space, no_space
# SupportedStylesForEmptyBraces: space, no_space
Layout/SpaceBeforeBlockBraces:
Exclude:
- 'config/initializers/paperclip.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: require_no_space, require_space # SupportedStyles: require_no_space, require_space
@ -68,19 +48,6 @@ Layout/SpaceInLambdaLiteral:
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/content_security_policy.rb' - 'config/initializers/content_security_policy.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: space, no_space
Layout/SpaceInsideStringInterpolation:
Exclude:
- 'config/initializers/webauthn.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowInHeredoc.
Layout/TrailingWhitespace:
Exclude:
- 'config/initializers/paperclip.rb'
# Configuration parameters: AllowedMethods, AllowedPatterns. # Configuration parameters: AllowedMethods, AllowedPatterns.
Lint/AmbiguousBlockAssociation: Lint/AmbiguousBlockAssociation:
Exclude: Exclude:
@ -621,7 +588,6 @@ RSpec/NoExpectationExample:
RSpec/PendingWithoutReason: RSpec/PendingWithoutReason:
Exclude: Exclude:
- 'spec/controllers/statuses_controller_spec.rb'
- 'spec/models/account_spec.rb' - 'spec/models/account_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
@ -637,10 +603,6 @@ RSpec/RepeatedExample:
Exclude: Exclude:
- 'spec/policies/status_policy_spec.rb' - 'spec/policies/status_policy_spec.rb'
RSpec/RepeatedExampleGroupBody:
Exclude:
- 'spec/controllers/statuses_controller_spec.rb'
RSpec/StubbedMock: RSpec/StubbedMock:
Exclude: Exclude:
- 'spec/controllers/api/base_controller_spec.rb' - 'spec/controllers/api/base_controller_spec.rb'

@ -55,7 +55,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ENV DEBIAN_FRONTEND="noninteractive" \ ENV DEBIAN_FRONTEND="noninteractive" \
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin"
# Ignoreing these here since we don't want to pin any versions and the Debian image removes apt-get content after use # Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use
# hadolint ignore=DL3008,DL3009 # hadolint ignore=DL3008,DL3009
RUN apt-get update && \ RUN apt-get update && \
echo "Etc/UTC" > /etc/localtime && \ echo "Etc/UTC" > /etc/localtime && \

@ -99,54 +99,87 @@ gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.2' gem 'json-ld-preloaded', '~> 3.2'
gem 'rdf-normalize', '~> 0.5' gem 'rdf-normalize', '~> 0.5'
group :development, :test do gem 'private_address_check', '~> 0.5'
gem 'fabrication', '~> 2.30'
gem 'fuubar', '~> 2.5' group :test do
gem 'i18n-tasks', '~> 1.0', require: false # RSpec runner for rails
gem 'rspec-rails', '~> 6.0' gem 'rspec-rails', '~> 6.0'
# Used to split testing into chunks in CI
gem 'rspec_chunked', '~> 0.6' gem 'rspec_chunked', '~> 0.6'
gem 'rubocop-capybara', require: false # RSpec progress bar formatter
gem 'rubocop-performance', require: false gem 'fuubar', '~> 2.5'
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false
gem 'rubocop', require: false
end
group :production, :test do # Extra RSpec extenion methods and helpers for sidekiq
gem 'private_address_check', '~> 0.5' gem 'rspec-sidekiq', '~> 3.1'
end
group :test do # Browser integration testing
gem 'capybara', '~> 3.39' gem 'capybara', '~> 3.39'
gem 'climate_control'
# Used to mock environment variables
gem 'climate_control', '~> 0.2'
# Generating fake data for specs
gem 'faker', '~> 3.2' gem 'faker', '~> 3.2'
# Generate test objects for specs
gem 'fabrication', '~> 2.30'
# Add back helpers functions removed in Rails 5.1
gem 'rails-controller-testing', '~> 1.0'
# Validate schemas in specs
gem 'json-schema', '~> 4.0' gem 'json-schema', '~> 4.0'
# Test harness fo rack components
gem 'rack-test', '~> 2.1' gem 'rack-test', '~> 2.1'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec_junit_formatter', '~> 0.6' # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false
gem 'rspec-sidekiq', '~> 3.1'
gem 'simplecov', '~> 0.22', require: false gem 'simplecov', '~> 0.22', require: false
# Stub web requests for specs
gem 'webmock', '~> 3.18' gem 'webmock', '~> 3.18'
end end
group :development do group :development do
# Code linting CLI and plugins
gem 'rubocop', require: false
gem 'rubocop-capybara', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false
# Annotates modules with schema
gem 'annotate', '~> 3.2' gem 'annotate', '~> 3.2'
# Enhanced error message pages for development
gem 'better_errors', '~> 2.9' gem 'better_errors', '~> 2.9'
gem 'binding_of_caller', '~> 1.0' gem 'binding_of_caller', '~> 1.0'
# Preview mail in the browser
gem 'letter_opener', '~> 1.8' gem 'letter_opener', '~> 1.8'
gem 'letter_opener_web', '~> 2.0' gem 'letter_opener_web', '~> 2.0'
gem 'memory_profiler'
# Security analysis CLI tools
gem 'brakeman', '~> 5.4', require: false gem 'brakeman', '~> 5.4', require: false
gem 'bundler-audit', '~> 0.9', require: false gem 'bundler-audit', '~> 0.9', require: false
# Linter CLI for HAML files
gem 'haml_lint', require: false gem 'haml_lint', require: false
# Deployment automation
gem 'capistrano', '~> 3.17' gem 'capistrano', '~> 3.17'
gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rails', '~> 1.6'
gem 'capistrano-rbenv', '~> 2.2' gem 'capistrano-rbenv', '~> 2.2'
gem 'capistrano-yarn', '~> 2.0' gem 'capistrano-yarn', '~> 2.0'
gem 'stackprof' # Validate missing i18n keys
gem 'i18n-tasks', '~> 1.0', require: false
# Profiling tools
gem 'memory_profiler', require: false
gem 'stackprof', require: false
end end
group :production do group :production do

@ -601,8 +601,6 @@ GEM
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.12.0) rspec-support (3.12.0)
rspec_chunked (0.6) rspec_chunked (0.6)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.50.2) rubocop (1.50.2)
json (~> 2.3) json (~> 2.3)
parallel (~> 1.10) parallel (~> 1.10)
@ -787,7 +785,7 @@ DEPENDENCIES
capybara (~> 3.39) capybara (~> 3.39)
charlock_holmes (~> 0.7.7) charlock_holmes (~> 0.7.7)
chewy (~> 7.3) chewy (~> 7.3)
climate_control climate_control (~> 0.2)
cocoon (~> 1.2) cocoon (~> 1.2)
color_diff (~> 0.1) color_diff (~> 0.1)
concurrent-ruby concurrent-ruby
@ -866,7 +864,6 @@ DEPENDENCIES
rspec-rails (~> 6.0) rspec-rails (~> 6.0)
rspec-sidekiq (~> 3.1) rspec-sidekiq (~> 3.1)
rspec_chunked (~> 0.6) rspec_chunked (~> 0.6)
rspec_junit_formatter (~> 0.6)
rubocop rubocop
rubocop-capybara rubocop-capybara
rubocop-performance rubocop-performance

@ -29,7 +29,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
def create def create
authorize :domain_allow, :create? authorize :domain_allow, :create?
@domain_allow = DomainAllow.find_by(resource_params) @domain_allow = DomainAllow.find_by(domain: resource_params[:domain])
if @domain_allow.nil? if @domain_allow.nil?
@domain_allow = DomainAllow.create!(resource_params) @domain_allow = DomainAllow.create!(resource_params)

@ -2,6 +2,8 @@
class Api::V1::Statuses::ReblogsController < Api::BaseController class Api::V1::Statuses::ReblogsController < Api::BaseController
include Authorization include Authorization
include Redisable
include Lockable
before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user! before_action :require_user!
@ -10,7 +12,9 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
override_rate_limit_headers :create, family: :statuses override_rate_limit_headers :create, family: :statuses
def create def create
@status = ReblogService.new.call(current_account, @reblog, reblog_params) with_redis_lock("reblog:#{current_account.id}:#{@reblog.id}") do
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
end
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: REST::StatusSerializer
end end

@ -132,7 +132,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end end
def set_sessions def set_sessions
@sessions = current_user.session_activations @sessions = current_user.session_activations.order(updated_at: :desc)
end end
def set_strikes def set_strikes

@ -10,6 +10,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers before_action :set_cache_headers
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
skip_before_action :require_functional! skip_before_action :require_functional!
include Localized include Localized
@ -40,4 +42,14 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
def set_cache_headers def set_cache_headers
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end
def set_last_used_at_by_app
@last_used_at_by_app = Doorkeeper::AccessToken
.select('DISTINCT ON (application_id) application_id, last_used_at')
.where(resource_owner_id: current_resource_owner.id)
.where.not(last_used_at: nil)
.order(application_id: :desc, last_used_at: :desc)
.pluck(:application_id, :last_used_at)
.to_h
end
end end

@ -1,7 +1,7 @@
import * as html from '../html'; import * as html from '../html';
describe('html', () => { describe('html', () => {
describe('unsecapeHTML', () => { describe('unescapeHTML', () => {
it('returns unescaped HTML', () => { it('returns unescaped HTML', () => {
const output = html.unescapeHTML('<p>lorem</p><p>ipsum</p><br>&lt;br&gt;'); const output = html.unescapeHTML('<p>lorem</p><p>ipsum</p><br>&lt;br&gt;');
expect(output).toEqual('lorem\n\nipsum\n<br>'); expect(output).toEqual('lorem\n\nipsum\n<br>');

@ -14,11 +14,6 @@ declare module '*.jpg' {
export default path; export default path;
} }
declare module '*.jpg' {
const path: string;
export default path;
}
declare module '*.png' { declare module '*.png' {
const path: string; const path: string;
export default path; export default path;

@ -16,7 +16,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
@account, @account,
target_account, target_account,
status_ids: target_statuses.nil? ? [] : target_statuses.map(&:id), status_ids: target_statuses.nil? ? [] : target_statuses.map(&:id),
comment: @json['content'] || '', comment: report_comment,
uri: report_uri uri: report_uri
) )
end end
@ -35,4 +35,8 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
def report_uri def report_uri
@json['id'] unless @json['id'].nil? || non_matching_uri_hosts?(@account.uri, @json['id']) @json['id'] unless @json['id'].nil? || non_matching_uri_hosts?(@account.uri, @json['id'])
end end
def report_comment
(@json['content'] || '')[0...5000]
end
end end

@ -28,6 +28,8 @@ class ActivityPub::TagManager
return activity_account_status_url(target.account, target) if target.reblog? return activity_account_status_url(target.account, target) if target.reblog?
short_account_status_url(target.account, target) short_account_status_url(target.account, target)
when :flag
target.uri
end end
end end
@ -43,6 +45,8 @@ class ActivityPub::TagManager
account_status_url(target.account, target) account_status_url(target.account, target)
when :emoji when :emoji
emoji_url(target) emoji_url(target)
when :flag
target.uri
end end
end end

@ -9,10 +9,6 @@ module ApplicationExtension
validates :redirect_uri, length: { maximum: 2_000 } validates :redirect_uri, length: { maximum: 2_000 }
end end
def most_recently_used_access_token
@most_recently_used_access_token ||= access_tokens.where.not(last_used_at: nil).order(last_used_at: :desc).first
end
def confirmation_redirect_uri def confirmation_redirect_uri
redirect_uri.lines.first.strip redirect_uri.lines.first.strip
end end

@ -140,7 +140,7 @@ class LinkDetailsExtractor
end end
def html def html
player_url.present? ? content_tag(:iframe, nil, src: player_url, width: width, height: height, allowtransparency: 'true', scrolling: 'no', frameborder: '0') : nil player_url.present? ? content_tag(:iframe, nil, src: player_url, width: width, height: height, allowfullscreen: 'true', allowtransparency: 'true', scrolling: 'no', frameborder: '0') : nil
end end
def width def width

@ -40,7 +40,10 @@ class Report < ApplicationRecord
scope :resolved, -> { where.not(action_taken_at: nil) } scope :resolved, -> { where.not(action_taken_at: nil) }
scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) } scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) }
validates :comment, length: { maximum: 1_000 } # A report is considered local if the reporter is local
delegate :local?, to: :account
validates :comment, length: { maximum: 1_000 }, if: :local?
validates :rule_ids, absence: true, unless: :violation? validates :rule_ids, absence: true, unless: :violation?
validate :validate_rule_ids validate :validate_rule_ids
@ -51,10 +54,6 @@ class Report < ApplicationRecord
violation: 2_000, violation: 2_000,
} }
def local?
false # Force uri_for to use uri attribute
end
before_validation :set_uri, only: :create before_validation :set_uri, only: :create
after_create_commit :trigger_webhooks after_create_commit :trigger_webhooks

@ -101,8 +101,8 @@ class BackupService < BaseService
actor[:likes] = 'likes.json' actor[:likes] = 'likes.json'
actor[:bookmarks] = 'bookmarks.json' actor[:bookmarks] = 'bookmarks.json'
download_to_zip(tar, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists? download_to_zip(zipfile, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists?
download_to_zip(tar, account.header, "header#{File.extname(account.header.path)}") if account.header.exists? download_to_zip(zipfile, account.header, "header#{File.extname(account.header.path)}") if account.header.exists?
json = Oj.dump(actor) json = Oj.dump(actor)

@ -3,8 +3,8 @@
class VoteValidator < ActiveModel::Validator class VoteValidator < ActiveModel::Validator
def validate(vote) def validate(vote)
vote.errors.add(:base, I18n.t('polls.errors.expired')) if vote.poll_expired? vote.errors.add(:base, I18n.t('polls.errors.expired')) if vote.poll_expired?
vote.errors.add(:base, I18n.t('polls.errors.invalid_choice')) if invalid_choice?(vote) vote.errors.add(:base, I18n.t('polls.errors.invalid_choice')) if invalid_choice?(vote)
vote.errors.add(:base, I18n.t('polls.errors.self_vote')) if self_vote?(vote)
vote.errors.add(:base, I18n.t('polls.errors.already_voted')) if additional_voting_not_allowed?(vote) vote.errors.add(:base, I18n.t('polls.errors.already_voted')) if additional_voting_not_allowed?(vote)
end end
@ -27,6 +27,10 @@ class VoteValidator < ActiveModel::Validator
vote.choice.negative? || vote.choice >= vote.poll.options.size vote.choice.negative? || vote.choice >= vote.poll.options.size
end end
def self_vote?(vote)
vote.account_id == vote.poll.account_id
end
def already_voted_for_same_choice_on_multiple_poll?(vote) def already_voted_for_same_choice_on_multiple_poll?(vote)
if vote.persisted? if vote.persisted?
account_votes_on_same_poll(vote).where(choice: vote.choice).where.not(poll_votes: { id: vote }).exists? account_votes_on_same_poll(vote).where(choice: vote.choice).where.not(poll_votes: { id: vote }).exists?

@ -18,8 +18,8 @@
.announcements-list__item__action-bar .announcements-list__item__action-bar
.announcements-list__item__meta .announcements-list__item__meta
- if application.most_recently_used_access_token - if @last_used_at_by_app[application.id]
= t('doorkeeper.authorized_applications.index.last_used_at', date: l(application.most_recently_used_access_token.last_used_at.to_date)) = t('doorkeeper.authorized_applications.index.last_used_at', date: l(@last_used_at_by_app[application.id].to_date))
- else - else
= t('doorkeeper.authorized_applications.index.never_used') = t('doorkeeper.authorized_applications.index.never_used')

@ -24,7 +24,7 @@ class PostProcessMediaWorker
media_attachment.processing = :in_progress media_attachment.processing = :in_progress
media_attachment.save media_attachment.save
# Because paperclip-av-transcover overwrites this attribute # Because paperclip-av-transcoder overwrites this attribute
# we will save it here and restore it after reprocess is done # we will save it here and restore it after reprocess is done
previous_meta = media_attachment.file_meta previous_meta = media_attachment.file_meta

@ -1,3 +1,3 @@
if ENV['FFMPEG_BINARY'].present? if ENV['FFMPEG_BINARY'].present?
FFMPEG.ffmpeg_binary = ENV['FFMPEG_BINARY'] FFMPEG.ffmpeg_binary = ENV['FFMPEG_BINARY']
end end

@ -73,7 +73,7 @@ Devise.setup do |config|
oidc_options[:display_name] = ENV['OIDC_DISPLAY_NAME'] #OPTIONAL oidc_options[:display_name] = ENV['OIDC_DISPLAY_NAME'] #OPTIONAL
oidc_options[:issuer] = ENV['OIDC_ISSUER'] if ENV['OIDC_ISSUER'] #NEED oidc_options[:issuer] = ENV['OIDC_ISSUER'] if ENV['OIDC_ISSUER'] #NEED
oidc_options[:discovery] = ENV['OIDC_DISCOVERY'] == 'true' if ENV['OIDC_DISCOVERY'] #OPTIONAL (default: false) oidc_options[:discovery] = ENV['OIDC_DISCOVERY'] == 'true' if ENV['OIDC_DISCOVERY'] #OPTIONAL (default: false)
oidc_options[:client_auth_method] = ENV['OIDC_CLIENT_AUTH_METHOD'] if ENV['OIDC_CLIENT_AUTH_METHOD'] #OPTIONAL (default: basic) oidc_options[:client_auth_method] = ENV['OIDC_CLIENT_AUTH_METHOD'] if ENV['OIDC_CLIENT_AUTH_METHOD'] #OPTIONAL (default: basic)
scope_string = ENV['OIDC_SCOPE'] if ENV['OIDC_SCOPE'] #NEED scope_string = ENV['OIDC_SCOPE'] if ENV['OIDC_SCOPE'] #NEED
scopes = scope_string.split(',') scopes = scope_string.split(',')
oidc_options[:scope] = scopes.map { |x| x.to_sym } oidc_options[:scope] = scopes.map { |x| x.to_sym }

@ -61,13 +61,13 @@ if ENV['S3_ENABLED'] == 'true'
s3_options: { s3_options: {
signature_version: ENV.fetch('S3_SIGNATURE_VERSION') { 'v4' }, signature_version: ENV.fetch('S3_SIGNATURE_VERSION') { 'v4' },
http_open_timeout: ENV.fetch('S3_OPEN_TIMEOUT'){ '5' }.to_i, http_open_timeout: ENV.fetch('S3_OPEN_TIMEOUT') { '5' }.to_i,
http_read_timeout: ENV.fetch('S3_READ_TIMEOUT'){ '5' }.to_i, http_read_timeout: ENV.fetch('S3_READ_TIMEOUT') { '5' }.to_i,
http_idle_timeout: 5, http_idle_timeout: 5,
retry_limit: 0, retry_limit: 0,
} }
) )
Paperclip::Attachment.default_options[:s3_permissions] = ->(*) { nil } if ENV['S3_PERMISSION'] == '' Paperclip::Attachment.default_options[:s3_permissions] = ->(*) { nil } if ENV['S3_PERMISSION'] == ''
if ENV.has_key?('S3_ENDPOINT') if ENV.has_key?('S3_ENDPOINT')
@ -124,7 +124,7 @@ elsif ENV['SWIFT_ENABLED'] == 'true'
openstack_cache_ttl: ENV.fetch('SWIFT_CACHE_TTL') { 60 }, openstack_cache_ttl: ENV.fetch('SWIFT_CACHE_TTL') { 60 },
openstack_temp_url_key: ENV['SWIFT_TEMP_URL_KEY'], openstack_temp_url_key: ENV['SWIFT_TEMP_URL_KEY'],
}, },
fog_file: { 'Cache-Control' => 'public, max-age=315576000, immutable' }, fog_file: { 'Cache-Control' => 'public, max-age=315576000, immutable' },
fog_directory: ENV['SWIFT_CONTAINER'], fog_directory: ENV['SWIFT_CONTAINER'],

@ -1,7 +1,7 @@
WebAuthn.configure do |config| WebAuthn.configure do |config|
# This value needs to match `window.location.origin` evaluated by # This value needs to match `window.location.origin` evaluated by
# the User Agent during registration and authentication ceremonies. # the User Agent during registration and authentication ceremonies.
config.origin = "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}" config.origin = "#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}"
# Relying Party name for display purposes # Relying Party name for display purposes
config.rp_name = "Mastodon" config.rp_name = "Mastodon"

@ -1446,6 +1446,7 @@ en:
expired: The poll has already ended expired: The poll has already ended
invalid_choice: The chosen vote option does not exist invalid_choice: The chosen vote option does not exist
over_character_limit: cannot be longer than %{max} characters each over_character_limit: cannot be longer than %{max} characters each
self_vote: You cannot vote in your own polls
too_few_options: must have more than one item too_few_options: must have more than one item
too_many_options: can't contain more than %{max} items too_many_options: can't contain more than %{max} items
preferences: preferences:

@ -10,7 +10,6 @@ const config = {
'<rootDir>/tmp/', '<rootDir>/tmp/',
'<rootDir>/app/javascript/themes/', '<rootDir>/app/javascript/themes/',
], ],
setupFiles: ['raf/polyfill'],
setupFilesAfterEnv: ['<rootDir>/app/javascript/mastodon/test_setup.js'], setupFilesAfterEnv: ['<rootDir>/app/javascript/mastodon/test_setup.js'],
collectCoverageFrom: [ collectCoverageFrom: [
'app/javascript/mastodon/**/*.{js,jsx,ts,tsx}', 'app/javascript/mastodon/**/*.{js,jsx,ts,tsx}',

@ -24,7 +24,7 @@ module Mastodon
desc 'remove', 'Remove remote media files, headers or avatars' desc 'remove', 'Remove remote media files, headers or avatars'
long_desc <<-DESC long_desc <<-DESC
Removes locally cached copies of media attachments (and optionally profile Removes locally cached copies of media attachments (and optionally profile
headers and avatars) from other servers. By default, only media attachements headers and avatars) from other servers. By default, only media attachments
are removed. are removed.
The --days option specifies how old media attachments have to be before The --days option specifies how old media attachments have to be before
they are removed. In case of avatars and headers, it specifies how old they are removed. In case of avatars and headers, it specifies how old

@ -25,7 +25,7 @@ namespace :tests do
end end
if Account.where(domain: Rails.configuration.x.local_domain).exists? if Account.where(domain: Rails.configuration.x.local_domain).exists?
puts 'Faux remote accounts not properly claned up' puts 'Faux remote accounts not properly cleaned up'
exit(1) exit(1)
end end

@ -76,7 +76,7 @@
"intl-messageformat": "^2.2.0", "intl-messageformat": "^2.2.0",
"intl-relativeformat": "^6.4.3", "intl-relativeformat": "^6.4.3",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsdom": "^21.1.2", "jsdom": "^22.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mark-loader": "^0.1.6", "mark-loader": "^0.1.6",
"marky": "^1.2.5", "marky": "^1.2.5",
@ -158,7 +158,6 @@
"@types/pg": "^8.6.6", "@types/pg": "^8.6.6",
"@types/prop-types": "^15.7.5", "@types/prop-types": "^15.7.5",
"@types/punycode": "^2.1.0", "@types/punycode": "^2.1.0",
"@types/raf": "^3.4.0",
"@types/react": "^16.14.38", "@types/react": "^16.14.38",
"@types/react-dom": "^16.9.18", "@types/react-dom": "^16.9.18",
"@types/react-helmet": "^6.1.6", "@types/react-helmet": "^6.1.6",
@ -198,7 +197,6 @@
"jest-environment-jsdom": "^29.5.0", "jest-environment-jsdom": "^29.5.0",
"lint-staged": "^13.2.2", "lint-staged": "^13.2.2",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"raf": "^3.4.1",
"react-intl-translations-manager": "^5.0.3", "react-intl-translations-manager": "^5.0.3",
"react-test-renderer": "^16.14.0", "react-test-renderer": "^16.14.0",
"stylelint": "^15.6.1", "stylelint": "^15.6.1",

@ -32,7 +32,7 @@ RSpec.describe Admin::ConfirmationsController do
end end
end end
describe 'POST #resernd' do describe 'POST #resend' do
subject { post :resend, params: { account_id: user.account.id } } subject { post :resend, params: { account_id: user.account.id } }
let!(:user) { Fabricate(:user, confirmed_at: confirmed_at) } let!(:user) { Fabricate(:user, confirmed_at: confirmed_at) }

@ -5,23 +5,182 @@ require 'rails_helper'
describe Api::V1::Admin::CanonicalEmailBlocksController do describe Api::V1::Admin::CanonicalEmailBlocksController do
render_views render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } let(:role) { UserRole.find_by(name: 'Admin') }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } let(:user) { Fabricate(:user, role: role) }
let(:account) { Fabricate(:account) } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'admin:read:canonical_email_blocks admin:write:canonical_email_blocks' }
before do before do
allow(controller).to receive(:doorkeeper_token) { token } allow(controller).to receive(:doorkeeper_token) { token }
end end
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
describe 'GET #index' do describe 'GET #index' do
context 'with wrong scope' do
before do
get :index
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
get :index
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 } get :index
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
context 'when there is no canonical email block' do
it 'returns an empty list' do
get :index
body = body_as_json
expect(body).to be_empty
end
end
context 'when there are canonical email blocks' do
let!(:canonical_email_blocks) { Fabricate.times(5, :canonical_email_block) }
let(:expected_email_hashes) { canonical_email_blocks.pluck(:canonical_email_hash) }
it 'returns the correct canonical email hashes' do
get :index
json = body_as_json
expect(json.pluck(:canonical_email_hash)).to match_array(expected_email_hashes)
end
context 'with limit param' do
let(:params) { { limit: 2 } }
it 'returns only the requested number of canonical email blocks' do
get :index, params: params
json = body_as_json
expect(json.size).to eq(params[:limit])
end
end
context 'with since_id param' do
let(:params) { { since_id: canonical_email_blocks[1].id } }
it 'returns only the canonical email blocks after since_id' do
get :index, params: params
canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
json = body_as_json
expect(json.pluck(:id)).to match_array(canonical_email_blocks_ids[2..])
end
end
context 'with max_id param' do
let(:params) { { max_id: canonical_email_blocks[3].id } }
it 'returns only the canonical email blocks before max_id' do
get :index, params: params
canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
json = body_as_json
expect(json.pluck(:id)).to match_array(canonical_email_blocks_ids[..2])
end
end
end
end
describe 'GET #show' do
let!(:canonical_email_block) { Fabricate(:canonical_email_block) }
let(:params) { { id: canonical_email_block.id } }
context 'with wrong scope' do
before do
get :show, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
get :show, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
context 'when canonical email block exists' do
it 'returns http success' do
get :show, params: params
expect(response).to have_http_status(200)
end
it 'returns canonical email block data correctly' do
get :show, params: params
json = body_as_json
expect(json[:id]).to eq(canonical_email_block.id.to_s)
expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
end
end
context 'when canonical block does not exist' do
it 'returns http not found' do
get :show, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end end
describe 'POST #test' do describe 'POST #test' do
context 'with wrong scope' do
before do
post :test
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
post :test, params: { email: 'whatever@email.com' }
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
context 'when required email is not provided' do context 'when required email is not provided' do
it 'returns http bad request' do it 'returns http bad request' do
post :test post :test
@ -68,4 +227,132 @@ describe Api::V1::Admin::CanonicalEmailBlocksController do
end end
end end
end end
describe 'POST #create' do
let(:params) { { email: 'example@email.com' } }
let(:canonical_email_block) { CanonicalEmailBlock.new(email: params[:email]) }
context 'with wrong scope' do
before do
post :create, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
post :create, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
post :create, params: params
expect(response).to have_http_status(200)
end
it 'returns canonical_email_hash correctly' do
post :create, params: params
json = body_as_json
expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
end
context 'when required email param is not provided' do
it 'returns http unprocessable entity' do
post :create
expect(response).to have_http_status(422)
end
end
context 'when canonical_email_hash param is provided instead of email' do
let(:params) { { canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } }
it 'returns http success' do
post :create, params: params
expect(response).to have_http_status(200)
end
it 'returns correct canonical_email_hash' do
post :create, params: params
json = body_as_json
expect(json[:canonical_email_hash]).to eq(params[:canonical_email_hash])
end
end
context 'when both email and canonical_email_hash params are provided' do
let(:params) { { email: 'example@email.com', canonical_email_hash: 'dd501ce4e6b08698f19df96f2f15737e48a75660b1fa79b6ff58ea25ee4851a4' } }
it 'returns http success' do
post :create, params: params
expect(response).to have_http_status(200)
end
it 'ignores canonical_email_hash param' do
post :create, params: params
json = body_as_json
expect(json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
end
end
context 'when canonical email was already blocked' do
before do
canonical_email_block.save
end
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
end
describe 'DELETE #destroy' do
let!(:canonical_email_block) { Fabricate(:canonical_email_block) }
let(:params) { { id: canonical_email_block.id } }
context 'with wrong scope' do
before do
delete :destroy, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
delete :destroy, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
delete :destroy, params: params
expect(response).to have_http_status(200)
end
context 'when canonical email block is not found' do
it 'returns http not found' do
delete :destroy, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end
end end

@ -128,5 +128,13 @@ RSpec.describe Api::V1::Admin::DomainAllowsController do
expect(response).to have_http_status(422) expect(response).to have_http_status(422)
end end
end end
context 'when domain name is not specified' do
it 'returns http unprocessable entity' do
post :create
expect(response).to have_http_status(422)
end
end
end end
end end

@ -5,19 +5,280 @@ require 'rails_helper'
describe Api::V1::Admin::EmailDomainBlocksController do describe Api::V1::Admin::EmailDomainBlocksController do
render_views render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } let(:role) { UserRole.find_by(name: 'Admin') }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } let(:user) { Fabricate(:user, role: role) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:account) { Fabricate(:account) } let(:account) { Fabricate(:account) }
let(:scopes) { 'admin:read:email_domain_blocks admin:write:email_domain_blocks' }
before do before do
allow(controller).to receive(:doorkeeper_token) { token } allow(controller).to receive(:doorkeeper_token) { token }
end end
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
describe 'GET #index' do describe 'GET #index' do
context 'with wrong scope' do
before do
get :index
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
get :index
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 } get :index
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
context 'when there is no email domain block' do
it 'returns an empty list' do
get :index
json = body_as_json
expect(json).to be_empty
end
end
context 'when there are email domain blocks' do
let!(:email_domain_blocks) { Fabricate.times(5, :email_domain_block) }
let(:blocked_email_domains) { email_domain_blocks.pluck(:domain) }
it 'return the correct blocked email domains' do
get :index
json = body_as_json
expect(json.pluck(:domain)).to match_array(blocked_email_domains)
end
context 'with limit param' do
let(:params) { { limit: 2 } }
it 'returns only the requested number of email domain blocks' do
get :index, params: params
json = body_as_json
expect(json.size).to eq(params[:limit])
end
end
context 'with since_id param' do
let(:params) { { since_id: email_domain_blocks[1].id } }
it 'returns only the email domain blocks after since_id' do
get :index, params: params
email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s)
json = body_as_json
expect(json.pluck(:id)).to match_array(email_domain_blocks_ids[2..])
end
end
context 'with max_id param' do
let(:params) { { max_id: email_domain_blocks[3].id } }
it 'returns only the email domain blocks before max_id' do
get :index, params: params
email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s)
json = body_as_json
expect(json.pluck(:id)).to match_array(email_domain_blocks_ids[..2])
end
end
end
end
describe 'GET #show' do
let!(:email_domain_block) { Fabricate(:email_domain_block) }
let(:params) { { id: email_domain_block.id } }
context 'with wrong scope' do
before do
get :show, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
get :show, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
context 'when email domain block exists' do
it 'returns http success' do
get :show, params: params
expect(response).to have_http_status(200)
end
it 'returns the correct blocked domain' do
get :show, params: params
json = body_as_json
expect(json[:domain]).to eq(email_domain_block.domain)
end
end
context 'when email domain block does not exist' do
it 'returns http not found' do
get :show, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end
describe 'POST #create' do
let(:params) { { domain: 'example.com' } }
context 'with wrong scope' do
before do
post :create, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
post :create, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
post :create, params: params
expect(response).to have_http_status(200)
end
it 'returns the correct blocked email domain' do
post :create, params: params
json = body_as_json
expect(json[:domain]).to eq(params[:domain])
end
context 'when domain param is not provided' do
let(:params) { { domain: '' } }
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
context 'when provided domain name has an invalid character' do
let(:params) { { domain: 'do\uD800.com' } }
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
context 'when provided domain is already blocked' do
before do
EmailDomainBlock.create(params)
end
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
end
describe 'DELETE #destroy' do
let!(:email_domain_block) { Fabricate(:email_domain_block) }
let(:params) { { id: email_domain_block.id } }
context 'with wrong scope' do
before do
delete :destroy, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read:statuses'
end
context 'with wrong role' do
before do
delete :destroy, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
delete :destroy, params: params
expect(response).to have_http_status(200)
end
it 'returns an empty body' do
delete :destroy, params: params
json = body_as_json
expect(json).to be_empty
end
it 'deletes email domain block' do
delete :destroy, params: params
email_domain_block = EmailDomainBlock.find_by(id: params[:id])
expect(email_domain_block).to be_nil
end
context 'when email domain block does not exist' do
it 'returns http not found' do
delete :destroy, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end end
end end

@ -5,19 +5,305 @@ require 'rails_helper'
describe Api::V1::Admin::IpBlocksController do describe Api::V1::Admin::IpBlocksController do
render_views render_views
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } let(:role) { UserRole.find_by(name: 'Admin') }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'admin:read') } let(:user) { Fabricate(:user, role: role) }
let(:account) { Fabricate(:account) } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'admin:read:ip_blocks admin:write:ip_blocks' }
before do before do
allow(controller).to receive(:doorkeeper_token) { token } allow(controller).to receive(:doorkeeper_token) { token }
end end
shared_examples 'forbidden for wrong scope' do |wrong_scope|
let(:scopes) { wrong_scope }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
end
end
describe 'GET #index' do describe 'GET #index' do
context 'with wrong scope' do
before do
get :index
end
it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks'
end
context 'with wrong role' do
before do
get :index
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
get :index
expect(response).to have_http_status(200)
end
context 'when there is no ip block' do
it 'returns an empty body' do
get :index
json = body_as_json
expect(json).to be_empty
end
end
context 'when there are ip blocks' do
let!(:ip_blocks) do
[
IpBlock.create(ip: '192.0.2.0/24', severity: :no_access),
IpBlock.create(ip: '172.16.0.1', severity: :sign_up_requires_approval, comment: 'Spam'),
IpBlock.create(ip: '2001:0db8::/32', severity: :sign_up_block, expires_in: 10.days),
]
end
let(:expected_response) do
ip_blocks.map do |ip_block|
{
id: ip_block.id.to_s,
ip: ip_block.ip,
severity: ip_block.severity.to_s,
comment: ip_block.comment,
created_at: ip_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
expires_at: ip_block.expires_at&.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
}
end
end
it 'returns the correct blocked ips' do
get :index
json = body_as_json
expect(json).to match_array(expected_response)
end
context 'with limit param' do
let(:params) { { limit: 2 } }
it 'returns only the requested number of ip blocks' do
get :index, params: params
json = body_as_json
expect(json.size).to eq(params[:limit])
end
end
end
end
describe 'GET #show' do
let!(:ip_block) { IpBlock.create(ip: '192.0.2.0/24', severity: :no_access) }
let(:params) { { id: ip_block.id } }
context 'with wrong scope' do
before do
get :show, params: params
end
it_behaves_like 'forbidden for wrong scope', 'admin:write:ip_blocks'
end
context 'with wrong role' do
before do
get :show, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do
get :show, params: params
expect(response).to have_http_status(200)
end
it 'returns the correct ip block' do
get :show, params: params
json = body_as_json
expect(json[:ip]).to eq("#{ip_block.ip}/#{ip_block.ip.prefix}")
expect(json[:severity]).to eq(ip_block.severity.to_s)
end
context 'when ip block does not exist' do
it 'returns http not found' do
get :show, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end
describe 'POST #create' do
let(:params) { { ip: '151.0.32.55', severity: 'no_access', comment: 'Spam' } }
context 'with wrong scope' do
before do
post :create, params: params
end
it_behaves_like 'forbidden for wrong scope', 'admin:read:ip_blocks'
end
context 'with wrong role' do
before do
post :create, params: params
end
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
end
it 'returns http success' do it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 } post :create, params: params
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'returns the correct ip block' do
post :create, params: params
json = body_as_json
expect(json[:ip]).to eq("#{params[:ip]}/32")
expect(json[:severity]).to eq(params[:severity])
expect(json[:comment]).to eq(params[:comment])
end
context 'when ip is not provided' do
let(:params) { { ip: '', severity: 'no_access' } }
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
context 'when severity is not provided' do
let(:params) { { ip: '173.65.23.1', severity: '' } }
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
context 'when provided ip is already blocked' do
before do
IpBlock.create(params)
end
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
context 'when provided ip address is invalid' do
let(:params) { { ip: '520.13.54.120', severity: 'no_access' } }
it 'returns http unprocessable entity' do
post :create, params: params
expect(response).to have_http_status(422)
end
end
end
describe 'PUT #update' do
context 'when ip block exists' do
let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access', comment: 'Spam', expires_in: 48.hours) }
let(:params) { { id: ip_block.id, severity: 'sign_up_requires_approval', comment: 'Decreasing severity' } }
it 'returns http success' do
put :update, params: params
expect(response).to have_http_status(200)
end
it 'returns the correct ip block' do
put :update, params: params
json = body_as_json
expect(json).to match(hash_including({
ip: "#{ip_block.ip}/#{ip_block.ip.prefix}",
severity: 'sign_up_requires_approval',
comment: 'Decreasing severity',
}))
end
it 'updates the severity correctly' do
expect { put :update, params: params }.to change { ip_block.reload.severity }.from('no_access').to('sign_up_requires_approval')
end
it 'updates the comment correctly' do
expect { put :update, params: params }.to change { ip_block.reload.comment }.from('Spam').to('Decreasing severity')
end
end
context 'when ip block does not exist' do
it 'returns http not found' do
put :update, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end
describe 'DELETE #destroy' do
context 'when ip block exists' do
let!(:ip_block) { IpBlock.create(ip: '185.200.13.3', severity: 'no_access') }
let(:params) { { id: ip_block.id } }
it 'returns http success' do
delete :destroy, params: params
expect(response).to have_http_status(200)
end
it 'returns an empty body' do
delete :destroy, params: params
json = body_as_json
expect(json).to be_empty
end
it 'deletes the ip block' do
delete :destroy, params: params
expect(IpBlock.find_by(id: ip_block.id)).to be_nil
end
end
context 'when ip block does not exist' do
it 'returns http not found' do
delete :destroy, params: { id: 0 }
expect(response).to have_http_status(404)
end
end
end end
end end

@ -129,7 +129,7 @@ describe ApplicationController do
end end
end end
context 'with request with unparseable Date header' do context 'with request with unparsable Date header' do
before do before do
get :success get :success

@ -719,65 +719,180 @@ describe StatusesController do
end end
context 'when status is public' do context 'when status is public' do
pending before do
status.update(visibility: :public)
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
end end
context 'when status is private' do context 'when status is private' do
pending before do
status.update(visibility: :private)
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http not_found' do
expect(response).to have_http_status(404)
end
end end
context 'when status is direct' do context 'when status is direct' do
pending before do
status.update(visibility: :direct)
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http not_found' do
expect(response).to have_http_status(404)
end
end end
context 'when signed-in' do context 'when signed-in' do
let(:user) { Fabricate(:user) }
before do
sign_in(user)
end
context 'when status is public' do context 'when status is public' do
pending before do
status.update(visibility: :public)
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
end end
context 'when status is private' do context 'when status is private' do
before do
status.update(visibility: :private)
end
context 'when user is authorized to see it' do context 'when user is authorized to see it' do
pending before do
user.account.follow!(account)
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
end end
context 'when user is not authorized to see it' do context 'when user is not authorized to see it' do
pending before do
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http not_found' do
expect(response).to have_http_status(404)
end
end end
end end
context 'when status is direct' do context 'when status is direct' do
before do
status.update(visibility: :direct)
end
context 'when user is authorized to see it' do context 'when user is authorized to see it' do
pending before do
Fabricate(:mention, account: user.account, status: status)
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
end end
context 'when user is not authorized to see it' do context 'when user is not authorized to see it' do
pending before do
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http not_found' do
expect(response).to have_http_status(404)
end
end end
end end
end end
context 'with signature' do context 'with signature' do
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
before do
allow(controller).to receive(:signed_request_actor).and_return(remote_account)
end
context 'when status is public' do context 'when status is public' do
pending before do
status.update(visibility: :public)
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
end end
context 'when status is private' do context 'when status is private' do
before do
status.update(visibility: :private)
end
context 'when user is authorized to see it' do context 'when user is authorized to see it' do
pending before do
remote_account.follow!(account)
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
end end
context 'when user is not authorized to see it' do context 'when user is not authorized to see it' do
pending before do
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http not_found' do
expect(response).to have_http_status(404)
end
end end
end end
context 'when status is direct' do context 'when status is direct' do
before do
status.update(visibility: :direct)
end
context 'when user is authorized to see it' do context 'when user is authorized to see it' do
pending before do
Fabricate(:mention, account: remote_account, status: status)
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
end end
context 'when user is not authorized to see it' do context 'when user is not authorized to see it' do
pending before do
get :activity, params: { account_username: account.username, id: status.id }
end
it 'returns http not_found' do
expect(response).to have_http_status(404)
end
end end
end end
end end

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
Fabricator(:canonical_email_block) do Fabricator(:canonical_email_block) do
email 'test@example.com' email { sequence(:email) { |i| "#{i}#{Faker::Internet.email}" } }
reference_account { Fabricate(:account) } reference_account { Fabricate(:account) }
end end

@ -39,6 +39,37 @@ RSpec.describe ActivityPub::Activity::Flag do
end end
end end
context 'when the report comment is excessively long' do
subject do
described_class.new({
'@context': 'https://www.w3.org/ns/activitystreams',
id: flag_id,
type: 'Flag',
content: long_comment,
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: [
ActivityPub::TagManager.instance.uri_for(flagged),
ActivityPub::TagManager.instance.uri_for(status),
],
}.with_indifferent_access, sender)
end
let(:long_comment) { Faker::Lorem.characters(number: 6000) }
before do
subject.perform
end
it 'creates a report but with a truncated comment' do
report = Report.find_by(account: sender, target_account: flagged)
expect(report).to_not be_nil
expect(report.comment.length).to eq 5000
expect(report.comment).to eq long_comment[0...5000]
expect(report.status_ids).to eq [status.id]
end
end
context 'when the reported status is private and should not be visible to the remote server' do context 'when the reported status is private and should not be visible to the remote server' do
let(:status) { Fabricate(:status, account: flagged, uri: 'foobar', visibility: :private) } let(:status) { Fabricate(:status, account: flagged, uri: 'foobar', visibility: :private) }

@ -0,0 +1,34 @@
# frozen_string_literal: true
require 'rails_helper'
require 'mastodon/migration_warning'
describe Mastodon::MigrationWarning do
describe 'migration_duration_warning' do
before do
allow(migration).to receive(:valid_environment?).and_return(true)
allow(migration).to receive(:sleep).with(1)
end
let(:migration) { Class.new(ActiveRecord::Migration[6.1]).extend(described_class) }
context 'with the default message' do
it 'warns about long migrations' do
expectation = expect { migration.migration_duration_warning }
expectation.to output(/interrupt this migration/).to_stdout
expectation.to output(/Continuing in 5/).to_stdout
end
end
context 'with an additional message' do
it 'warns about long migrations' do
expectation = expect { migration.migration_duration_warning('Get ready for it') }
expectation.to output(/interrupt this migration/).to_stdout
expectation.to output(/Get ready for it/).to_stdout
expectation.to output(/Continuing in 5/).to_stdout
end
end
end
end

@ -25,7 +25,7 @@ RSpec.describe AccountMigration do
end end
end end
context 'with unresolveable account' do context 'with unresolvable account' do
let(:target_acct) { 'target@remote' } let(:target_acct) { 'target@remote' }
before do before do

@ -698,7 +698,7 @@ RSpec.describe Account do
expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil
end end
xit 'does not match URL querystring' do xit 'does not match URL query string' do
expect(subject.match('https://example.com/?x=@alice')).to be_nil expect(subject.match('https://example.com/?x=@alice')).to be_nil
end end
end end

@ -121,10 +121,17 @@ describe Report do
end end
describe 'validations' do describe 'validations' do
it 'is invalid if comment is longer than 1000 characters' do let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }
it 'is invalid if comment is longer than 1000 characters only if reporter is local' do
report = Fabricate.build(:report, comment: Faker::Lorem.characters(number: 1001)) report = Fabricate.build(:report, comment: Faker::Lorem.characters(number: 1001))
report.valid? expect(report.valid?).to be false
expect(report).to model_have_error_on_field(:comment) expect(report).to model_have_error_on_field(:comment)
end end
it 'is valid if comment is longer than 1000 characters and reporter is not local' do
report = Fabricate.build(:report, account: remote_account, comment: Faker::Lorem.characters(number: 1001))
expect(report.valid?).to be true
end
end end
end end

@ -90,7 +90,7 @@ RSpec.describe UserSettings::Setting do
describe '#key' do describe '#key' do
context 'when there is no namespace' do context 'when there is no namespace' do
it 'returnsn a symbol' do it 'returns a symbol' do
expect(subject.key).to eq :foo expect(subject.key).to eq :foo
end end
end end

@ -181,7 +181,7 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
'@context': ['https://www.w3.org/ns/activitystreams'], '@context': ['https://www.w3.org/ns/activitystreams'],
id: "https://foo.test/users/#{i}/featured", id: "https://foo.test/users/#{i}/featured",
type: 'OrderedCollection', type: 'OrderedCollection',
totelItems: 1, totalItems: 1,
orderedItems: [status_json], orderedItems: [status_json],
}.with_indifferent_access }.with_indifferent_access
webfinger = { webfinger = {

@ -21,6 +21,27 @@ RSpec.describe BackupService, type: :service do
end end
end end
context 'when the user has an avatar and header' do
before do
user.account.update!(avatar: attachment_fixture('avatar.gif'))
user.account.update!(header: attachment_fixture('emojo.png'))
end
it 'stores them as expected' do
service_call
json = Oj.load(read_zip_file(backup, 'actor.json'))
avatar_path = json.dig('icon', 'url')
header_path = json.dig('image', 'url')
expect(avatar_path).to_not be_nil
expect(header_path).to_not be_nil
expect(read_zip_file(backup, avatar_path)).to be_present
expect(read_zip_file(backup, header_path)).to be_present
end
end
it 'marks the backup as processed' do it 'marks the backup as processed' do
expect { service_call }.to change(backup, :processed).from(false).to(true) expect { service_call }.to change(backup, :processed).from(false).to(true)
end end

@ -6,6 +6,14 @@ RSpec.describe ReportService, type: :service do
subject { described_class.new } subject { described_class.new }
let(:source_account) { Fabricate(:account) } let(:source_account) { Fabricate(:account) }
let(:target_account) { Fabricate(:account) }
context 'with a local account' do
it 'has a uri' do
report = subject.call(source_account, target_account)
expect(report.uri).to_not be_nil
end
end
context 'with a remote account' do context 'with a remote account' do
let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }
@ -35,7 +43,6 @@ RSpec.describe ReportService, type: :service do
-> { described_class.new.call(source_account, target_account, status_ids: [status.id]) } -> { described_class.new.call(source_account, target_account, status_ids: [status.id]) }
end end
let(:target_account) { Fabricate(:account) }
let(:status) { Fabricate(:status, account: target_account, visibility: :direct) } let(:status) { Fabricate(:status, account: target_account, visibility: :direct) }
context 'when it is addressed to the reporter' do context 'when it is addressed to the reporter' do
@ -91,8 +98,7 @@ RSpec.describe ReportService, type: :service do
-> { described_class.new.call(source_account, target_account) } -> { described_class.new.call(source_account, target_account) }
end end
let!(:target_account) { Fabricate(:account) } let!(:other_report) { Fabricate(:report, target_account: target_account) }
let!(:other_report) { Fabricate(:report, target_account: target_account) }
before do before do
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear

@ -2184,11 +2184,6 @@
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
"@types/raf@^3.4.0":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2"
integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==
"@types/range-parser@*": "@types/range-parser@*":
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
@ -2800,7 +2795,7 @@ acorn@^6.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
acorn@^8.0.4, acorn@^8.1.0, acorn@^8.5.0, acorn@^8.8.0, acorn@^8.8.1, acorn@^8.8.2: acorn@^8.0.4, acorn@^8.1.0, acorn@^8.5.0, acorn@^8.8.0, acorn@^8.8.1:
version "8.8.2" version "8.8.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
@ -7537,19 +7532,16 @@ jsdom@^20.0.0:
ws "^8.11.0" ws "^8.11.0"
xml-name-validator "^4.0.0" xml-name-validator "^4.0.0"
jsdom@^21.1.2: jsdom@^22.0.0:
version "21.1.2" version "22.0.0"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.2.tgz#6433f751b8718248d646af1cdf6662dc8a1ca7f9" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-22.0.0.tgz#3295c6992c70089c4b8f5cf060489fddf7ee9816"
integrity sha512-sCpFmK2jv+1sjff4u7fzft+pUh2KSUbUrEHYHyfSIbGTIcmnjyp83qg6qLwdJ/I3LpTXx33ACxeRL7Lsyc6lGQ== integrity sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw==
dependencies: dependencies:
abab "^2.0.6" abab "^2.0.6"
acorn "^8.8.2"
acorn-globals "^7.0.0"
cssstyle "^3.0.0" cssstyle "^3.0.0"
data-urls "^4.0.0" data-urls "^4.0.0"
decimal.js "^10.4.3" decimal.js "^10.4.3"
domexception "^4.0.0" domexception "^4.0.0"
escodegen "^2.0.0"
form-data "^4.0.0" form-data "^4.0.0"
html-encoding-sniffer "^3.0.0" html-encoding-sniffer "^3.0.0"
http-proxy-agent "^5.0.0" http-proxy-agent "^5.0.0"
@ -9520,7 +9512,7 @@ quick-lru@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
raf@^3.1.0, raf@^3.4.1: raf@^3.1.0:
version "3.4.1" version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==

Loading…
Cancel
Save