diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9d9e54d4f..47497794f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -20,7 +20,7 @@ "forwardPorts": [3000, 4000], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "bundle install --path vendor/bundle && yarn install && ./bin/rails db:setup", + "postCreateCommand": "bundle install --path vendor/bundle && yarn install && git checkout -- Gemfile.lock && ./bin/rails db:setup", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode" diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 538f6cccd..46f42c454 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -27,6 +27,7 @@ services: ES_ENABLED: 'true' ES_HOST: es ES_PORT: '9200' + LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 # Overrides default command so things don't shut down after the process ends. command: sleep infinity networks: @@ -72,6 +73,12 @@ services: soft: -1 hard: -1 + libretranslate: + image: libretranslate/libretranslate:v1.2.9 + restart: unless-stopped + networks: + - internal_network + volumes: postgres-data: redis-data: diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 63aefc37e..3d232120b 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -10,6 +10,9 @@ on: paths: - .github/workflows/build-image.yml - Dockerfile +permissions: + contents: read + jobs: build-image: runs-on: ubuntu-latest diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index 1c60515f8..a9d8ea2ea 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -9,6 +9,9 @@ on: env: RAILS_ENV: test +permissions: + contents: read + jobs: check-i18n: runs-on: ubuntu-latest diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index f77a9720e..cd8cb12c4 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -55,7 +55,7 @@ jobs: with: node-version: 16.x cache: yarn - - name: Intall dependencies + - name: Install dependencies run: yarn install --frozen-lockfile - name: Set-up RuboCop Problem Mathcher uses: r7kamura/rubocop-problem-matchers-action@v1 diff --git a/Dockerfile b/Dockerfile index 7757bd0e4..cf311fef2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ SHELL ["/bin/bash", "-c"] RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections # Install Node v16 (LTS) -ENV NODE_VER="16.16.0" +ENV NODE_VER="16.17.1" RUN ARCH= && \ dpkgArch="$(dpkg --print-architecture)" && \ case "${dpkgArch##*-}" in \ @@ -19,7 +19,7 @@ RUN ARCH= && \ esac && \ echo "Etc/UTC" > /etc/localtime && \ apt-get update && \ - apt-get install -y --no-install-recommends ca-certificates wget python apt-utils && \ + apt-get install -y --no-install-recommends ca-certificates wget python3 apt-utils && \ cd ~ && \ wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \ tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \ diff --git a/Gemfile b/Gemfile index 2c2837acc..f3565982d 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ gem 'pkg-config', '~> 1.4' gem 'rexml', '~> 3.2' gem 'puma', '~> 5.6' -gem 'rails', '~> 6.1.6' +gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' gem 'rack', '~> 2.2.4' @@ -46,7 +46,7 @@ gem 'omniauth-rails_csrf_protection', '~> 0.1' gem 'color_diff', '~> 0.1' gem 'discard', '~> 1.2' -gem 'doorkeeper', '~> 5.5' +gem 'doorkeeper', '~> 5.6' gem 'ed25519', '~> 1.3' gem 'fast_blank', '~> 1.0' gem 'fastimage' @@ -55,7 +55,7 @@ gem 'redis-namespace', '~> 1.9' gem 'htmlentities', '~> 4.3' gem 'http', '~> 5.1' gem 'http_accept_language', '~> 2.1' -gem 'httplog', '~> 1.5.0' +gem 'httplog', '~> 1.6.0' gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' @@ -116,7 +116,7 @@ end group :test do gem 'capybara', '~> 3.37' gem 'climate_control', '~> 0.2' - gem 'faker', '~> 2.22' + gem 'faker', '~> 2.23' gem 'microformats', '~> 4.4' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.1' diff --git a/Gemfile.lock b/Gemfile.lock index a6cb3146f..6847b3642 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,40 +10,40 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.6) - actionpack (= 6.1.6) - activesupport (= 6.1.6) + actioncable (6.1.7) + actionpack (= 6.1.7) + activesupport (= 6.1.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.6) - actionpack (= 6.1.6) - activejob (= 6.1.6) - activerecord (= 6.1.6) - activestorage (= 6.1.6) - activesupport (= 6.1.6) + actionmailbox (6.1.7) + actionpack (= 6.1.7) + activejob (= 6.1.7) + activerecord (= 6.1.7) + activestorage (= 6.1.7) + activesupport (= 6.1.7) mail (>= 2.7.1) - actionmailer (6.1.6) - actionpack (= 6.1.6) - actionview (= 6.1.6) - activejob (= 6.1.6) - activesupport (= 6.1.6) + actionmailer (6.1.7) + actionpack (= 6.1.7) + actionview (= 6.1.7) + activejob (= 6.1.7) + activesupport (= 6.1.7) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.6) - actionview (= 6.1.6) - activesupport (= 6.1.6) + actionpack (6.1.7) + actionview (= 6.1.7) + activesupport (= 6.1.7) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.6) - actionpack (= 6.1.6) - activerecord (= 6.1.6) - activestorage (= 6.1.6) - activesupport (= 6.1.6) + actiontext (6.1.7) + actionpack (= 6.1.7) + activerecord (= 6.1.7) + activestorage (= 6.1.7) + activesupport (= 6.1.7) nokogiri (>= 1.8.5) - actionview (6.1.6) - activesupport (= 6.1.6) + actionview (6.1.7) + activesupport (= 6.1.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -54,22 +54,22 @@ GEM case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) active_record_query_trace (1.8) - activejob (6.1.6) - activesupport (= 6.1.6) + activejob (6.1.7) + activesupport (= 6.1.7) globalid (>= 0.3.6) - activemodel (6.1.6) - activesupport (= 6.1.6) - activerecord (6.1.6) - activemodel (= 6.1.6) - activesupport (= 6.1.6) - activestorage (6.1.6) - actionpack (= 6.1.6) - activejob (= 6.1.6) - activerecord (= 6.1.6) - activesupport (= 6.1.6) + activemodel (6.1.7) + activesupport (= 6.1.7) + activerecord (6.1.7) + activemodel (= 6.1.7) + activesupport (= 6.1.7) + activestorage (6.1.7) + actionpack (= 6.1.7) + activejob (= 6.1.7) + activerecord (= 6.1.7) + activesupport (= 6.1.7) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.6) + activesupport (6.1.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -110,12 +110,11 @@ GEM coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) - better_html (1.0.16) - actionview (>= 4.0) - activesupport (>= 4.0) + better_html (2.0.1) + actionview (>= 6.0) + activesupport (>= 6.0) ast (~> 2.0) erubi (~> 1.4) - html_tokenizer (~> 0.0.6) parser (>= 2.4) smart_properties bindata (2.4.10) @@ -176,7 +175,7 @@ GEM coderay (1.1.3) color_diff (0.1) concurrent-ruby (1.1.10) - connection_pool (2.2.5) + connection_pool (2.3.0) cose (1.2.1) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) @@ -207,7 +206,7 @@ GEM docile (1.3.4) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - doorkeeper (5.5.4) + doorkeeper (5.6.0) railties (>= 5) dotenv (2.8.1) dotenv-rails (2.8.1) @@ -224,12 +223,12 @@ GEM faraday (~> 1) multi_json encryptor (3.0.0) - erubi (1.10.0) + erubi (1.11.0) et-orbi (1.2.7) tzinfo excon (0.76.0) fabrication (2.30.0) - faker (2.22.0) + faker (2.23.0) i18n (>= 1.8.11, < 2) faraday (1.9.3) faraday-em_http (~> 1.0) @@ -301,7 +300,6 @@ GEM highline (2.0.3) hiredis (0.6.3) hkdf (0.3.0) - html_tokenizer (0.0.7) htmlentities (4.3.4) http (5.1.0) addressable (~> 2.8) @@ -313,15 +311,15 @@ GEM http-form_data (2.3.0) http_accept_language (2.1.1) httpclient (2.8.3) - httplog (1.5.0) - rack (>= 1.0) + httplog (1.6.0) + rack (>= 2.0) rainbow (>= 2.0.0) i18n (1.12.0) concurrent-ruby (~> 1.0) - i18n-tasks (1.0.11) + i18n-tasks (1.0.12) activesupport (>= 4.0.2) ast (>= 2.1.0) - better_html (~> 1.0) + better_html (>= 1.0, < 3.0) erubi highline (>= 2.0.0) i18n @@ -386,7 +384,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.18.0) + loofah (2.19.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -407,7 +405,7 @@ GEM mime-types-data (3.2022.0105) mini_mime (1.1.2) mini_portile2 (2.8.0) - minitest (5.16.2) + minitest (5.16.3) msgpack (1.5.4) multi_json (1.15.0) multipart-post (2.1.1) @@ -454,7 +452,7 @@ GEM orm_adapter (0.5.0) ox (2.14.11) parallel (1.22.1) - parser (3.1.2.0) + parser (3.1.2.1) ast (~> 2.4.1) parslet (2.0.0) pastel (0.8.0) @@ -502,20 +500,20 @@ GEM rack rack-test (2.0.2) rack (>= 1.3) - rails (6.1.6) - actioncable (= 6.1.6) - actionmailbox (= 6.1.6) - actionmailer (= 6.1.6) - actionpack (= 6.1.6) - actiontext (= 6.1.6) - actionview (= 6.1.6) - activejob (= 6.1.6) - activemodel (= 6.1.6) - activerecord (= 6.1.6) - activestorage (= 6.1.6) - activesupport (= 6.1.6) + rails (6.1.7) + actioncable (= 6.1.7) + actionmailbox (= 6.1.7) + actionmailer (= 6.1.7) + actionpack (= 6.1.7) + actiontext (= 6.1.7) + actionview (= 6.1.7) + activejob (= 6.1.7) + activemodel (= 6.1.7) + activerecord (= 6.1.7) + activestorage (= 6.1.7) + activesupport (= 6.1.7) bundler (>= 1.15.0) - railties (= 6.1.6) + railties (= 6.1.7) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -531,9 +529,9 @@ GEM railties (>= 6.0.0, < 7) rails-settings-cached (0.6.6) rails (>= 4.2.0) - railties (6.1.6) - actionpack (= 6.1.6) - activesupport (= 6.1.6) + railties (6.1.7) + actionpack (= 6.1.7) + activesupport (= 6.1.7) method_source rake (>= 12.2) thor (~> 1.0) @@ -613,10 +611,10 @@ GEM activerecord (>= 4.0.0) railties (>= 4.0.0) semantic_range (3.0.0) - sidekiq (6.5.5) - connection_pool (>= 2.2.2) + sidekiq (6.5.7) + connection_pool (>= 2.2.5) rack (~> 2.0) - redis (>= 4.5.0) + redis (>= 4.5.0, < 5) sidekiq-bulk (0.2.0) sidekiq sidekiq-scheduler (4.0.2) @@ -686,12 +684,12 @@ GEM unf (~> 0.1.0) tzinfo (2.0.5) concurrent-ruby (~> 1.0) - tzinfo-data (1.2022.3) + tzinfo-data (1.2022.4) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext unf_ext (0.0.8.2) - unicode-display_width (2.1.0) + unicode-display_width (2.3.0) uniform_notifier (1.16.0) validate_email (0.1.6) activemodel (>= 3.0) @@ -764,11 +762,11 @@ DEPENDENCIES devise-two-factor (~> 4.0) devise_pam_authenticatable2 (~> 9.2) discard (~> 1.2) - doorkeeper (~> 5.5) + doorkeeper (~> 5.6) dotenv-rails (~> 2.8) ed25519 (~> 1.3) fabrication (~> 2.30) - faker (~> 2.22) + faker (~> 2.23) fast_blank (~> 1.0) fastimage fog-core (<= 2.1.0) @@ -781,7 +779,7 @@ DEPENDENCIES htmlentities (~> 4.3) http (~> 5.1) http_accept_language (~> 2.1) - httplog (~> 1.5.0) + httplog (~> 1.6.0) i18n-tasks (~> 1.0) idn-ruby json-ld @@ -820,7 +818,7 @@ DEPENDENCIES rack (~> 2.2.4) rack-attack (~> 6.6) rack-cors (~> 1.1) - rails (~> 6.1.6) + rails (~> 6.1.7) rails-controller-testing (~> 1.0) rails-i18n (~> 6.0) rails-settings-cached (~> 0.6) diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 620c0ff78..654f2d92c 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -10,10 +10,10 @@ class AboutController < ApplicationController before_action :require_open_federation!, only: [:show, :more] before_action :set_body_classes, only: :show before_action :set_instance_presenter - before_action :set_expires_in, only: [:more, :terms] + before_action :set_expires_in, only: [:more] before_action :set_registration_form_time, only: :show - skip_before_action :require_functional!, only: [:more, :terms] + skip_before_action :require_functional!, only: [:more] def show; end @@ -28,8 +28,6 @@ class AboutController < ApplicationController @blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks? end - def terms; end - helper_method :display_blocks? helper_method :display_blocks_rationale? helper_method :public_fetch_mode? diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 9949206cb..02f3c3dd7 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,7 +7,7 @@ class AccountsController < ApplicationController include AccountControllerConcern include SignatureAuthentication - before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers before_action :set_body_classes diff --git a/app/controllers/activitypub/claims_controller.rb b/app/controllers/activitypub/claims_controller.rb index 08ad952df..339333e46 100644 --- a/app/controllers/activitypub/claims_controller.rb +++ b/app/controllers/activitypub/claims_controller.rb @@ -6,7 +6,7 @@ class ActivityPub::ClaimsController < ActivityPub::BaseController skip_before_action :authenticate_user! - before_action :require_signature! + before_action :require_account_signature! before_action :set_claim_result def create diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index ac7ab8a0b..23d874071 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -4,7 +4,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern - before_action :require_signature!, if: :authorized_fetch_mode? + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_items before_action :set_size before_action :set_type diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb index 940b77cf0..4e445bcb1 100644 --- a/app/controllers/activitypub/followers_synchronizations_controller.rb +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -4,7 +4,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro include SignatureVerification include AccountOwnedConcern - before_action :require_signature! + before_action :require_account_signature! before_action :set_items before_action :set_cache_headers diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 92dcb5ac7..5ee85474e 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -6,7 +6,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController include AccountOwnedConcern before_action :skip_unknown_actor_activity - before_action :require_signature! + before_action :require_actor_signature! skip_before_action :authenticate_user! def create @@ -49,17 +49,17 @@ class ActivityPub::InboxesController < ActivityPub::BaseController end def upgrade_account - if signed_request_account.ostatus? + if signed_request_account&.ostatus? signed_request_account.update(last_webfingered_at: nil) ResolveAccountWorker.perform_async(signed_request_account.acct) end - DeliveryFailureTracker.reset!(signed_request_account.inbox_url) + DeliveryFailureTracker.reset!(signed_request_actor.inbox_url) end def process_collection_synchronization raw_params = request.headers['Collection-Synchronization'] - return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' + return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' || signed_request_account.nil? # Re-using the syntax for signature parameters tree = SignatureParamsParser.new.parse(raw_params) @@ -71,6 +71,6 @@ class ActivityPub::InboxesController < ActivityPub::BaseController end def process_payload - ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id) + ActivityPub::ProcessingWorker.perform_async(signed_request_actor.id, body, @account&.id, signed_request_actor.class.name) end end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index cd3992502..60d201f76 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -6,7 +6,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern - before_action :require_signature!, if: :authorized_fetch_mode? + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_statuses before_action :set_cache_headers diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 4ff7cfa08..8e0f9de2e 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -7,7 +7,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController DESCENDANTS_LIMIT = 60 - before_action :require_signature!, if: :authorized_fetch_mode? + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_status before_action :set_cache_headers before_action :set_replies diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 2e393fbb6..7ce6599c5 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -131,4 +131,10 @@ class Api::BaseController < ApplicationController def disallow_unauthenticated_api_access? authorized_fetch_mode? end + + private + + def respond_with_error(code) + render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code + 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 a665863eb..b61de13b9 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::FollowerAccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:accounts' } + before_action -> { authorize_if_got_token! :read, :'read:accounts' } before_action :set_account after_action :insert_pagination_headers diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 7d885a212..37d3c2d78 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::FollowingAccountsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:accounts' } + before_action -> { authorize_if_got_token! :read, :'read:accounts' } before_action :set_account after_action :insert_pagination_headers diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 5537cc9b0..be84720aa 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -30,12 +30,12 @@ class Api::V1::AccountsController < Api::BaseController self.response_body = Oj.dump(response.body) self.status = response.status rescue ActiveRecord::RecordInvalid => e - render json: ValidationErrorFormatter.new(e, :'account.username' => :username, :'invite_request.text' => :reason).as_json, status: :unprocessable_entity + render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: :unprocessable_entity end def follow - follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true) - options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } } + follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, languages: params.key?(:languages) ? params[:languages] : nil, with_rate_limit: true) + options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options) end diff --git a/app/controllers/api/v1/statuses/translations_controller.rb b/app/controllers/api/v1/statuses/translations_controller.rb new file mode 100644 index 000000000..540b17d00 --- /dev/null +++ b/app/controllers/api/v1/statuses/translations_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::TranslationsController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :read, :'read:statuses' } + before_action :set_status + before_action :set_translation + + rescue_from TranslationService::NotConfiguredError, with: :not_found + rescue_from TranslationService::UnexpectedResponseError, TranslationService::QuotaExceededError, TranslationService::TooManyRequestsError, with: :service_unavailable + + def create + render json: @translation, serializer: REST::TranslationSerializer + end + + private + + def set_status + @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found + end + + def set_translation + @translation = TranslateStatusService.new.call(@status, content_locale) + end +end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 4dd0cac55..2394574b3 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -45,10 +45,14 @@ module SignatureVerification end end - def require_signature! + def require_account_signature! render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account end + def require_actor_signature! + render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_actor + end + def signed_request? request.headers['Signature'].present? end @@ -68,7 +72,11 @@ module SignatureVerification end def signed_request_account - return @signed_request_account if defined?(@signed_request_account) + signed_request_actor.is_a?(Account) ? signed_request_actor : nil + end + + def signed_request_actor + return @signed_request_actor if defined?(@signed_request_actor) raise SignatureVerificationError, 'Request not signed' unless signed_request? raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters? @@ -78,26 +86,30 @@ module SignatureVerification verify_signature_strength! verify_body_digest! - account = account_from_key_id(signature_params['keyId']) + actor = actor_from_key_id(signature_params['keyId']) - raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil? + raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? signature = Base64.decode64(signature_params['signature']) compare_signed_string = build_signed_string - return account unless verify_signature(account, signature, compare_signed_string).nil? + return actor unless verify_signature(actor, signature, compare_signed_string).nil? - account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) } + actor = stoplight_wrap_request { actor_refresh_key!(actor) } - raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil? + raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? - return account unless verify_signature(account, signature, compare_signed_string).nil? + return actor unless verify_signature(actor, signature, compare_signed_string).nil? - @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 + fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)" rescue SignatureVerificationError => e - @signature_verification_failure_reason = e.message - @signed_request_account = nil + fail_with! e.message + rescue HTTP::Error, OpenSSL::SSL::SSLError => e + fail_with! "Failed to fetch remote data: #{e.message}" + rescue Mastodon::UnexpectedResponseError + fail_with! 'Failed to fetch remote data (got unexpected reply from server)' + rescue Stoplight::Error::RedLight + fail_with! 'Fetching attempt skipped because of recent connection failure' end def request_body @@ -106,6 +118,11 @@ module SignatureVerification private + def fail_with!(message) + @signature_verification_failure_reason = message + @signed_request_actor = nil + end + def signature_params @signature_params ||= begin raw_signature = request.headers['Signature'] @@ -138,13 +155,23 @@ module SignatureVerification digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] } sha256 = digests.assoc('sha-256') raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil? - raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" if body_digest != sha256[1] + + return if body_digest == sha256[1] + + digest_size = begin + Base64.strict_decode64(sha256[1].strip).length + rescue ArgumentError + raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}" + end + + raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32 + raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" end - def verify_signature(account, 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 + def verify_signature(actor, signature, compare_signed_string) + if actor.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string) + @signed_request_actor = actor + @signed_request_actor end rescue OpenSSL::PKey::RSAError nil @@ -207,7 +234,7 @@ module SignatureVerification signature_params['keyId'].blank? || signature_params['signature'].blank? end - def account_from_key_id(key_id) + def actor_from_key_id(key_id) domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id if domain_not_allowed?(domain) @@ -216,27 +243,34 @@ module SignatureVerification end if key_id.start_with?('acct:') - stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) } + stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) - account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account) - account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) } + account = ActivityPub::TagManager.instance.uri_to_actor(key_id) + account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) } account end - rescue Mastodon::HostValidationError - nil + rescue Mastodon::PrivateNetworkAddressError => e + raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" + rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e + raise SignatureVerificationError, e.message end def stoplight_wrap_request(&block) Stoplight("source:#{request.remote_ip}", &block) - .with_fallback { nil } .with_threshold(1) .with_cool_off_time(5.minutes.seconds) .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } .run end - def account_refresh_key(account) - return if account.local? || !account.activitypub? - ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true) + def actor_refresh_key!(actor) + return if actor.local? || !actor.activitypub? + return actor.refresh! if actor.respond_to?(:refresh!) && actor.possibly_stale? + + ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false) + rescue Mastodon::PrivateNetworkAddressError => e + raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" + rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e + raise SignatureVerificationError, e.message end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index f898994ac..0d9e624ef 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -4,7 +4,7 @@ class FollowerAccountsController < ApplicationController include AccountControllerConcern include SignatureVerification - before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 11c6b6d50..68e1a79be 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -4,7 +4,7 @@ class FollowingAccountsController < ApplicationController include AccountControllerConcern include SignatureVerification - before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } diff --git a/app/controllers/privacy_controller.rb b/app/controllers/privacy_controller.rb new file mode 100644 index 000000000..ced84dbe5 --- /dev/null +++ b/app/controllers/privacy_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class PrivacyController < ApplicationController + layout 'public' + + before_action :set_instance_presenter + before_action :set_expires_in + + skip_before_action :require_functional! + + def show; end + + private + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + + def set_expires_in + expires_in 0, public: true + end +end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 3812f541e..55cc3790f 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -8,7 +8,7 @@ class StatusesController < ApplicationController layout 'public' - before_action :require_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :set_instance_presenter before_action :set_link_headers diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 46821a200..315eabb3d 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -8,7 +8,7 @@ class TagsController < ApplicationController layout 'public' - before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :authenticate_user!, if: :whitelist_mode? before_action :set_local before_action :set_tag diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 53170b7a6..cc2a7d4d4 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -51,6 +51,7 @@ const messages = defineMessages({ add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' }, + languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, }); const dateFormatOptions = { @@ -85,6 +86,7 @@ class Header extends ImmutablePureComponent { onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, onEditAccountNote: PropTypes.func.isRequired, + onChangeLanguages: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, domain: PropTypes.string.isRequired, hidden: PropTypes.bool, @@ -215,6 +217,9 @@ class Header extends ImmutablePureComponent { } else { menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); } + + menu.push({ text: intl.formatMessage(messages.languages), action: this.props.onChangeLanguages }); + menu.push(null); } menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle }); diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index 645ff29ea..783d318f9 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -23,6 +23,7 @@ export default class Header extends ImmutablePureComponent { onUnblockDomain: PropTypes.func.isRequired, onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, + onChangeLanguages: PropTypes.func.isRequired, hideTabs: PropTypes.bool, domain: PropTypes.string.isRequired, hidden: PropTypes.bool, @@ -92,6 +93,10 @@ export default class Header extends ImmutablePureComponent { this.props.onEditAccountNote(this.props.account); } + handleChangeLanguages = () => { + this.props.onChangeLanguages(this.props.account); + } + render () { const { account, hidden, hideTabs } = this.props; @@ -118,6 +123,7 @@ export default class Header extends ImmutablePureComponent { onEndorseToggle={this.handleEndorseToggle} onAddToList={this.handleAddToList} onEditAccountNote={this.handleEditAccountNote} + onChangeLanguages={this.handleChangeLanguages} domain={this.props.domain} hidden={hidden} /> diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js index 3fa7c1448..dc8b2d55a 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js @@ -130,12 +130,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(unblockDomain(domain)); }, - onAddToList(account){ + onAddToList (account) { dispatch(openModal('LIST_ADDER', { accountId: account.get('id'), })); }, + onChangeLanguages (account) { + dispatch(openModal('SUBSCRIBED_LANGUAGES', { + accountId: account.get('id'), + })); + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/javascript/flavours/glitch/features/report/components/status_check_box.js b/app/javascript/flavours/glitch/features/report/components/status_check_box.js index 76bf0eb85..2231fc0ce 100644 --- a/app/javascript/flavours/glitch/features/report/components/status_check_box.js +++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.js @@ -7,6 +7,7 @@ import DisplayName from 'flavours/glitch/components/display_name'; import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import Option from './option'; import MediaAttachments from 'flavours/glitch/components/media_attachments'; +import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon'; export default class StatusCheckBox extends React.PureComponent { @@ -36,7 +37,7 @@ export default class StatusCheckBox extends React.PureComponent { -
·
+
·
} /> diff --git a/app/javascript/flavours/glitch/features/subscribed_languages_modal/index.js b/app/javascript/flavours/glitch/features/subscribed_languages_modal/index.js new file mode 100644 index 000000000..3047b434c --- /dev/null +++ b/app/javascript/flavours/glitch/features/subscribed_languages_modal/index.js @@ -0,0 +1,125 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { is, List as ImmutableList, Set as ImmutableSet } from 'immutable'; +import { languages as preloadedLanguages } from 'flavours/glitch/util/initial_state'; +import Option from 'flavours/glitch/features/report/components/option'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import IconButton from 'flavours/glitch/components/icon_button'; +import Button from 'flavours/glitch/components/button'; +import { followAccount } from 'flavours/glitch/actions/accounts'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); + +const getAccountLanguages = createSelector([ + (state, accountId) => state.getIn(['timelines', `account:${accountId}`, 'items'], ImmutableList()), + state => state.get('statuses'), +], (statusIds, statuses) => + new ImmutableSet(statusIds.map(statusId => statuses.get(statusId)).filter(status => !status.get('reblog')).map(status => status.get('language')))); + +const mapStateToProps = (state, { accountId }) => ({ + acct: state.getIn(['accounts', accountId, 'acct']), + availableLanguages: getAccountLanguages(state, accountId), + selectedLanguages: ImmutableSet(state.getIn(['relationships', accountId, 'languages']) || ImmutableList()), +}); + +const mapDispatchToProps = (dispatch, { accountId }) => ({ + + onSubmit (languages) { + dispatch(followAccount(accountId, { languages })); + }, + +}); + +export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl +class SubscribedLanguagesModal extends ImmutablePureComponent { + + static propTypes = { + accountId: PropTypes.string.isRequired, + acct: PropTypes.string.isRequired, + availableLanguages: ImmutablePropTypes.setOf(PropTypes.string), + selectedLanguages: ImmutablePropTypes.setOf(PropTypes.string), + onClose: PropTypes.func.isRequired, + languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), + intl: PropTypes.object.isRequired, + submit: PropTypes.func.isRequired, + }; + + static defaultProps = { + languages: preloadedLanguages, + }; + + state = { + selectedLanguages: this.props.selectedLanguages, + }; + + handleLanguageToggle = (value, checked) => { + const { selectedLanguages } = this.state; + + if (checked) { + this.setState({ selectedLanguages: selectedLanguages.add(value) }); + } else { + this.setState({ selectedLanguages: selectedLanguages.delete(value) }); + } + }; + + handleSubmit = () => { + this.props.onSubmit(this.state.selectedLanguages.toArray()); + this.props.onClose(); + } + + renderItem (value) { + const language = this.props.languages.find(language => language[0] === value); + const checked = this.state.selectedLanguages.includes(value); + + if (!language) { + return null; + } + + return ( +