diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 88979723c3..ecdf9f5f53 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -70,7 +70,7 @@ services: hard: -1 libretranslate: - image: libretranslate/libretranslate:v1.5.4 + image: libretranslate/libretranslate:v1.5.5 restart: unless-stopped volumes: - lt-data:/home/libretranslate/.local diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml index b03b787d5f..1e2455d3d9 100644 --- a/.github/workflows/build-security.yml +++ b/.github/workflows/build-security.yml @@ -38,7 +38,7 @@ jobs: tags: | type=raw,value=edge type=raw,value=nightly - type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} + type=raw,value=${{ needs.compute-suffix.outputs.prerelease }} secrets: inherit build-image-streaming: @@ -60,5 +60,5 @@ jobs: tags: | type=raw,value=edge type=raw,value=nightly - type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} + type=raw,value=${{ needs.compute-suffix.outputs.prerelease }} secrets: inherit diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index e17a4c1add..fd57c6e5a1 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -140,7 +140,7 @@ jobs: - name: Upload coverage reports to Codecov if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: coverage/lcov/mastodon.lcov @@ -347,7 +347,7 @@ jobs: if: failure() with: name: test-search-screenshots - path: tmp/screenshots/ + path: tmp/capybara/ test-back-and-return: name: Back to original and return test diff --git a/.rubocop.yml b/.rubocop.yml index 69845bfabe..583a0c2174 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -146,6 +146,11 @@ Rails/UnusedIgnoredColumns: Rails/NegateInclude: Enabled: false +# Reason: Enforce default limit, but allow some elements to span lines +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecexamplelength +RSpec/ExampleLength: + CountAsOne: ['array', 'heredoc', 'method_call'] + # Reason: Deprecated cop, will be removed in 3.0, replaced by SpecFilePathFormat # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath RSpec/FilePath: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8324722e5b..fbc5d80ff3 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -36,10 +36,10 @@ Metrics/PerceivedComplexity: # Configuration parameters: CountAsOne. RSpec/ExampleLength: - Max: 22 + Max: 20 # Override default of 5 RSpec/MultipleExpectations: - Max: 8 + Max: 7 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: diff --git a/Gemfile b/Gemfile index 108e69165e..eb40bb2c57 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,7 @@ gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.8' -gem 'bootsnap', '~> 1.17.0', require: false +gem 'bootsnap', '~> 1.18.0', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' gem 'chewy', '~> 7.3' @@ -63,7 +63,7 @@ gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar' gem 'nokogiri', '~> 1.16.2' -gem 'nsa', github: 'jhawthorn/nsa', ref: 'e020fcc3a54d993ab45b7194d89ab720296c111b' +gem 'nsa' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' diff --git a/Gemfile.lock b/Gemfile.lock index f5233036f1..171f6da7ec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,17 +7,6 @@ GIT hkdf (~> 0.2) jwt (~> 2.0) -GIT - remote: https://github.com/jhawthorn/nsa.git - revision: e020fcc3a54d993ab45b7194d89ab720296c111b - ref: e020fcc3a54d993ab45b7194d89ab720296c111b - specs: - nsa (0.2.8) - activesupport (>= 4.2, < 7.2) - concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (>= 3.5) - statsd-ruby (~> 1.4, >= 1.4.0) - GEM remote: https://rubygems.org/ specs: @@ -155,9 +144,9 @@ GEM binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) blurhash (0.1.7) - bootsnap (1.17.1) + bootsnap (1.18.3) msgpack (~> 1.2) - brakeman (6.1.1) + brakeman (6.1.2) racc browser (5.3.1) brpoplpush-redis_script (0.1.3) @@ -193,7 +182,8 @@ GEM cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) - crack (0.4.5) + crack (0.4.6) + bigdecimal rexml crass (1.0.6) css_parser (1.14.0) @@ -319,13 +309,13 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.55.0) + haml_lint (0.56.0) haml (>= 5.0) parallel (~> 1.10) rainbow rubocop (>= 1.0) sysexits (~> 1.1) - hashdiff (1.0.1) + hashdiff (1.1.0) hashie (5.0.0) hcaptcha (7.1.0) json @@ -361,7 +351,7 @@ GEM terminal-table (>= 1.5.1) idn-ruby (0.1.5) io-console (0.7.2) - irb (1.11.1) + irb (1.11.2) rdoc reline (>= 0.4.2) jmespath (1.6.2) @@ -468,6 +458,11 @@ GEM nokogiri (1.16.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) + nsa (0.3.0) + activesupport (>= 4.2, < 7.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + sidekiq (>= 3.5) + statsd-ruby (~> 1.4, >= 1.4.0) oj (3.16.3) bigdecimal (>= 3.0) omniauth (2.1.1) @@ -511,7 +506,7 @@ GEM pastel (0.8.0) tty-color (~> 0.5) pg (1.5.4) - pghero (3.4.0) + pghero (3.4.1) activerecord (>= 6) posix-spawn (0.3.15) premailer (1.21.0) @@ -773,7 +768,7 @@ GEM unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2023.4) + tzinfo-data (1.2024.1) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext @@ -800,7 +795,7 @@ GEM webfinger (1.2.0) activesupport httpclient (>= 2.4) - webmock (3.19.1) + webmock (3.20.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -831,7 +826,7 @@ DEPENDENCIES better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) - bootsnap (~> 1.17.0) + bootsnap (~> 1.18.0) brakeman (~> 6.0) browser bundler-audit (~> 0.9) @@ -888,7 +883,7 @@ DEPENDENCIES net-http (~> 0.4.0) net-ldap (~> 0.18) nokogiri (~> 1.16.2) - nsa! + nsa oj (~> 3.14) omniauth (~> 2.0) omniauth-cas (~> 3.0.0.beta.1) diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index ba85e0a722..e8b0f47cde 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -62,11 +62,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController 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) - params = SignatureParamsTransformer.new.apply(tree) + params = SignatureParser.parse(raw_params) ActivityPub::PrepareFollowersSynchronizationService.new.call(signed_request_account, params) - rescue Parslet::ParseFailed + rescue SignatureParser::ParsingError Rails.logger.warn 'Error parsing Collection-Synchronization header' end diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb index 9dfb181a28..7129a5f6ca 100644 --- a/app/controllers/api/v1/admin/reports_controller.rb +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -35,6 +35,7 @@ class Api::V1::Admin::ReportsController < Api::BaseController def update authorize @report, :update? @report.update!(report_params) + log_action :update, @report render json: @report, serializer: REST::Admin::ReportSerializer end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index b3aaafe82c..e2c01635f6 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -84,13 +84,9 @@ class Api::V1::StatusesController < Api::BaseController with_rate_limit: true ) - render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer + render json: @status, serializer: serializer_for_status rescue PostStatusService::UnexpectedMentionsError => e - unexpected_accounts = ActiveModel::Serializer::CollectionSerializer.new( - e.accounts, - serializer: REST::AccountSerializer - ) - render json: { error: e.message, unexpected_accounts: unexpected_accounts }, status: 422 + render json: unexpected_accounts_error_json(e), status: 422 end def update @@ -174,6 +170,21 @@ class Api::V1::StatusesController < Api::BaseController ) end + def serializer_for_status + @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer + end + + def unexpected_accounts_error_json(error) + { + error: error.message, + unexpected_accounts: serialized_accounts(error.accounts), + } + end + + def serialized_accounts(accounts) + ActiveModel::Serializer::CollectionSerializer.new(accounts, serializer: REST::AccountSerializer) + end + def pagination_params(core_params) params.slice(:limit).permit(:limit).merge(core_params) end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 962b78de65..6ed7b2baac 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -183,7 +183,9 @@ class Auth::SessionsController < Devise::SessionsController ) # Only send a notification email every hour at most - return if redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour, get: true).present? + return if redis.get("2fa_failure_notification:#{user.id}").present? + + redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour) UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 92f1eb5a16..3155866271 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -12,39 +12,6 @@ module SignatureVerification 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(:param)) do - (param.is_a?(Array) ? param : [param]).each_with_object({}) { |(key, value), hash| hash[key] = value } - 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_account_signature! render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account end @@ -135,12 +102,8 @@ module SignatureVerification end 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 + @signature_params ||= SignatureParser.parse(request.headers['Signature']) + rescue SignatureParser::ParsingError raise SignatureVerificationError, 'Error parsing signature parameters' end diff --git a/app/controllers/intents_controller.rb b/app/controllers/intents_controller.rb index ea024e30e6..65c315208d 100644 --- a/app/controllers/intents_controller.rb +++ b/app/controllers/intents_controller.rb @@ -1,27 +1,26 @@ # frozen_string_literal: true class IntentsController < ApplicationController - before_action :check_uri + EXPECTED_SCHEME = 'web+mastodon' + before_action :handle_invalid_uri, unless: :valid_uri? rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri def show - if uri.scheme == 'web+mastodon' - case uri.host - when 'follow' - return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:')) - when 'share' - return redirect_to share_path(text: uri.query_values['text']) - end + case uri.host + when 'follow' + redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:')) + when 'share' + redirect_to share_path(text: uri.query_values['text']) + else + handle_invalid_uri end - - not_found end private - def check_uri - not_found if uri.blank? + def valid_uri? + uri.present? && uri.scheme == EXPECTED_SCHEME end def handle_invalid_uri diff --git a/app/helpers/react_component_helper.rb b/app/helpers/react_component_helper.rb index ce616e8306..821a6f1e2d 100644 --- a/app/helpers/react_component_helper.rb +++ b/app/helpers/react_component_helper.rb @@ -15,9 +15,20 @@ module ReactComponentHelper div_tag_with_data(data) end + def serialized_media_attachments(media_attachments) + media_attachments.map { |attachment| serialized_attachment(attachment) } + end + private def div_tag_with_data(data) content_tag(:div, nil, data: data) end + + def serialized_attachment(attachment) + ActiveModelSerializers::SerializableResource.new( + attachment, + serializer: REST::MediaAttachmentSerializer + ).as_json + end end diff --git a/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx b/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx index ecce92b309..90f4334a6e 100644 --- a/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx +++ b/app/javascript/mastodon/components/admin/ReportReasonSelector.jsx @@ -124,7 +124,7 @@ class ReportReasonSelector extends PureComponent { api().put(`/api/v1/admin/reports/${id}`, { category, - rule_ids, + rule_ids: category === 'violation' ? rule_ids : [], }).catch(err => { console.error(err); }); diff --git a/app/javascript/mastodon/features/compose/components/upload_button.jsx b/app/javascript/mastodon/features/compose/components/upload_button.jsx index 3866f239d7..50c9ad6321 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.jsx +++ b/app/javascript/mastodon/features/compose/components/upload_button.jsx @@ -65,6 +65,7 @@ class UploadButton extends ImmutablePureComponent { key={resetFileKey} ref={this.setRef} type='file' + name='file-upload-input' multiple accept={acceptContentTypes.toArray().join(',')} onChange={this.handleChange} diff --git a/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx index ac414d04d6..f76526e045 100644 --- a/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx +++ b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx @@ -59,7 +59,7 @@ Source.propTypes = { id: PropTypes.oneOf(['friends_of_friends', 'similar_to_recently_followed', 'featured', 'most_followed', 'most_interactions']), }; -const Card = ({ id, source }) => { +const Card = ({ id, sources }) => { const intl = useIntl(); const account = useSelector(state => state.getIn(['accounts', id])); const relationship = useSelector(state => state.getIn(['relationships', id])); @@ -89,7 +89,7 @@ const Card = ({ id, source }) => {
- {firstVerifiedField ? : } + {firstVerifiedField ? : }