diff --git a/.codeclimate.yml b/.codeclimate.yml index b4ec9400e..9c9b4517a 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -30,7 +30,7 @@ plugins: channel: eslint-7 rubocop: enabled: true - channel: rubocop-0-82 + channel: rubocop-0-88 sass-lint: enabled: true exclude_patterns: diff --git a/.rubocop.yml b/.rubocop.yml index 25e0fa940..14728bf0e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -25,30 +25,68 @@ Layout/AccessModifierIndentation: Layout/EmptyLineAfterMagicComment: Enabled: false +Layout/EmptyLineAfterGuardClause: + Enabled: false + +Layout/EmptyLinesAroundAttributeAccessor: + Enabled: true + +Layout/HashAlignment: + Enabled: false + # EnforcedHashRocketStyle: table + # EnforcedColonStyle: table + +Layout/SpaceAroundMethodCallOperator: + Enabled: true + Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: space +Lint/DeprecatedOpenSSLConstant: + Enabled: true + +Lint/DuplicateElsifCondition: + Enabled: true + +Lint/MixedRegexpCaptureTypes: + Enabled: true + +Lint/RaiseException: + Enabled: true + +Lint/StructNewOverride: + Enabled: true + Lint/UselessAccessModifier: ContextCreatingMethods: - class_methods Metrics/AbcSize: Max: 100 + Exclude: + - 'lib/mastodon/*_cli.rb' Metrics/BlockLength: - Max: 35 + Max: 55 Exclude: - 'lib/tasks/**/*' + - 'lib/mastodon/*_cli.rb' Metrics/BlockNesting: Max: 3 + Exclude: + - 'lib/mastodon/*_cli.rb' Metrics/ClassLength: CountComments: false - Max: 300 + Max: 400 + Exclude: + - 'lib/mastodon/*_cli.rb' Metrics/CyclomaticComplexity: Max: 25 + Exclude: + - 'lib/mastodon/*_cli.rb' Layout/LineLength: AllowURI: true @@ -56,7 +94,9 @@ Layout/LineLength: Metrics/MethodLength: CountComments: false - Max: 55 + Max: 65 + Exclude: + - 'lib/mastodon/*_cli.rb' Metrics/ModuleLength: CountComments: false @@ -67,24 +107,29 @@ Metrics/ParameterLists: CountKeywordArgs: true Metrics/PerceivedComplexity: - Max: 20 + Max: 25 Naming/MemoizedInstanceVariableName: Enabled: false +Naming/MethodParameterName: + Enabled: true + Rails: Enabled: true -Rails/EnumHash: +Rails/ApplicationController: Enabled: false + Exclude: + - 'app/controllers/well_known/**/*.rb' -Rails/HasAndBelongsToMany: +Rails/BelongsTo: Enabled: false -Rails/SkipsModelValidations: +Rails/ContentTag: Enabled: false -Rails/HttpStatus: +Rails/EnumHash: Enabled: false Rails/Exit: @@ -92,9 +137,60 @@ Rails/Exit: - 'lib/mastodon/*' - 'lib/cli.rb' +Rails/FilePath: + Enabled: false + +Rails/HasAndBelongsToMany: + Enabled: false + +Rails/HasManyOrHasOneDependent: + Enabled: false + Rails/HelperInstanceVariable: Enabled: false +Rails/HttpStatus: + Enabled: false + +Rails/IndexBy: + Enabled: false + +Rails/InverseOf: + Enabled: false + +Rails/LexicallyScopedActionFilter: + Enabled: false + +Rails/OutputSafety: + Enabled: true + +Rails/RakeEnvironment: + Enabled: false + +Rails/RedundantForeignKey: + Enabled: false + +Rails/SkipsModelValidations: + Enabled: false + +Rails/UniqueValidationWithoutIndex: + Enabled: false + +Style/AccessorGrouping: + Enabled: true + +Style/AccessModifierDeclarations: + Enabled: false + +Style/ArrayCoercion: + Enabled: true + +Style/BisectedAttrAccessor: + Enabled: true + +Style/CaseLikeIf: + Enabled: false + Style/ClassAndModuleChildren: Enabled: false @@ -109,6 +205,15 @@ Style/Documentation: Style/DoubleNegation: Enabled: true +Style/ExpandPathArguments: + Enabled: false + +Style/ExponentialNotation: + Enabled: true + +Style/FormatString: + Enabled: false + Style/FormatStringToken: Enabled: false @@ -118,9 +223,33 @@ Style/FrozenStringLiteralComment: Style/GuardClause: Enabled: false +Style/HashAsLastArrayItem: + Enabled: false + +Style/HashEachMethods: + Enabled: true + +Style/HashLikeCase: + Enabled: true + +Style/HashTransformKeys: + Enabled: true + +Style/HashTransformValues: + Enabled: false + +Style/IfUnlessModifier: + Enabled: false + +Style/InverseMethods: + Enabled: false + Style/Lambda: Enabled: false +Style/MutableConstant: + Enabled: false + Style/PercentLiteralDelimiters: PreferredDelimiters: '%i': '()' @@ -129,9 +258,36 @@ Style/PercentLiteralDelimiters: Style/PerlBackrefs: AutoCorrect: false +Style/RedundantAssignment: + Enabled: false + +Style/RedundantFetchBlock: + Enabled: true + +Style/RedundantFileExtensionInRequire: + Enabled: true + +Style/RedundantRegexpCharacterClass: + Enabled: false + +Style/RedundantRegexpEscape: + Enabled: false + +Style/RedundantReturn: + Enabled: true + Style/RegexpLiteral: Enabled: false +Style/RescueStandardError: + Enabled: false + +Style/SignalException: + Enabled: false + +Style/SlicingWithRange: + Enabled: true + Style/SymbolArray: Enabled: false @@ -140,3 +296,6 @@ Style/TrailingCommaInArrayLiteral: Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: 'comma' + +Style/UnpackFirst: + Enabled: false diff --git a/Aptfile b/Aptfile index 0a01fa24b..419d159ef 100644 --- a/Aptfile +++ b/Aptfile @@ -5,7 +5,6 @@ libidn11 libidn11-dev libpq-dev libprotobuf-dev -libssl-dev libxdamage1 libxfixes3 protobuf-compiler diff --git a/Dockerfile b/Dockerfile index fa6abad5a..c52f89fdc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,8 @@ RUN apt update && \ ./autogen.sh && \ ./configure --prefix=/opt/jemalloc && \ make -j$(nproc) > /dev/null && \ - make install_bin install_include install_lib + make install_bin install_include install_lib && \ + cd .. && rm -rf jemalloc-$JE_VER $JE_VER.tar.gz # Install Ruby ENV RUBY_VER="2.6.6" @@ -56,7 +57,8 @@ RUN apt update && \ --disable-install-doc && \ ln -s /opt/jemalloc/lib/* /usr/lib/ && \ make -j$(nproc) > /dev/null && \ - make install + make install && \ + cd .. && rm -rf ruby-$RUBY_VER.tar.gz ruby-$RUBY_VER ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin" @@ -107,11 +109,14 @@ RUN apt -y --no-install-recommends install \ rm -rf /var/lib/apt/lists/* # Add tini -ENV TINI_VERSION="0.18.0" -ENV TINI_SUM="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855" -ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini -RUN echo "$TINI_SUM tini" | sha256sum -c - -RUN chmod +x /tini +ENV TINI_VERSION="0.19.0" +RUN dpkgArch="$(dpkg --print-architecture)" && \ + ARCH=$dpkgArch && \ + wget https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-$ARCH \ + https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-$ARCH.sha256sum && \ + cat tini-$ARCH.sha256sum | sha256sum -c - && \ + mv tini-$ARCH /tini && rm tini-$ARCH.sha256sum && \ + chmod +x /tini # Copy over mastodon source, and dependencies from building, and set permissions COPY --chown=mastodon:mastodon . /opt/mastodon diff --git a/Gemfile b/Gemfile index 507c3ddff..f549f447a 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem 'makara', '~> 0.4' gem 'pghero', '~> 2.7' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.78', require: false +gem 'aws-sdk-s3', '~> 1.79', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' @@ -56,7 +56,7 @@ gem 'fast_blank', '~> 1.0' gem 'fastimage' gem 'goldfinger', '~> 2.1' gem 'hiredis', '~> 0.6' -gem 'redis-namespace', '~> 1.7' +gem 'redis-namespace', '~> 1.8' gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b' gem 'htmlentities', '~> 4.3' gem 'http', '~> 4.4' @@ -99,6 +99,7 @@ gem 'twitter-text', '~> 1.14' gem 'tzinfo-data', '~> 1.2020' gem 'webpacker', '~> 5.2' gem 'webpush' +gem 'webauthn', '~> 3.0.0.alpha1' gem 'json-ld' gem 'json-ld-preloaded', '~> 3.1' @@ -126,7 +127,7 @@ group :test do gem 'rspec-sidekiq', '~> 3.1' gem 'simplecov', '~> 0.19', require: false gem 'webmock', '~> 3.8' - gem 'parallel_tests', '~> 3.1' + gem 'parallel_tests', '~> 3.2' gem 'rspec_junit_formatter', '~> 0.4' end @@ -139,8 +140,8 @@ group :development do gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.4' gem 'memory_profiler' - gem 'rubocop', '~> 0.86', require: false - gem 'rubocop-rails', '~> 2.6', require: false + gem 'rubocop', '~> 0.90', require: false + gem 'rubocop-rails', '~> 2.8', require: false gem 'brakeman', '~> 4.9', require: false gem 'bundler-audit', '~> 0.7', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 0de091b5f..21c857669 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -67,6 +67,7 @@ GEM public_suffix (>= 2.0.2, < 5.0) airbrussh (1.4.0) sshkit (>= 1.6.1, != 1.7.0) + android_key_attestation (0.3.0) annotate (3.1.1) activerecord (>= 3.2, < 7.0) rake (>= 10.4, < 14.0) @@ -76,34 +77,36 @@ GEM encryptor (~> 3.0.0) av (0.9.0) cocaine (~> 0.5.3) + awrence (1.1.1) aws-eventstream (1.1.0) - aws-partitions (1.356.0) - aws-sdk-core (3.104.3) + aws-partitions (1.365.0) + aws-sdk-core (3.105.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.36.0) + aws-sdk-kms (1.37.0) aws-sdk-core (~> 3, >= 3.99.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.78.0) + aws-sdk-s3 (1.79.1) aws-sdk-core (~> 3, >= 3.104.3) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) aws-sigv4 (1.2.2) aws-eventstream (~> 1, >= 1.0.2) - bcrypt (3.1.15) + bcrypt (3.1.16) better_errors (2.7.1) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) + bindata (2.4.8) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) blurhash (0.1.4) ffi (~> 1.10.0) bootsnap (1.4.8) msgpack (~> 1.0) - brakeman (4.9.0) + brakeman (4.9.1) browser (4.2.0) builder (3.2.4) bullet (6.1.0) @@ -138,6 +141,7 @@ GEM xpath (~> 3.2) case_transform (0.2) activesupport + cbor (0.5.9.6) charlock_holmes (0.7.7) chewy (5.1.0) activesupport (>= 4.0) @@ -153,6 +157,9 @@ GEM color_diff (0.1) concurrent-ruby (1.1.7) connection_pool (2.2.3) + cose (1.0.0) + cbor (~> 0.5.9) + openssl-signature_algorithm (~> 0.4.0) crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.6) @@ -188,13 +195,13 @@ GEM railties (>= 3.2) e2mmap (0.1.0) ed25519 (1.2.4) - elasticsearch (7.8.1) - elasticsearch-api (= 7.8.1) - elasticsearch-transport (= 7.8.1) - elasticsearch-api (7.8.1) + elasticsearch (7.9.0) + elasticsearch-api (= 7.9.0) + elasticsearch-transport (= 7.9.0) + elasticsearch-api (7.9.0) multi_json elasticsearch-dsl (0.1.9) - elasticsearch-transport (7.8.1) + elasticsearch-transport (7.9.0) faraday (~> 1) multi_json encryptor (3.0.0) @@ -239,7 +246,7 @@ GEM http (~> 4.0) nokogiri (~> 1.8) oj (~> 3.0) - hamlit (2.11.0) + hamlit (2.11.1) temple (>= 0.8.2) thor tilt @@ -299,7 +306,7 @@ GEM json-ld (~> 3.1) rdf (~> 3.1) jsonapi-renderer (0.2.2) - jwt (2.2.1) + jwt (2.2.2) kaminari (1.2.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.1) @@ -326,7 +333,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.6.0) + loofah (2.7.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -339,7 +346,7 @@ GEM redis (>= 3.0.5) memory_profiler (0.9.14) method_source (1.0.0) - microformats (4.2.0) + microformats (4.2.1) json (~> 2.2) nokogiri (~> 1.10) mime-types (3.3.1) @@ -348,15 +355,15 @@ GEM mimemagic (0.3.5) mini_mime (1.0.2) mini_portile2 (2.4.0) - minitest (5.14.1) + minitest (5.14.2) msgpack (1.3.3) multi_json (1.15.0) multipart-post (2.1.1) - net-ldap (0.16.2) + net-ldap (0.16.3) net-scp (3.0.0) net-ssh (>= 2.6.5, < 7.0.0) net-ssh (6.1.0) - nio4r (2.5.2) + nio4r (2.5.3) nokogiri (1.10.10) mini_portile2 (~> 2.4.0) nokogumbo (2.0.2) @@ -366,7 +373,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.10.8) + oj (3.10.14) omniauth (1.9.1) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -377,8 +384,10 @@ GEM omniauth-saml (1.10.2) omniauth (~> 1.3, >= 1.3.2) ruby-saml (~> 1.9) + openssl (2.2.0) + openssl-signature_algorithm (0.4.0) orm_adapter (0.5.0) - ox (2.13.2) + ox (2.13.3) paperclip (6.0.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) @@ -389,7 +398,7 @@ GEM av (~> 0.9.0) paperclip (>= 2.5.2) parallel (1.19.2) - parallel_tests (3.1.0) + parallel_tests (3.2.0) parallel parser (2.7.1.4) ast (~> 2.4.1) @@ -417,8 +426,8 @@ GEM pry (~> 0.13.0) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (4.0.5) - puma (4.3.5) + public_suffix (4.0.6) + puma (4.3.6) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) @@ -467,7 +476,7 @@ GEM thor (>= 0.19.0, < 2.0) rainbow (3.0.0) rake (13.0.1) - rdf (3.1.5) + rdf (3.1.6) hamster (~> 3.0) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.4.0) @@ -480,7 +489,7 @@ GEM redis-activesupport (5.2.0) activesupport (>= 3, < 7) redis-store (>= 1.3, < 2) - redis-namespace (1.7.0) + redis-namespace (1.8.0) redis (>= 3.0.4) redis-rack (2.1.3) rack (>= 2.0.8, < 3) @@ -526,31 +535,34 @@ GEM rspec-support (3.9.3) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (0.86.0) + rubocop (0.90.0) parallel (~> 1.10) - parser (>= 2.7.0.1) + parser (>= 2.7.1.1) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.7) rexml - rubocop-ast (>= 0.0.3, < 1.0) + rubocop-ast (>= 0.3.0, < 1.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) rubocop-ast (0.3.0) parser (>= 2.7.1.4) - rubocop-rails (2.6.0) + rubocop-rails (2.8.0) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 0.82.0) + rubocop (>= 0.87.0) ruby-progressbar (1.10.1) ruby-saml (1.11.0) nokogiri (>= 1.5.10) rufus-scheduler (3.6.0) fugit (~> 1.1, >= 1.1.6) safe_yaml (1.0.5) + safety_net_attestation (0.4.0) + jwt (~> 2.0) sanitize (5.2.1) crass (~> 1.0.2) nokogiri (>= 1.8.0) nokogumbo (~> 2.0) + securecompare (1.0.0) semantic_range (2.3.0) sidekiq (6.1.1) connection_pool (>= 2.2.2) @@ -565,10 +577,10 @@ GEM sidekiq (>= 3) thwait tilt (>= 1.4.0) - sidekiq-unique-jobs (6.0.22) + sidekiq-unique-jobs (6.0.23) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 4.0, < 7.0) - thor (~> 0) + thor (>= 0.20, < 2.0) simple-navigation (4.1.0) activesupport (>= 2.3.2) simple_form (5.0.2) @@ -605,6 +617,9 @@ GEM thwait (0.2.0) e2mmap tilt (2.0.10) + tpm-key_attestation (0.9.0) + bindata (~> 2.4) + openssl-signature_algorithm (~> 0.4.0) tty-color (0.5.2) tty-cursor (0.7.1) tty-prompt (0.22.0) @@ -626,13 +641,23 @@ GEM unf_ext (0.0.7.7) unicode-display_width (1.7.0) uniform_notifier (1.13.0) - warden (1.2.8) - rack (>= 2.0.6) + warden (1.2.9) + rack (>= 2.0.9) + webauthn (3.0.0.alpha1) + android_key_attestation (~> 0.3.0) + awrence (~> 1.1) + bindata (~> 2.4) + cbor (~> 0.5.9) + cose (~> 1.0) + openssl (~> 2.0) + safety_net_attestation (~> 0.4.0) + securecompare (~> 1.0) + tpm-key_attestation (~> 0.9.0) webmock (3.8.3) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.2.0) + webpacker (5.2.1) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) @@ -655,7 +680,7 @@ DEPENDENCIES active_record_query_trace (~> 1.7) addressable (~> 2.7) annotate (~> 3.1) - aws-sdk-s3 (~> 1.78) + aws-sdk-s3 (~> 1.79) better_errors (~> 2.7) binding_of_caller (~> 0.7) blurhash (~> 0.1) @@ -726,7 +751,7 @@ DEPENDENCIES paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) parallel (~> 1.19) - parallel_tests (~> 3.1) + parallel_tests (~> 3.2) parslet pg (~> 1.2) pghero (~> 2.7) @@ -747,14 +772,14 @@ DEPENDENCIES rails-settings-cached (~> 0.6) rdf-normalize (~> 0.4) redis (~> 4.2) - redis-namespace (~> 1.7) + redis-namespace (~> 1.8) redis-rails (~> 5.0) rqrcode (~> 1.1) rspec-rails (~> 4.0) rspec-sidekiq (~> 3.1) rspec_junit_formatter (~> 0.4) - rubocop (~> 0.86) - rubocop-rails (~> 2.6) + rubocop (~> 0.90) + rubocop-rails (~> 2.8) ruby-progressbar (~> 1.10) sanitize (~> 5.2) sidekiq (~> 6.1) @@ -775,6 +800,7 @@ DEPENDENCIES tty-prompt (~> 0.22) twitter-text (~> 1.14) tzinfo-data (~> 1.2020) + webauthn (~> 3.0.0.alpha1) webmock (~> 3.8) webpacker (~> 5.2) webpush diff --git a/Procfile b/Procfile index d48b0373b..d15c835b8 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ -web: if [ "$RUN_STREAMING" != "true" ]; then BIND=0.0.0.0 bundle exec puma -C config/puma.rb; else BIND=0.0.0.0 node ./streaming; fi +web: bin/heroku-web worker: bundle exec sidekiq # For the streaming API, you need a separate app that shares Postgres and Redis: diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index db77b628c..d97d88fd9 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -28,8 +28,7 @@ class AccountsController < ApplicationController end @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? - @statuses = filtered_status_page - @statuses = cache_collection(@statuses, Status) + @statuses = cached_filtered_status_page @rss_url = rss_url unless @statuses.empty? @@ -142,8 +141,13 @@ class AccountsController < ApplicationController request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) end - def filtered_status_page - filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id)) + def cached_filtered_status_page + cache_collection_paginated_by_id( + filtered_statuses, + Status, + PAGE_SIZE, + params_slice(:max_id, :min_id, :since_id) + ) end def params_slice(*keys) diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 380de54f5..c8b6dcc88 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -12,7 +12,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def show expires_in 3.minutes, public: public_fetch_mode? - render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true + render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter end private @@ -20,17 +20,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def set_items case params[:id] when 'featured' - @items = begin - # Because in public fetch mode we cache the response, there would be no - # benefit from performing the check below, since a blocked account or domain - # would likely be served the cache from the reverse proxy anyway - - if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain))) - [] - else - cache_collection(@account.pinned_statuses, Status) - end - end + @items = for_signed_account { cache_collection(@account.pinned_statuses, Status) } + when 'tags' + @items = for_signed_account { @account.featured_tags } when 'devices' @items = @account.devices else @@ -40,7 +32,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def set_size case params[:id] - when 'featured', 'devices' + when 'featured', 'devices', 'tags' @size = @items.size else not_found @@ -51,7 +43,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController case params[:id] when 'featured' @type = :ordered - when 'devices' + when 'devices', 'tags' @type = :unordered else not_found @@ -66,4 +58,16 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController items: @items ) end + + def for_signed_account + # Because in public fetch mode we cache the response, there would be no + # benefit from performing the check below, since a blocked account or domain + # would likely be served the cache from the reverse proxy anyway + + if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain))) + [] + else + yield + end + end end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index e25a4bc07..5fd735ad6 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -20,9 +20,9 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def outbox_presenter if page_requested? ActivityPub::CollectionPresenter.new( - id: account_outbox_url(@account, page_params), + id: outbox_url(page_params), type: :ordered, - part_of: account_outbox_url(@account), + part_of: outbox_url, prev: prev_page, next: next_page, items: @statuses @@ -32,12 +32,20 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController id: account_outbox_url(@account), type: :ordered, size: @account.statuses_count, - first: account_outbox_url(@account, page: true), - last: account_outbox_url(@account, page: true, min_id: 0) + first: outbox_url(page: true), + last: outbox_url(page: true, min_id: 0) ) end end + def outbox_url(**kwargs) + if params[:account_username].present? + account_outbox_url(@account, **kwargs) + else + instance_actor_outbox_url(**kwargs) + end + end + def next_page account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT end @@ -49,9 +57,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def set_statuses return unless page_requested? - @statuses = @account.statuses.permitted_for(@account, signed_request_account) - @statuses = @statuses.paginate_by_id(LIMIT, params_slice(:max_id, :min_id, :since_id)) - @statuses = cache_collection(@statuses, Status) + @statuses = cache_collection_paginated_by_id( + @account.statuses.permitted_for(@account, signed_request_account), + Status, + LIMIT, + params_slice(:max_id, :min_id, :since_id) + ) end def page_requested? @@ -61,4 +72,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def page_params { page: true, max_id: params[:max_id], min_id: params[:min_id] }.compact end + + def set_account + @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative + end end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 045e7dd26..467225547 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -71,6 +71,7 @@ class Api::BaseController < ApplicationController def limit_param(default_limit) return default_limit unless params[:limit] + [params[:limit].to_i.abs, default_limit * 2].min end diff --git a/app/controllers/api/v1/accounts/featured_tags_controller.rb b/app/controllers/api/v1/accounts/featured_tags_controller.rb new file mode 100644 index 000000000..014d71956 --- /dev/null +++ b/app/controllers/api/v1/accounts/featured_tags_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::FeaturedTagsController < Api::BaseController + before_action :set_account + before_action :set_featured_tags + + respond_to :json + + def index + render json: @featured_tags, each_serializer: REST::AccountFeaturedTagSerializer + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def set_featured_tags + @featured_tags = @account.suspended? ? @account.featured_tags : [] + end +end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 2277067c9..a665863eb 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController end def hide_results? - (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) + @account.suspended? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) end def default_accounts diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 93d4bd3a4..7d885a212 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController end def hide_results? - (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) + @account.suspended? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) end def default_accounts diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb index 8dad6fee9..4b5f6902c 100644 --- a/app/controllers/api/v1/accounts/identity_proofs_controller.rb +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController before_action :set_account def index - @proofs = @account.identity_proofs.active + @proofs = @account.suspended? ? [] : @account.identity_proofs.active render json: @proofs, each_serializer: REST::IdentityProofSerializer end diff --git a/app/controllers/api/v1/accounts/lists_controller.rb b/app/controllers/api/v1/accounts/lists_controller.rb index ccb751f8f..c92f1f8a0 100644 --- a/app/controllers/api/v1/accounts/lists_controller.rb +++ b/app/controllers/api/v1/accounts/lists_controller.rb @@ -6,7 +6,7 @@ class Api::V1::Accounts::ListsController < Api::BaseController before_action :set_account def index - @lists = @account.lists.where(account: current_account) + @lists = @account.suspended? ? [] : @account.lists.where(account: current_account) render json: @lists, each_serializer: REST::ListSerializer end diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 1d3992a28..503f85c97 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController before_action :require_user! def index - accounts = Account.where(id: account_ids).select('id') + accounts = Account.without_suspended.where(id: account_ids).select('id') # .where doesn't guarantee that our results are in the same order # we requested them, so return the "right" order to the requestor. @accounts = accounts.index_by(&:id).values_at(*account_ids).compact diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 114ee0a82..92ccb8061 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -18,14 +18,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def load_statuses - cached_account_statuses + @account.suspended? ? [] : cached_account_statuses end def cached_account_statuses - cache_collection account_statuses, Status - end - - def account_statuses statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses statuses.merge!(only_media_scope) if truthy_param?(:only_media) @@ -33,7 +29,12 @@ class Api::V1::Accounts::StatusesController < Api::BaseController statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) statuses.merge!(hashtag_scope) if params[:tagged].present? - statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id)) + cache_collection_paginated_by_id( + statuses, + Status, + limit_param(DEFAULT_STATUSES_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ) end def permitted_account_statuses @@ -41,17 +42,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def only_media_scope - Status.where(id: account_media_status_ids) - end - - def account_media_status_ids - # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids. - # Also, Avoid getting slow by not narrowing down by `statuses.account_id`. - # When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used - # and the table will be joined by `Merge Semi Join`, so the query will be slow. - @account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account) - .paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) - .reorder(id: :desc).distinct(:id).pluck(:id) + Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id) end def pinned_scope diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 0080faf33..61dcb87c2 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -9,7 +9,6 @@ class Api::V1::AccountsController < Api::BaseController before_action :require_user!, except: [:show, :create] before_action :set_account, except: [:create] - before_action :check_account_suspension, only: [:show] before_action :check_enabled_registrations, only: [:create] skip_before_action :require_authenticated_user!, only: :create @@ -73,10 +72,6 @@ class Api::V1::AccountsController < Api::BaseController AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options) end - def check_account_suspension - gone if @account.suspended? - end - def account_params params.permit(:username, :email, :password, :agreement, :locale, :reason) end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index c35ea5ab2..24c7fbef1 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -79,7 +79,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController private def set_accounts - @accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + @accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end def set_account diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb index 1d48d3160..c8f4cd8d8 100644 --- a/app/controllers/api/v1/admin/reports_controller.rb +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -63,7 +63,7 @@ class Api::V1::Admin::ReportsController < Api::BaseController private def set_reports - @reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + @reports = filtered_reports.order(id: :desc).with_accounts.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end def set_report diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index a2baeef90..586cdfca9 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -18,6 +18,8 @@ class Api::V1::BlocksController < Api::BaseController def paginated_blocks @paginated_blocks ||= Block.eager_load(target_account: :account_stat) + .joins(:target_account) + .merge(Account.without_suspended) .where(account: current_account) .paginate_by_max_id( limit_param(DEFAULT_ACCOUNTS_LIMIT), diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb index c15212f0a..aa3fb88f0 100644 --- a/app/controllers/api/v1/bookmarks_controller.rb +++ b/app/controllers/api/v1/bookmarks_controller.rb @@ -17,14 +17,11 @@ class Api::V1::BookmarksController < Api::BaseController end def cached_bookmarks - cache_collection( - Status.reorder(nil).joins(:bookmarks).merge(results), - Status - ) + cache_collection(results.map(&:status), Status) end def results - @_results ||= account_bookmarks.paginate_by_id( + @_results ||= account_bookmarks.eager_load(:status).to_a_paginated_by_id( limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id) ) diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index bc8013379..6c7583403 100644 --- a/app/controllers/api/v1/conversations_controller.rb +++ b/app/controllers/api/v1/conversations_controller.rb @@ -32,7 +32,7 @@ class Api::V1::ConversationsController < Api::BaseController def paginated_conversations AccountConversation.where(account: current_account) - .paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + .to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end def insert_pagination_headers diff --git a/app/controllers/api/v1/crypto/encrypted_messages_controller.rb b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb index c764915e5..68cf4384f 100644 --- a/app/controllers/api/v1/crypto/encrypted_messages_controller.rb +++ b/app/controllers/api/v1/crypto/encrypted_messages_controller.rb @@ -26,7 +26,7 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController end def set_encrypted_messages - @encrypted_messages = @current_device.encrypted_messages.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + @encrypted_messages = @current_device.encrypted_messages.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end def insert_pagination_headers diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb index c87dbc4ce..9e80f468a 100644 --- a/app/controllers/api/v1/endorsements_controller.rb +++ b/app/controllers/api/v1/endorsements_controller.rb @@ -25,7 +25,7 @@ class Api::V1::EndorsementsController < Api::BaseController end def endorsed_accounts - current_account.endorsed_accounts.includes(:account_stat) + current_account.endorsed_accounts.includes(:account_stat).without_suspended end def insert_pagination_headers diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index 3e242905d..21836bc17 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -17,14 +17,11 @@ class Api::V1::FavouritesController < Api::BaseController end def cached_favourites - cache_collection( - Status.reorder(nil).joins(:favourites).merge(results), - Status - ) + cache_collection(results.map(&:status), Status) end def results - @_results ||= account_favourites.paginate_by_id( + @_results ||= account_favourites.eager_load(:status).to_a_paginated_by_id( limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id) ) diff --git a/app/controllers/api/v1/featured_tags/suggestions_controller.rb b/app/controllers/api/v1/featured_tags/suggestions_controller.rb index 8c1b81a0f..75545d3c7 100644 --- a/app/controllers/api/v1/featured_tags/suggestions_controller.rb +++ b/app/controllers/api/v1/featured_tags/suggestions_controller.rb @@ -3,15 +3,15 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index before_action :require_user! - before_action :set_most_used_tags, only: :index + before_action :set_recently_used_tags, only: :index def index - render json: @most_used_tags, each_serializer: REST::TagSerializer + render json: @recently_used_tags, each_serializer: REST::TagSerializer end private - def set_most_used_tags - @most_used_tags = Tag.most_used(current_account).where.not(id: current_account.featured_tags).limit(10) + def set_recently_used_tags + @recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10) end end diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 0ee6e531f..0420b7bef 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController end def default_accounts - Account.includes(:follow_requests, :account_stat).references(:follow_requests) + Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests) end def paginated_follow_requests diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index 23078263e..b66ea9bfe 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController def load_accounts if unlimited? - @list.accounts.includes(:account_stat).all + @list.accounts.without_suspended.includes(:account_stat).all else - @list.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) + @list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) end end diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 054172bee..e5ac45fef 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -38,6 +38,6 @@ class Api::V1::ListsController < Api::BaseController end def list_params - params.permit(:title) + params.permit(:title, :replies_policy) end end diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 65439fe9b..805d0dee2 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -18,6 +18,8 @@ class Api::V1::MutesController < Api::BaseController def paginated_mutes @paginated_mutes ||= Mute.eager_load(:target_account) + .joins(:target_account) + .merge(Account.without_suspended) .where(account: current_account) .paginate_by_max_id( limit_param(DEFAULT_ACCOUNTS_LIMIT), diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 8ac227765..522c35ba5 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -14,7 +14,7 @@ class Api::V1::NotificationsController < Api::BaseController end def show - @notification = current_account.notifications.find(params[:id]) + @notification = current_account.notifications.without_suspended.find(params[:id]) render json: @notification, serializer: REST::NotificationSerializer end @@ -31,18 +31,16 @@ class Api::V1::NotificationsController < Api::BaseController private def load_notifications - cache_collection paginated_notifications, Notification - end - - def paginated_notifications - browserable_account_notifications.paginate_by_id( + cache_collection_paginated_by_id( + browserable_account_notifications, + Notification, limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params_slice(:max_id, :since_id, :min_id) ) end def browserable_account_notifications - current_account.notifications.browserable(exclude_types, from_account) + current_account.notifications.without_suspended.browserable(exclude_types, from_account) end def target_statuses_from_notifications diff --git a/app/controllers/api/v1/scheduled_statuses_controller.rb b/app/controllers/api/v1/scheduled_statuses_controller.rb index 9950296f3..f90642a73 100644 --- a/app/controllers/api/v1/scheduled_statuses_controller.rb +++ b/app/controllers/api/v1/scheduled_statuses_controller.rb @@ -32,7 +32,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController private def set_statuses - @statuses = current_account.scheduled_statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id)) + @statuses = current_account.scheduled_statuses.to_a_paginated_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id)) end def set_status diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 8229786d6..2b614a837 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -22,6 +22,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController def default_accounts Account + .without_suspended .includes(:favourites, :account_stat) .references(:favourites) .where(favourites: { status_id: @status.id }) diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 6c9e49d90..24db30fcc 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController end def default_accounts - Account.includes(:statuses, :account_stat).references(:statuses) + Account.without_suspended.includes(:statuses, :account_stat).references(:statuses) end def paginated_statuses diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index c6e7854d9..d253b744f 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -16,30 +16,29 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def load_statuses - cached_public_statuses + cached_public_statuses_page end - def cached_public_statuses - cache_collection public_statuses, Status + def cached_public_statuses_page + cache_collection(public_statuses, Status) end def public_statuses - statuses = public_timeline_statuses.paginate_by_id( + public_feed.get( limit_param(DEFAULT_STATUSES_LIMIT), - params_slice(:max_id, :since_id, :min_id) + params[:max_id], + params[:since_id], + params[:min_id] ) - - if truthy_param?(:only_media) - # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids. - status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id) - statuses.where(id: status_ids) - else - statuses - end end - def public_timeline_statuses - Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local)) + def public_feed + PublicFeed.new( + current_account, + local: truthy_param?(:local), + remote: truthy_param?(:remote), + only_media: truthy_param?(:only_media) + ) end def insert_pagination_headers diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 2d6ad5a80..64a1db58d 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -20,30 +20,29 @@ class Api::V1::Timelines::TagController < Api::BaseController end def cached_tagged_statuses - cache_collection tagged_statuses, Status + @tag.nil? ? [] : cache_collection(tag_timeline_statuses, Status) end - def tagged_statuses - if @tag.nil? - [] - else - statuses = tag_timeline_statuses.paginate_by_id( - limit_param(DEFAULT_STATUSES_LIMIT), - params_slice(:max_id, :since_id, :min_id) - ) - - if truthy_param?(:only_media) - # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids. - status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id) - statuses.where(id: status_ids) - else - statuses - end - end + def tag_timeline_statuses + tag_feed.get( + limit_param(DEFAULT_STATUSES_LIMIT), + params[:max_id], + params[:since_id], + params[:min_id] + ) end - def tag_timeline_statuses - HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, truthy_param?(:local)) + def tag_feed + TagFeed.new( + @tag, + current_account, + any: params[:any], + all: params[:all], + none: params[:none], + local: truthy_param?(:local), + remote: truthy_param?(:remote), + only_media: truthy_param?(:only_media) + ) end def insert_pagination_headers diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 1fd755334..c1ea702ad 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -37,6 +37,22 @@ class Auth::SessionsController < Devise::SessionsController store_location_for(:user, tmp_stored_location) if continue_after? end + def webauthn_options + user = find_user + + if user.webauthn_enabled? + options_for_get = WebAuthn::Credential.options_for_get( + allow: user.webauthn_credentials.pluck(:external_id) + ) + + session[:webauthn_challenge] = options_for_get.challenge + + render json: options_for_get, status: :ok + else + render json: { error: t('webauthn_credentials.not_enabled') }, status: :unauthorized + end + end + protected def find_user @@ -51,7 +67,7 @@ class Auth::SessionsController < Devise::SessionsController end def user_params - params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt) + params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {}) end def after_sign_in_path_for(resource) diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index c7d25ae00..abbdb410a 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -47,4 +47,8 @@ module CacheConcern raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact end + + def cache_collection_paginated_by_id(raw, klass, limit, options) + cache_collection raw.cache_ids.to_a_paginated_by_id(limit, options), klass + end end diff --git a/app/controllers/concerns/challengable_concern.rb b/app/controllers/concerns/challengable_concern.rb index b29d90b3c..2995a25e0 100644 --- a/app/controllers/concerns/challengable_concern.rb +++ b/app/controllers/concerns/challengable_concern.rb @@ -32,7 +32,6 @@ module ChallengableConcern if params.key?(:form_challenge) if challenge_passed? session[:challenge_passed_at] = Time.now.utc - return else flash.now[:alert] = I18n.t('challenge.invalid_password') render_challenge diff --git a/app/controllers/concerns/export_controller_concern.rb b/app/controllers/concerns/export_controller_concern.rb index bfe990c82..24cfc7a01 100644 --- a/app/controllers/concerns/export_controller_concern.rb +++ b/app/controllers/concerns/export_controller_concern.rb @@ -5,7 +5,6 @@ module ExportControllerConcern included do before_action :authenticate_user! - before_action :require_not_suspended! before_action :load_export skip_before_action :require_functional! @@ -30,8 +29,4 @@ module ExportControllerConcern def export_filename "#{controller_name}.csv" end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 10efbf2e0..f69c62ec2 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -7,6 +7,44 @@ module SignatureVerification include DomainControlHelper + EXPIRATION_WINDOW_LIMIT = 12.hours + CLOCK_SKEW_MARGIN = 1.hour + + class SignatureVerificationError < StandardError; end + + class SignatureParamsParser < Parslet::Parser + rule(:token) { match("[0-9a-zA-Z!#$%&'*+.^_`|~-]").repeat(1).as(:token) } + rule(:quoted_string) { str('"') >> (qdtext | quoted_pair).repeat.as(:quoted_string) >> str('"') } + # qdtext and quoted_pair are not exactly according to spec but meh + rule(:qdtext) { match('[^\\\\"]') } + rule(:quoted_pair) { str('\\') >> any } + rule(:bws) { match('\s').repeat } + rule(:param) { (token.as(:key) >> bws >> str('=') >> bws >> (token | quoted_string).as(:value)).as(:param) } + rule(:comma) { bws >> str(',') >> bws } + # Old versions of node-http-signature add an incorrect "Signature " prefix to the header + rule(:buggy_prefix) { str('Signature ') } + rule(:params) { buggy_prefix.maybe >> (param >> (comma >> param).repeat).as(:params) } + root(:params) + end + + class SignatureParamsTransformer < Parslet::Transform + rule(params: subtree(:p)) do + (p.is_a?(Array) ? p : [p]).each_with_object({}) { |(key, val), h| h[key] = val } + end + + rule(param: { key: simple(:key), value: simple(:val) }) do + [key, val] + end + + rule(quoted_string: simple(:string)) do + string.to_s + end + + rule(token: simple(:string)) do + string.to_s + end + end + def require_signature! render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account end @@ -24,72 +62,40 @@ module SignatureVerification end def signature_key_id - raw_signature = request.headers['Signature'] - signature_params = {} - - raw_signature.split(',').each do |part| - parsed_parts = part.match(/([a-z]+)="([^"]+)"/i) - next if parsed_parts.nil? || parsed_parts.size != 3 - signature_params[parsed_parts[1]] = parsed_parts[2] - end - signature_params['keyId'] + rescue SignatureVerificationError + nil end def signed_request_account return @signed_request_account if defined?(@signed_request_account) - unless signed_request? - @signature_verification_failure_reason = 'Request not signed' - @signed_request_account = nil - return - end - - if request.headers['Date'].present? && !matches_time_window? - @signature_verification_failure_reason = 'Signed request date outside acceptable time window' - @signed_request_account = nil - return - end + 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? - raw_signature = request.headers['Signature'] - signature_params = {} - - raw_signature.split(',').each do |part| - parsed_parts = part.match(/([a-z]+)="([^"]+)"/i) - next if parsed_parts.nil? || parsed_parts.size != 3 - signature_params[parsed_parts[1]] = parsed_parts[2] - end - - if incompatible_signature?(signature_params) - @signature_verification_failure_reason = 'Incompatible request signature' - @signed_request_account = nil - return - end + verify_signature_strength! account = account_from_key_id(signature_params['keyId']) - if account.nil? - @signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}" - @signed_request_account = nil - return - end + raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil? signature = Base64.decode64(signature_params['signature']) - compare_signed_string = build_signed_string(signature_params['headers']) + compare_signed_string = build_signed_string return account unless verify_signature(account, signature, compare_signed_string).nil? account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) } - if account.nil? - @signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}" - @signed_request_account = nil - return - end + raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil? return account unless verify_signature(account, signature, compare_signed_string).nil? - @signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}" + @signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)" + @signed_request_account = nil + rescue SignatureVerificationError => e + @signature_verification_failure_reason = e.message @signed_request_account = nil end @@ -99,8 +105,33 @@ module SignatureVerification private + def signature_params + @signature_params ||= begin + raw_signature = request.headers['Signature'] + tree = SignatureParamsParser.new.parse(raw_signature) + SignatureParamsTransformer.new.apply(tree) + end + rescue Parslet::ParseFailed + 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?(Request::REQUEST_TARGET) || signed_headers.include?('digest') + raise SignatureVerificationError, 'Mastodon requires the Host header to be signed' unless 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_signature(account, signature, compare_signed_string) - if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string) + if account.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string) @signed_request_account = account @signed_request_account end @@ -108,12 +139,20 @@ module SignatureVerification nil end - def build_signed_string(signed_headers) - signed_headers = 'date' if signed_headers.blank? - - signed_headers.downcase.split(' ').map do |signed_header| + def build_signed_string + signed_headers.map do |signed_header| if signed_header == Request::REQUEST_TARGET "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" + elsif signed_header == '(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']}" + elsif signed_header == '(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']}" elsif signed_header == 'digest' "digest: #{body_digest}" else @@ -123,13 +162,28 @@ module SignatureVerification end def matches_time_window? + created_time = nil + expires_time = nil + begin - time_sent = Time.httpdate(request.headers['Date']) + 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 return false end - (Time.now.utc - time_sent).abs <= 12.hours + 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 @@ -140,9 +194,8 @@ module SignatureVerification name.split(/-/).map(&:capitalize).join('-') end - def incompatible_signature?(signature_params) - signature_params['keyId'].blank? || - signature_params['signature'].blank? + def missing_required_signature_parameters? + signature_params['keyId'].blank? || signature_params['signature'].blank? end def account_from_key_id(key_id) diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb index daafe56f4..8a2a86a02 100644 --- a/app/controllers/concerns/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/two_factor_authentication_concern.rb @@ -8,7 +8,23 @@ module TwoFactorAuthenticationConcern end def two_factor_enabled? - find_user&.otp_required_for_login? + find_user&.two_factor_enabled? + end + + def valid_webauthn_credential?(user, webauthn_credential) + user_credential = user.webauthn_credentials.find_by!(external_id: webauthn_credential.id) + + begin + webauthn_credential.verify( + session[:webauthn_challenge], + public_key: user_credential.public_key, + sign_count: user_credential.sign_count + ) + + user_credential.update!(sign_count: webauthn_credential.sign_count) + rescue WebAuthn::Error + false + end end def valid_otp_attempt?(user) @@ -21,14 +37,29 @@ module TwoFactorAuthenticationConcern def authenticate_with_two_factor user = self.resource = find_user - if user_params[:otp_attempt].present? && session[:attempt_user_id] - authenticate_with_two_factor_attempt(user) + if user.webauthn_enabled? && user_params[:credential].present? && session[:attempt_user_id] + authenticate_with_two_factor_via_webauthn(user) + elsif user_params[:otp_attempt].present? && session[:attempt_user_id] + authenticate_with_two_factor_via_otp(user) elsif user.present? && user.external_or_valid_password?(user_params[:password]) prompt_for_two_factor(user) end end - def authenticate_with_two_factor_attempt(user) + def authenticate_with_two_factor_via_webauthn(user) + webauthn_credential = WebAuthn::Credential.from_get(user_params[:credential]) + + if valid_webauthn_credential?(user, webauthn_credential) + session.delete(:attempt_user_id) + remember_me(user) + sign_in(user) + render json: { redirect_path: root_path }, status: :ok + else + render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity + end + end + + def authenticate_with_two_factor_via_otp(user) if valid_otp_attempt?(user) session.delete(:attempt_user_id) remember_me(user) @@ -43,6 +74,12 @@ module TwoFactorAuthenticationConcern set_locale do session[:attempt_user_id] = user.id @body_classes = 'lighter' + @webauthn_enabled = user.webauthn_enabled? + @scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank? + 'webauthn' + else + 'totp' + end render :two_factor end end diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index 6f02d6a35..4b074ca19 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -17,6 +17,6 @@ class InstanceActorsController < ApplicationController end def restrict_fields_to - %i(id type preferred_username inbox public_key endpoints url manually_approves_followers) + %i(id type preferred_username inbox outbox public_key endpoints url manually_approves_followers) end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index fb8389034..45151cdd7 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :store_current_location before_action :authenticate_resource_owner! + before_action :require_not_suspended!, only: :destroy before_action :set_body_classes skip_before_action :require_functional! @@ -25,4 +26,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def store_current_location store_location_for(:user, request.url) end + + def require_not_suspended! + forbidden if current_account.suspended? + end end diff --git a/app/controllers/settings/aliases_controller.rb b/app/controllers/settings/aliases_controller.rb index b7c9a409d..a421b8ede 100644 --- a/app/controllers/settings/aliases_controller.rb +++ b/app/controllers/settings/aliases_controller.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class Settings::AliasesController < Settings::BaseController - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! + before_action :require_not_suspended! before_action :set_aliases, except: :destroy before_action :set_alias, only: :destroy diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index ed3f82a8e..d3ac268d8 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::ApplicationsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_application, only: [:show, :update, :destroy, :regenerate] before_action :prepare_scopes, only: [:create, :update] diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index 3c404cfff..8311538a5 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class Settings::BaseController < ApplicationController + layout 'admin' + + before_action :authenticate_user! before_action :set_body_classes before_action :set_cache_headers @@ -13,4 +16,8 @@ class Settings::BaseController < ApplicationController def set_cache_headers response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' end + + def require_not_suspended! + forbidden if current_account.suspended? + end end diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index 15a59c999..7d4844e60 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true class Settings::DeletesController < Settings::BaseController - layout 'admin' + skip_before_action :require_functional! - before_action :check_enabled_deletion - before_action :authenticate_user! before_action :require_not_suspended! - - skip_before_action :require_functional! + before_action :check_enabled_deletion def show @confirmation = Form::DeleteConfirmation.new diff --git a/app/controllers/settings/exports/blocked_accounts_controller.rb b/app/controllers/settings/exports/blocked_accounts_controller.rb index 2092104e0..2190caa36 100644 --- a/app/controllers/settings/exports/blocked_accounts_controller.rb +++ b/app/controllers/settings/exports/blocked_accounts_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class BlockedAccountsController < ApplicationController + class BlockedAccountsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/blocked_domains_controller.rb b/app/controllers/settings/exports/blocked_domains_controller.rb index 6676ce340..bee4b2431 100644 --- a/app/controllers/settings/exports/blocked_domains_controller.rb +++ b/app/controllers/settings/exports/blocked_domains_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class BlockedDomainsController < ApplicationController + class BlockedDomainsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/following_accounts_controller.rb b/app/controllers/settings/exports/following_accounts_controller.rb index 74281ddca..acefcb15d 100644 --- a/app/controllers/settings/exports/following_accounts_controller.rb +++ b/app/controllers/settings/exports/following_accounts_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class FollowingAccountsController < ApplicationController + class FollowingAccountsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/lists_controller.rb b/app/controllers/settings/exports/lists_controller.rb index cf5a9de44..bc65f56a0 100644 --- a/app/controllers/settings/exports/lists_controller.rb +++ b/app/controllers/settings/exports/lists_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class ListsController < ApplicationController + class ListsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/muted_accounts_controller.rb b/app/controllers/settings/exports/muted_accounts_controller.rb index e511619ca..50b7bf1f7 100644 --- a/app/controllers/settings/exports/muted_accounts_controller.rb +++ b/app/controllers/settings/exports/muted_accounts_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class MutedAccountsController < ApplicationController + class MutedAccountsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 0e93d07a9..30138d29e 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -3,11 +3,6 @@ class Settings::ExportsController < Settings::BaseController include Authorization - layout 'admin' - - before_action :authenticate_user! - before_action :require_not_suspended! - skip_before_action :require_functional! def show @@ -16,8 +11,6 @@ class Settings::ExportsController < Settings::BaseController end def create - raise Mastodon::NotPermittedError unless user_signed_in? - backup = nil RedisLock.acquire(lock_options) do |lock| @@ -37,8 +30,4 @@ class Settings::ExportsController < Settings::BaseController def lock_options { redis: Redis.current, key: "backup:#{current_user.id}" } end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb index 3a3241425..e805527d0 100644 --- a/app/controllers/settings/featured_tags_controller.rb +++ b/app/controllers/settings/featured_tags_controller.rb @@ -1,12 +1,9 @@ # frozen_string_literal: true class Settings::FeaturedTagsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_featured_tags, only: :index before_action :set_featured_tag, except: [:index, :create] - before_action :set_most_used_tags, only: :index + before_action :set_recently_used_tags, only: :index def index @featured_tag = FeaturedTag.new @@ -20,7 +17,7 @@ class Settings::FeaturedTagsController < Settings::BaseController redirect_to settings_featured_tags_path else set_featured_tags - set_most_used_tags + set_recently_used_tags render :index end @@ -41,8 +38,8 @@ class Settings::FeaturedTagsController < Settings::BaseController @featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?) end - def set_most_used_tags - @most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) + def set_recently_used_tags + @recently_used_tags = Tag.recently_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) end def featured_tag_params diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb index 3a90b7c4d..bf2899da6 100644 --- a/app/controllers/settings/identity_proofs_controller.rb +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::IdentityProofsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :check_required_params, only: :new def index diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index 7b8c4ae23..d4516526e 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::ImportsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_account def show diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb index 97193ade0..6d469f384 100644 --- a/app/controllers/settings/migration/redirects_controller.rb +++ b/app/controllers/settings/migration/redirects_controller.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true class Settings::Migration::RedirectsController < Settings::BaseController - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_not_suspended! - skip_before_action :require_functional! - def new @redirect = Form::Redirect.new end @@ -38,8 +35,4 @@ class Settings::Migration::RedirectsController < Settings::BaseController def resource_params params.require(:form_redirect).permit(:acct, :current_password, :current_username) end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb index 68304bb51..62603aba8 100644 --- a/app/controllers/settings/migrations_controller.rb +++ b/app/controllers/settings/migrations_controller.rb @@ -1,15 +1,12 @@ # frozen_string_literal: true class Settings::MigrationsController < Settings::BaseController - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_not_suspended! before_action :set_migrations before_action :set_cooldown - skip_before_action :require_functional! - def show @migration = current_account.migrations.build end @@ -44,8 +41,4 @@ class Settings::MigrationsController < Settings::BaseController def on_cooldown? @cooldown.present? end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb index df2a6eed3..28df65f8f 100644 --- a/app/controllers/settings/pictures_controller.rb +++ b/app/controllers/settings/pictures_controller.rb @@ -2,7 +2,6 @@ module Settings class PicturesController < BaseController - before_action :authenticate_user! before_action :set_account before_action :set_picture diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index bac9b329d..be4dc904d 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class Settings::PreferencesController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! - def show; end def update diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 19a7ce157..0c15447a6 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::ProfilesController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_account def show diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index df5ace803..ee2fc5dc8 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true class Settings::SessionsController < Settings::BaseController - before_action :authenticate_user! - before_action :set_session, only: :destroy - skip_before_action :require_functional! + before_action :require_not_suspended! + before_action :set_session, only: :destroy + def destroy @session.destroy! flash[:notice] = I18n.t('sessions.revoke_success') diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index ef4df3339..1a0afe58b 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -5,31 +5,31 @@ module Settings class ConfirmationsController < BaseController include ChallengableConcern - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_challenge! before_action :ensure_otp_secret - skip_before_action :require_functional! - def new prepare_two_factor_form end def create - if current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) + if current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt], otp_secret: session[:new_otp_secret]) flash.now[:notice] = I18n.t('two_factor_authentication.enabled_success') current_user.otp_required_for_login = true + current_user.otp_secret = session[:new_otp_secret] @recovery_codes = current_user.generate_otp_backup_codes! current_user.save! UserMailer.two_factor_enabled(current_user).deliver_later! + session.delete(:new_otp_secret) + render 'settings/two_factor_authentication/recovery_codes/index' else - flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code') + flash.now[:alert] = I18n.t('otp_authentication.wrong_code') prepare_two_factor_form render :new end @@ -43,12 +43,15 @@ module Settings def prepare_two_factor_form @confirmation = Form::TwoFactorConfirmation.new - @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Rails.configuration.x.local_domain) + @new_otp_secret = session[:new_otp_secret] + @provision_url = current_user.otp_provisioning_uri(current_user.email, + otp_secret: @new_otp_secret, + issuer: Rails.configuration.x.local_domain) @qrcode = RQRCode::QRCode.new(@provision_url) end def ensure_otp_secret - redirect_to settings_two_factor_authentication_path unless current_user.otp_secret + redirect_to settings_otp_authentication_path if session[:new_otp_secret].blank? end end end diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb new file mode 100644 index 000000000..cbba842a9 --- /dev/null +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Settings + module TwoFactorAuthentication + class OtpAuthenticationController < BaseController + include ChallengableConcern + + skip_before_action :require_functional! + + before_action :verify_otp_not_enabled, only: [:show] + before_action :require_challenge!, only: [:create] + + def show + @confirmation = Form::TwoFactorConfirmation.new + end + + def create + session[:new_otp_secret] = User.generate_otp_secret(32) + + redirect_to new_settings_two_factor_authentication_confirmation_path + end + + private + + def confirmation_params + params.require(:form_two_factor_confirmation).permit(:otp_attempt) + end + + def verify_otp_not_enabled + redirect_to settings_two_factor_authentication_methods_path if current_user.otp_enabled? + end + + def acceptable_code? + current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) || + current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt]) + end + end + end +end diff --git a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb index 0c4f5bff7..6ec53224d 100644 --- a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb +++ b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb @@ -5,13 +5,10 @@ module Settings class RecoveryCodesController < BaseController include ChallengableConcern - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_challenge!, on: :create - skip_before_action :require_functional! - def create @recovery_codes = current_user.generate_otp_backup_codes! current_user.save! diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb new file mode 100644 index 000000000..1c557092b --- /dev/null +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module Settings + module TwoFactorAuthentication + class WebauthnCredentialsController < BaseController + skip_before_action :require_functional! + + before_action :require_otp_enabled + before_action :require_webauthn_enabled, only: [:index, :destroy] + + def new; end + + def index; end + + def options + current_user.update(webauthn_id: WebAuthn.generate_user_id) unless current_user.webauthn_id + + options_for_create = WebAuthn::Credential.options_for_create( + user: { + name: current_user.account.username, + display_name: current_user.account.username, + id: current_user.webauthn_id, + }, + exclude: current_user.webauthn_credentials.pluck(:external_id) + ) + + session[:webauthn_challenge] = options_for_create.challenge + + render json: options_for_create, status: :ok + end + + def create + webauthn_credential = WebAuthn::Credential.from_create(params[:credential]) + + if webauthn_credential.verify(session[:webauthn_challenge]) + user_credential = current_user.webauthn_credentials.build( + external_id: webauthn_credential.id, + public_key: webauthn_credential.public_key, + nickname: params[:nickname], + sign_count: webauthn_credential.sign_count + ) + + if user_credential.save + flash[:success] = I18n.t('webauthn_credentials.create.success') + status = :ok + + if current_user.webauthn_credentials.size == 1 + UserMailer.webauthn_enabled(current_user).deliver_later! + else + UserMailer.webauthn_credential_added(current_user, user_credential).deliver_later! + end + else + flash[:error] = I18n.t('webauthn_credentials.create.error') + status = :internal_server_error + end + else + flash[:error] = t('webauthn_credentials.create.error') + status = :unauthorized + end + + render json: { redirect_path: settings_two_factor_authentication_methods_path }, status: status + end + + def destroy + credential = current_user.webauthn_credentials.find_by(id: params[:id]) + if credential + credential.destroy + if credential.destroyed? + flash[:success] = I18n.t('webauthn_credentials.destroy.success') + + if current_user.webauthn_credentials.empty? + UserMailer.webauthn_disabled(current_user).deliver_later! + else + UserMailer.webauthn_credential_deleted(current_user, credential).deliver_later! + end + else + flash[:error] = I18n.t('webauthn_credentials.destroy.error') + end + else + flash[:error] = I18n.t('webauthn_credentials.destroy.error') + end + redirect_to settings_two_factor_authentication_methods_path + end + + private + + def require_otp_enabled + unless current_user.otp_enabled? + flash[:error] = t('webauthn_credentials.otp_required') + redirect_to settings_two_factor_authentication_methods_path + end + end + + def require_webauthn_enabled + unless current_user.webauthn_enabled? + flash[:error] = t('webauthn_credentials.not_enabled') + redirect_to settings_two_factor_authentication_methods_path + end + end + end + end +end diff --git a/app/controllers/settings/two_factor_authentication_methods_controller.rb b/app/controllers/settings/two_factor_authentication_methods_controller.rb new file mode 100644 index 000000000..205933ea8 --- /dev/null +++ b/app/controllers/settings/two_factor_authentication_methods_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Settings + class TwoFactorAuthenticationMethodsController < BaseController + include ChallengableConcern + + skip_before_action :require_functional! + + before_action :require_challenge!, only: :disable + before_action :require_otp_enabled + + def index; end + + def disable + current_user.disable_two_factor! + UserMailer.two_factor_disabled(current_user).deliver_later! + + redirect_to settings_otp_authentication_path, flash: { notice: I18n.t('two_factor_authentication.disabled_success') } + end + + private + + def require_otp_enabled + redirect_to settings_otp_authentication_path unless current_user.otp_enabled? + end + end +end diff --git a/app/controllers/settings/two_factor_authentications_controller.rb b/app/controllers/settings/two_factor_authentications_controller.rb deleted file mode 100644 index 9118a7933..000000000 --- a/app/controllers/settings/two_factor_authentications_controller.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module Settings - class TwoFactorAuthenticationsController < BaseController - include ChallengableConcern - - layout 'admin' - - before_action :authenticate_user! - before_action :verify_otp_required, only: [:create] - before_action :require_challenge!, only: [:create] - - skip_before_action :require_functional! - - def show - @confirmation = Form::TwoFactorConfirmation.new - end - - def create - current_user.otp_secret = User.generate_otp_secret(32) - current_user.save! - redirect_to new_settings_two_factor_authentication_confirmation_path - end - - def destroy - if acceptable_code? - current_user.otp_required_for_login = false - current_user.save! - UserMailer.two_factor_disabled(current_user).deliver_later! - redirect_to settings_two_factor_authentication_path - else - flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code') - @confirmation = Form::TwoFactorConfirmation.new - render :show - end - end - - private - - def confirmation_params - params.require(:form_two_factor_confirmation).permit(:otp_attempt) - end - - def verify_otp_required - redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login? - end - - def acceptable_code? - current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) || - current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt]) - end - end -end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 6426a7d69..6616ba107 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -10,8 +10,9 @@ class TagsController < ApplicationController before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :authenticate_user!, if: :whitelist_mode? - before_action :set_tag before_action :set_local + before_action :set_tag + before_action :set_statuses before_action :set_body_classes before_action :set_instance_presenter @@ -25,20 +26,11 @@ class TagsController < ApplicationController format.rss do expires_in 0, public: true - - limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE - @statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(limit) - @statuses = cache_collection(@statuses, Status) - render xml: RSS::TagSerializer.render(@tag, @statuses) end format.json do expires_in 3.minutes, public: public_fetch_mode? - - @statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, @local).paginate_by_max_id(PAGE_SIZE, params[:max_id]) - @statuses = cache_collection(@statuses, Status) - render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end end @@ -54,6 +46,15 @@ class TagsController < ApplicationController @local = truthy_param?(:local) end + def set_statuses + case request.format&.to_sym + when :json + @statuses = cache_collection(TagFeed.new(@tag, current_account, local: @local).get(PAGE_SIZE, params[:max_id], params[:since_id], params[:min_id]), Status) + when :rss + @statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status) + end + end + def set_body_classes @body_classes = 'with-modals' end @@ -62,16 +63,16 @@ class TagsController < ApplicationController @instance_presenter = InstancePresenter.new end + def limit_param + params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE + end + def collection_presenter ActivityPub::CollectionPresenter.new( - id: tag_url(@tag, filter_params), + id: tag_url(@tag), type: :ordered, size: @tag.statuses.count, items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) } ) end - - def filter_params - params.slice(:any, :all, :none).permit(:any, :all, :none) - end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8eccf884e..7d49a6dc1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -163,6 +163,8 @@ module ApplicationHelper end json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json + # rubocop:disable Rails/OutputSafety content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json') + # rubocop:enable Rails/OutputSafety end end diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index cb2c682a4..d28f7dad8 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -1,6 +1,5 @@ import api, { getLinks } from '../api'; -import openDB from '../storage/db'; -import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer'; +import { importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; @@ -74,45 +73,13 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST'; export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS'; export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; -function getFromDB(dispatch, getState, index, id) { - return new Promise((resolve, reject) => { - const request = index.get(id); - - request.onerror = reject; - - request.onsuccess = () => { - if (!request.result) { - reject(); - return; - } - - dispatch(importAccount(request.result)); - resolve(request.result.moved && getFromDB(dispatch, getState, index, request.result.moved)); - }; - }); -} - export function fetchAccount(id) { return (dispatch, getState) => { dispatch(fetchRelationships([id])); - - if (getState().getIn(['accounts', id], null) !== null) { - return; - } - dispatch(fetchAccountRequest(id)); - openDB().then(db => getFromDB( - dispatch, - getState, - db.transaction('accounts', 'read').objectStore('accounts').index('id'), - id, - ).then(() => db.close(), error => { - db.close(); - throw error; - })).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => { + api(getState).get(`/api/v1/accounts/${id}`).then(response => { dispatch(importFetchedAccount(response.data)); - })).then(() => { dispatch(fetchAccountSuccess()); }).catch(error => { dispatch(fetchAccountFail(id, error)); diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js index d736bacef..5ab922436 100644 --- a/app/javascript/mastodon/actions/lists.js +++ b/app/javascript/mastodon/actions/lists.js @@ -150,10 +150,10 @@ export const createListFail = error => ({ error, }); -export const updateList = (id, title, shouldReset) => (dispatch, getState) => { +export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => { dispatch(updateListRequest(id)); - api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => { + api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => { dispatch(updateListSuccess(data)); if (shouldReset) { diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index e565e0b0a..3fc7c0702 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -1,9 +1,7 @@ import api from '../api'; -import openDB from '../storage/db'; -import { evictStatus } from '../storage/modifier'; import { deleteFromTimelines } from './timelines'; -import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus, importFetchedAccount } from './importer'; +import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer'; import { ensureComposeIsVisible } from './compose'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; @@ -40,48 +38,6 @@ export function fetchStatusRequest(id, skipLoading) { }; }; -function getFromDB(dispatch, getState, accountIndex, index, id) { - return new Promise((resolve, reject) => { - const request = index.get(id); - - request.onerror = reject; - - request.onsuccess = () => { - const promises = []; - - if (!request.result) { - reject(); - return; - } - - dispatch(importStatus(request.result)); - - if (getState().getIn(['accounts', request.result.account], null) === null) { - promises.push(new Promise((accountResolve, accountReject) => { - const accountRequest = accountIndex.get(request.result.account); - - accountRequest.onerror = accountReject; - accountRequest.onsuccess = () => { - if (!request.result) { - accountReject(); - return; - } - - dispatch(importAccount(accountRequest.result)); - accountResolve(); - }; - })); - } - - if (request.result.reblog && getState().getIn(['statuses', request.result.reblog], null) === null) { - promises.push(getFromDB(dispatch, getState, accountIndex, index, request.result.reblog)); - } - - resolve(Promise.all(promises)); - }; - }); -} - export function fetchStatus(id) { return (dispatch, getState) => { const skipLoading = getState().getIn(['statuses', id], null) !== null; @@ -94,23 +50,10 @@ export function fetchStatus(id) { dispatch(fetchStatusRequest(id, skipLoading)); - openDB().then(db => { - const transaction = db.transaction(['accounts', 'statuses'], 'read'); - const accountIndex = transaction.objectStore('accounts').index('id'); - const index = transaction.objectStore('statuses').index('id'); - - return getFromDB(dispatch, getState, accountIndex, index, id).then(() => { - db.close(); - }, error => { - db.close(); - throw error; - }); - }).then(() => { - dispatch(fetchStatusSuccess(skipLoading)); - }, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => { + api(getState).get(`/api/v1/statuses/${id}`).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(fetchStatusSuccess(skipLoading)); - })).catch(error => { + }).catch(error => { dispatch(fetchStatusFail(id, error, skipLoading)); }); }; @@ -152,7 +95,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) { dispatch(deleteStatusRequest(id)); api(getState).delete(`/api/v1/statuses/${id}`).then(response => { - evictStatus(id); dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); dispatch(importFetchedAccount(response.data.account)); diff --git a/app/javascript/mastodon/components/gifv.js b/app/javascript/mastodon/components/gifv.js index 83cfae49c..b775e5200 100644 --- a/app/javascript/mastodon/components/gifv.js +++ b/app/javascript/mastodon/components/gifv.js @@ -54,8 +54,6 @@ export default class GIFV extends React.PureComponent {