From 6cdbc345f4ed824b9491ac6b53406c44e23f7ba5 Mon Sep 17 00:00:00 2001 From: Meisam <39205857+MFTabriz@users.noreply.github.com> Date: Thu, 15 Dec 2022 15:43:05 +0100 Subject: Validate nodeinfo response by schema (#21395) * add json-schema to :test in Gemfile * Create node_info_2.0_schema.json * test match_response_schema * Create match_response_schema.rb * Update nodeinfo_controller_spec.rb * Rename spec/support/node_info_2.0_schema.json to spec/support/schema/node_info_2.0_schema.json * Update match_response_schema.rb * cleanup * additionally validate the json schema itself disable throwing errors test the schema matcher * rename nodeinfo schema to nodeinfo_2.0 * use Rails.root.join to construct the path * prettify json * sync Gemfile.lock --- .../well_known/nodeinfo_controller_spec.rb | 2 + spec/support/matchers/json/match_json_schema.rb | 6 + spec/support/schema/nodeinfo_2.0.json | 170 +++++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 spec/support/matchers/json/match_json_schema.rb create mode 100644 spec/support/schema/nodeinfo_2.0.json (limited to 'spec') diff --git a/spec/controllers/well_known/nodeinfo_controller_spec.rb b/spec/controllers/well_known/nodeinfo_controller_spec.rb index 694bb0fb9..36e85f20d 100644 --- a/spec/controllers/well_known/nodeinfo_controller_spec.rb +++ b/spec/controllers/well_known/nodeinfo_controller_spec.rb @@ -27,6 +27,8 @@ describe WellKnown::NodeInfoController, type: :controller do json = body_as_json + expect({ "foo" => 0 }).not_to match_json_schema("nodeinfo_2.0") + expect(json).to match_json_schema("nodeinfo_2.0") expect(json[:version]).to eq '2.0' expect(json[:usage]).to be_a Hash expect(json[:software]).to be_a Hash diff --git a/spec/support/matchers/json/match_json_schema.rb b/spec/support/matchers/json/match_json_schema.rb new file mode 100644 index 000000000..5d9c9a618 --- /dev/null +++ b/spec/support/matchers/json/match_json_schema.rb @@ -0,0 +1,6 @@ +RSpec::Matchers.define :match_json_schema do |schema| + match do |input_json| + schema_path = Rails.root.join('spec', 'support', 'schema', "#{schema}.json").to_s + JSON::Validator.validate(schema_path, input_json, validate_schema: true) + end +end diff --git a/spec/support/schema/nodeinfo_2.0.json b/spec/support/schema/nodeinfo_2.0.json new file mode 100644 index 000000000..085ce542b --- /dev/null +++ b/spec/support/schema/nodeinfo_2.0.json @@ -0,0 +1,170 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://nodeinfo.diaspora.software/ns/schema/2.0#", + "description": "NodeInfo schema version 2.0.", + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "software", + "protocols", + "services", + "openRegistrations", + "usage", + "metadata" + ], + "properties": { + "version": { + "description": "The schema version, must be 2.0.", + "enum": ["2.0"] + }, + "software": { + "description": "Metadata about server software in use.", + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { + "description": "The canonical name of this server software.", + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "version": { + "description": "The version of this server software.", + "type": "string" + } + } + }, + "protocols": { + "description": "The protocols supported on this server.", + "type": "array", + "minItems": 1, + "items": { + "enum": [ + "activitypub", + "buddycloud", + "dfrn", + "diaspora", + "libertree", + "ostatus", + "pumpio", + "tent", + "xmpp", + "zot" + ] + } + }, + "services": { + "description": "The third party sites this server can connect to via their application API.", + "type": "object", + "additionalProperties": false, + "required": ["inbound", "outbound"], + "properties": { + "inbound": { + "description": "The third party sites this server can retrieve messages from for combined display with regular traffic.", + "type": "array", + "minItems": 0, + "items": { + "enum": [ + "atom1.0", + "gnusocial", + "imap", + "pnut", + "pop3", + "pumpio", + "rss2.0", + "twitter" + ] + } + }, + "outbound": { + "description": "The third party sites this server can publish messages to on the behalf of a user.", + "type": "array", + "minItems": 0, + "items": { + "enum": [ + "atom1.0", + "blogger", + "buddycloud", + "diaspora", + "dreamwidth", + "drupal", + "facebook", + "friendica", + "gnusocial", + "google", + "insanejournal", + "libertree", + "linkedin", + "livejournal", + "mediagoblin", + "myspace", + "pinterest", + "pnut", + "posterous", + "pumpio", + "redmatrix", + "rss2.0", + "smtp", + "tent", + "tumblr", + "twitter", + "wordpress", + "xmpp" + ] + } + } + } + }, + "openRegistrations": { + "description": "Whether this server allows open self-registration.", + "type": "boolean" + }, + "usage": { + "description": "Usage statistics for this server.", + "type": "object", + "additionalProperties": false, + "required": ["users"], + "properties": { + "users": { + "description": "statistics about the users of this server.", + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "description": "The total amount of on this server registered users.", + "type": "integer", + "minimum": 0 + }, + "activeHalfyear": { + "description": "The amount of users that signed in at least once in the last 180 days.", + "type": "integer", + "minimum": 0 + }, + "activeMonth": { + "description": "The amount of users that signed in at least once in the last 30 days.", + "type": "integer", + "minimum": 0 + } + } + }, + "localPosts": { + "description": "The amount of posts that were made by users that are registered on this server.", + "type": "integer", + "minimum": 0 + }, + "localComments": { + "description": "The amount of comments that were made by users that are registered on this server.", + "type": "integer", + "minimum": 0 + } + } + }, + "metadata": { + "description": "Free form key value pairs for software specific values. Clients should not rely on any specific key present.", + "type": "object", + "minProperties": 0, + "additionalProperties": true + } + } +} -- cgit From 5fb1c3e934a1a782972ac2732ce7f0208c341ac2 Mon Sep 17 00:00:00 2001 From: Francis Murillo Date: Thu, 15 Dec 2022 14:47:06 +0000 Subject: Revoke all authorized applications on password reset (#21325) * Clear sessions on password change * Rename User::clear_sessions to revoke_access for a clearer meaning * Add reset paassword controller test * Use User.find instead of User.find_for_authentication for reset password test * Use redirect and render for better test meaning in reset password Co-authored-by: Effy Elden --- app/controllers/auth/passwords_controller.rb | 2 + app/models/user.rb | 16 +++--- spec/controllers/auth/passwords_controller_spec.rb | 61 ++++++++++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) (limited to 'spec') diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index 2996c0431..a8ad66929 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -10,6 +10,8 @@ class Auth::PasswordsController < Devise::PasswordsController super do |resource| if resource.errors.empty? resource.session_activations.destroy_all + + resource.revoke_access! end end end diff --git a/app/models/user.rb b/app/models/user.rb index 5530a9070..ca98a0afa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -377,6 +377,15 @@ class User < ApplicationRecord super end + def revoke_access! + Doorkeeper::AccessGrant.by_resource_owner(self).update_all(revoked_at: Time.now.utc) + + Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch| + batch.update_all(revoked_at: Time.now.utc) + Web::PushSubscription.where(access_token_id: batch).delete_all + end + end + def reset_password! # First, change password to something random and deactivate all sessions transaction do @@ -385,12 +394,7 @@ class User < ApplicationRecord end # Then, remove all authorized applications and connected push subscriptions - Doorkeeper::AccessGrant.by_resource_owner(self).in_batches.update_all(revoked_at: Time.now.utc) - - Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch| - batch.update_all(revoked_at: Time.now.utc) - Web::PushSubscription.where(access_token_id: batch).delete_all - end + revoke_access! # Finally, send a reset password prompt to the user send_reset_password_instructions diff --git a/spec/controllers/auth/passwords_controller_spec.rb b/spec/controllers/auth/passwords_controller_spec.rb index dcfdebb17..1c6874f08 100644 --- a/spec/controllers/auth/passwords_controller_spec.rb +++ b/spec/controllers/auth/passwords_controller_spec.rb @@ -35,4 +35,65 @@ describe Auth::PasswordsController, type: :controller do end end end + + describe 'POST #update' do + let(:user) { Fabricate(:user) } + + before do + @password = 'reset0password' + request.env['devise.mapping'] = Devise.mappings[:user] + end + + context 'with valid reset_password_token' do + let!(:session_activation) { Fabricate(:session_activation, user: user) } + let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) } + let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) } + + before do + @token = user.send_reset_password_instructions + + post :update, params: { user: { password: @password, password_confirmation: @password, reset_password_token: @token } } + end + + it 'redirect to sign in' do + expect(response).to redirect_to '/auth/sign_in' + end + + it 'changes password' do + this_user = User.find(user.id) + + expect(this_user).to_not be_nil + expect(this_user.valid_password?(@password)).to be true + end + + it 'deactivates all sessions' do + expect(user.session_activations.count).to eq 0 + end + + it 'revokes all access tokens' do + expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0 + end + + it 'removes push subscriptions' do + expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0 + end + end + + context 'with invalid reset_password_token' do + before do + post :update, params: { user: { password: @password, password_confirmation: @password, reset_password_token: 'some_invalid_value' } } + end + + it 'renders reset password' do + expect(response).to render_template(:new) + end + + it 'retains password' do + this_user = User.find(user.id) + + expect(this_user).to_not be_nil + expect(this_user.external_or_valid_password?(user.password)).to be true + end + end + end end -- cgit From baecdf28820b267aebe0c9fa92bdc85d929746e7 Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Fri, 16 Dec 2022 00:20:55 +0900 Subject: Fix typo in application_helper_spec.rb (#20981) enviroment -> environment --- spec/helpers/application_helper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 20ee32aa0..1dbd985bf 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -113,7 +113,7 @@ describe ApplicationHelper do Setting.site_title = site_title end - it 'returns site title on production enviroment' do + it 'returns site title on production environment' do Setting.site_title = 'site title' expect(Rails.env).to receive(:production?).and_return(true) expect(helper.title).to eq 'site title' -- cgit From 1f5740e65cc50ef3cc1feb7c0e5609df73d4173a Mon Sep 17 00:00:00 2001 From: Neil Matatall <448516+oreoshake@users.noreply.github.com> Date: Thu, 15 Dec 2022 05:39:41 -1000 Subject: Use Rails tag API to build RSS feed for spoilers and polls (#20163) * Use Rails tag API to build RSS feed for spoilers and polls While the previous method did not contain a bug or a potential issue, the tag API can be very resilient against future problems and reduces the amount of manual management of the escape status of the content. I've added tests to ensure that the formatting is broken and still escapes control characters correctly. * this seems cleaner and passes * Incorporate feedback by moving the br to its own line and using the tag helper over the string constant for the br tag itself * whoops, tag helper doesn't use a self-closing tag --- app/helpers/formatting_helper.rb | 25 +++++++++++++++++-------- spec/helpers/formatting_helper_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 spec/helpers/formatting_helper_spec.rb (limited to 'spec') diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index a9d2f9651..c70931489 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -23,19 +23,28 @@ module FormattingHelper before_html = begin if status.spoiler_text? - "

#{I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale)} #{h(status.spoiler_text)}


" - else - '' + tag.p do + tag.strong do + I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale) + end + + status.spoiler_text + end + tag.hr end - end.html_safe # rubocop:disable Rails/OutputSafety + end after_html = begin if status.preloadable_poll - "

#{status.preloadable_poll.options.map { |o| " #{h(o)}" }.join('
')}

" - else - '' + tag.p do + safe_join( + status.preloadable_poll.options.map do |o| + tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', o, disabled: true) + end, + tag.br + ) + end end - end.html_safe # rubocop:disable Rails/OutputSafety + end prerender_custom_emojis( safe_join([before_html, html, after_html]), diff --git a/spec/helpers/formatting_helper_spec.rb b/spec/helpers/formatting_helper_spec.rb new file mode 100644 index 000000000..af604a87b --- /dev/null +++ b/spec/helpers/formatting_helper_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe FormattingHelper, type: :helper do + include Devise::Test::ControllerHelpers + + describe '#rss_status_content_format' do + let(:status) { Fabricate(:status, text: 'Hello world<>', spoiler_text: 'This is a spoiler<>', poll: Fabricate(:poll, options: %w(Yes<> No))) } + let(:html) { helper.rss_status_content_format(status) } + + it 'renders the spoiler text' do + expect(html).to include('

This is a spoiler<>


') + end + + it 'renders the status text' do + expect(html).to include('

Hello world<>

') + end + + it 'renders the poll' do + expect(html).to include('Yes<>
') + end + end +end -- cgit From d412147d02e84cb76b252706a5357fe5d434c3db Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Fri, 16 Dec 2022 01:11:14 +0900 Subject: Save avatar or header correctly even if other one fails (#18465) * Save avatar or header correctly if other one fails * Fix test --- app/models/account.rb | 12 +++++++++--- spec/models/account_spec.rb | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) (limited to 'spec') diff --git a/app/models/account.rb b/app/models/account.rb index fc7359cfc..a7bda15d3 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -341,9 +341,15 @@ class Account < ApplicationRecord def save_with_optional_media! save! - rescue ActiveRecord::RecordInvalid - self.avatar = nil - self.header = nil + rescue ActiveRecord::RecordInvalid => e + errors = e.record.errors.errors + errors.each do |err| + if err.attribute == :avatar + self.avatar = nil + elsif err.attribute == :header + self.header = nil + end + end save! end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index edae05f9d..c9d782cee 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -160,7 +160,7 @@ RSpec.describe Account, type: :model do expect(account.avatar_remote_url).to eq 'https://remote.test/invalid_avatar' expect(account.header_remote_url).to eq expectation.header_remote_url expect(account.avatar_file_name).to eq nil - expect(account.header_file_name).to eq nil + expect(account.header_file_name).to eq expectation.header_file_name end end end -- cgit From 8556a649d58a7291db6942a2594533f9b8c06165 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 17:45:02 +0100 Subject: Fix changing domain block severity not undoing individual account effects (#22135) * Fix changing domain block severity not undoing individual account effects Fixes #22133 * Add tests --- app/controllers/admin/domain_blocks_controller.rb | 8 +--- .../api/v1/admin/domain_blocks_controller.rb | 6 +-- .../admin/domain_blocks_controller_spec.rb | 47 ++++++++++++++++++++++ .../api/v1/admin/domain_blocks_controller_spec.rb | 47 ++++++++++++++++++++++ 4 files changed, 98 insertions(+), 10 deletions(-) (limited to 'spec') diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index e79f7a43e..74764640b 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -55,12 +55,8 @@ module Admin def update authorize :domain_block, :update? - @domain_block.update(update_params) - - severity_changed = @domain_block.severity_changed? - - if @domain_block.save - DomainBlockWorker.perform_async(@domain_block.id, severity_changed) + if @domain_block.update(update_params) + DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?) log_action :update, @domain_block redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') else diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb index df5b1b3fc..8b77e9717 100644 --- a/app/controllers/api/v1/admin/domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -40,10 +40,8 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController def update authorize @domain_block, :update? - @domain_block.update(domain_block_params) - severity_changed = @domain_block.severity_changed? - @domain_block.save! - DomainBlockWorker.perform_async(@domain_block.id, severity_changed) + @domain_block.update!(domain_block_params) + DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?) log_action :update, @domain_block render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer end diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb index 98cda5004..f432060d9 100644 --- a/spec/controllers/admin/domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/domain_blocks_controller_spec.rb @@ -70,6 +70,53 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do end end + describe 'PUT #update' do + let!(:remote_account) { Fabricate(:account, domain: 'example.com') } + let(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: original_severity) } + + before do + BlockDomainService.new.call(domain_block) + end + + let(:subject) do + post :update, params: { id: domain_block.id, domain_block: { domain: 'example.com', severity: new_severity } } + end + + context 'downgrading a domain suspension to silence' do + let(:original_severity) { 'suspend' } + let(:new_severity) { 'silence' } + + it 'changes the block severity' do + expect { subject }.to change { domain_block.reload.severity }.from('suspend').to('silence') + end + + it 'undoes individual suspensions' do + expect { subject }.to change { remote_account.reload.suspended? }.from(true).to(false) + end + + it 'performs individual silences' do + expect { subject }.to change { remote_account.reload.silenced? }.from(false).to(true) + end + end + + context 'upgrading a domain silence to suspend' do + let(:original_severity) { 'silence' } + let(:new_severity) { 'suspend' } + + it 'changes the block severity' do + expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend') + end + + it 'undoes individual silences' do + expect { subject }.to change { remote_account.reload.silenced? }.from(true).to(false) + end + + it 'performs individual suspends' do + expect { subject }.to change { remote_account.reload.suspended? }.from(false).to(true) + end + end + end + describe 'DELETE #destroy' do it 'unblocks the domain' do service = double(call: true) diff --git a/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb index f12285b2a..606def602 100644 --- a/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb +++ b/spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb @@ -71,6 +71,53 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do end end + describe 'PUT #update' do + let!(:remote_account) { Fabricate(:account, domain: 'example.com') } + let(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: original_severity) } + + before do + BlockDomainService.new.call(domain_block) + end + + let(:subject) do + post :update, params: { id: domain_block.id, domain: 'example.com', severity: new_severity } + end + + context 'downgrading a domain suspension to silence' do + let(:original_severity) { 'suspend' } + let(:new_severity) { 'silence' } + + it 'changes the block severity' do + expect { subject }.to change { domain_block.reload.severity }.from('suspend').to('silence') + end + + it 'undoes individual suspensions' do + expect { subject }.to change { remote_account.reload.suspended? }.from(true).to(false) + end + + it 'performs individual silences' do + expect { subject }.to change { remote_account.reload.silenced? }.from(false).to(true) + end + end + + context 'upgrading a domain silence to suspend' do + let(:original_severity) { 'silence' } + let(:new_severity) { 'suspend' } + + it 'changes the block severity' do + expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend') + end + + it 'undoes individual silences' do + expect { subject }.to change { remote_account.reload.silenced? }.from(true).to(false) + end + + it 'performs individual suspends' do + expect { subject }.to change { remote_account.reload.suspended? }.from(false).to(true) + end + end + end + describe 'DELETE #destroy' do let!(:block) { Fabricate(:domain_block) } -- cgit From 70415714f14e067aba518a105c96475db31fa124 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Dec 2022 18:50:11 +0100 Subject: Add follow request banner on account header (#20785) * Add requested_by to relationship maps * Display whether an account has requested to follow you on their profile --- .../account/components/follow_request_note.js | 37 ++++++++++++++++++++++ .../mastodon/features/account/components/header.js | 3 ++ .../containers/follow_request_note_container.js | 15 +++++++++ app/javascript/mastodon/reducers/relationships.js | 11 +++++++ app/javascript/styles/mastodon/components.scss | 32 ++++++++++++++++++- app/models/concerns/account_interactions.rb | 4 +++ app/presenters/account_relationships_presenter.rb | 6 +++- app/serializers/rest/relationship_serializer.rb | 8 +++-- spec/models/account_spec.rb | 6 ++++ .../account_relationships_presenter_spec.rb | 9 ++++++ 10 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 app/javascript/mastodon/features/account/components/follow_request_note.js create mode 100644 app/javascript/mastodon/features/account/containers/follow_request_note_container.js (limited to 'spec') diff --git a/app/javascript/mastodon/features/account/components/follow_request_note.js b/app/javascript/mastodon/features/account/components/follow_request_note.js new file mode 100644 index 000000000..300ae4266 --- /dev/null +++ b/app/javascript/mastodon/features/account/components/follow_request_note.js @@ -0,0 +1,37 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import Icon from 'mastodon/components/icon'; + +export default class FollowRequestNote extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + }; + + render () { + const { account, onAuthorize, onReject } = this.props; + + return ( +
+
+ }} /> +
+ +
+ + + +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index f117412be..dddbf4dd4 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -14,6 +14,7 @@ import ShortNumber from 'mastodon/components/short_number'; import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import AccountNoteContainer from '../containers/account_note_container'; +import FollowRequestNoteContainer from '../containers/follow_request_note_container'; import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions'; import { Helmet } from 'react-helmet'; @@ -311,6 +312,8 @@ class Header extends ImmutablePureComponent { return (
+ {!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && } +
{!suspended && info} diff --git a/app/javascript/mastodon/features/account/containers/follow_request_note_container.js b/app/javascript/mastodon/features/account/containers/follow_request_note_container.js new file mode 100644 index 000000000..c33c3de59 --- /dev/null +++ b/app/javascript/mastodon/features/account/containers/follow_request_note_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import FollowRequestNote from '../components/follow_request_note'; +import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts'; + +const mapDispatchToProps = (dispatch, { account }) => ({ + onAuthorize () { + dispatch(authorizeFollowRequest(account.get('id'))); + }, + + onReject () { + dispatch(rejectFollowRequest(account.get('id'))); + }, +}); + +export default connect(null, mapDispatchToProps)(FollowRequestNote); diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js index 53949258a..850ece351 100644 --- a/app/javascript/mastodon/reducers/relationships.js +++ b/app/javascript/mastodon/reducers/relationships.js @@ -1,3 +1,6 @@ +import { + NOTIFICATIONS_UPDATE, +} from '../actions/notifications'; import { ACCOUNT_FOLLOW_SUCCESS, ACCOUNT_FOLLOW_REQUEST, @@ -12,6 +15,8 @@ import { ACCOUNT_PIN_SUCCESS, ACCOUNT_UNPIN_SUCCESS, RELATIONSHIPS_FETCH_SUCCESS, + FOLLOW_REQUEST_AUTHORIZE_SUCCESS, + FOLLOW_REQUEST_REJECT_SUCCESS, } from '../actions/accounts'; import { DOMAIN_BLOCK_SUCCESS, @@ -44,6 +49,12 @@ const initialState = ImmutableMap(); export default function relationships(state = initialState, action) { switch(action.type) { + case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: + return state.setIn([action.id, 'followed_by'], true).setIn([action.id, 'requested_by'], false); + case FOLLOW_REQUEST_REJECT_SUCCESS: + return state.setIn([action.id, 'followed_by'], false).setIn([action.id, 'requested_by'], false); + case NOTIFICATIONS_UPDATE: + return action.notification.type === 'follow_request' ? state.setIn([action.notification.account.id, 'requested_by'], true) : state; case ACCOUNT_FOLLOW_REQUEST: return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true); case ACCOUNT_FOLLOW_FAIL: diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 15fc6aa69..6a22f6009 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -166,6 +166,30 @@ &:disabled { opacity: 0.5; } + + &.button--confirmation { + color: $valid-value-color; + border-color: $valid-value-color; + + &:active, + &:focus, + &:hover { + background: $valid-value-color; + color: $primary-text-color; + } + } + + &.button--destructive { + color: $error-value-color; + border-color: $error-value-color; + + &:active, + &:focus, + &:hover { + background: $error-value-color; + color: $primary-text-color; + } + } } &.button--block { @@ -6722,7 +6746,8 @@ noscript { } } -.moved-account-banner { +.moved-account-banner, +.follow-request-banner { padding: 20px; background: lighten($ui-base-color, 4%); display: flex; @@ -6745,6 +6770,7 @@ noscript { justify-content: space-between; align-items: center; gap: 15px; + width: 100%; } .detailed-status__display-name { @@ -6752,6 +6778,10 @@ noscript { } } +.follow-request-banner .button { + width: 100%; +} + .column-inline-form { padding: 15px; display: flex; diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 15c49f2fe..de8bf338f 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -44,6 +44,10 @@ module AccountInteractions end end + def requested_by_map(target_account_ids, account_id) + follow_mapping(FollowRequest.where(account_id: target_account_ids, target_account_id: account_id), :account_id) + end + def endorsed_map(target_account_ids, account_id) follow_mapping(AccountPin.where(account_id: account_id, target_account_id: target_account_ids), :target_account_id) end diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb index d662380f6..ab8bac412 100644 --- a/app/presenters/account_relationships_presenter.rb +++ b/app/presenters/account_relationships_presenter.rb @@ -2,7 +2,7 @@ class AccountRelationshipsPresenter attr_reader :following, :followed_by, :blocking, :blocked_by, - :muting, :requested, :domain_blocking, + :muting, :requested, :requested_by, :domain_blocking, :endorsed, :account_note def initialize(account_ids, current_account_id, **options) @@ -15,6 +15,7 @@ class AccountRelationshipsPresenter @blocked_by = cached[:blocked_by].merge(Account.blocked_by_map(@uncached_account_ids, @current_account_id)) @muting = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id)) @requested = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id)) + @requested_by = cached[:requested_by].merge(Account.requested_by_map(@uncached_account_ids, @current_account_id)) @domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id)) @endorsed = cached[:endorsed].merge(Account.endorsed_map(@uncached_account_ids, @current_account_id)) @account_note = cached[:account_note].merge(Account.account_note_map(@uncached_account_ids, @current_account_id)) @@ -27,6 +28,7 @@ class AccountRelationshipsPresenter @blocked_by.merge!(options[:blocked_by_map] || {}) @muting.merge!(options[:muting_map] || {}) @requested.merge!(options[:requested_map] || {}) + @requested_by.merge!(options[:requested_by_map] || {}) @domain_blocking.merge!(options[:domain_blocking_map] || {}) @endorsed.merge!(options[:endorsed_map] || {}) @account_note.merge!(options[:account_note_map] || {}) @@ -44,6 +46,7 @@ class AccountRelationshipsPresenter blocked_by: {}, muting: {}, requested: {}, + requested_by: {}, domain_blocking: {}, endorsed: {}, account_note: {}, @@ -73,6 +76,7 @@ class AccountRelationshipsPresenter blocked_by: { account_id => blocked_by[account_id] }, muting: { account_id => muting[account_id] }, requested: { account_id => requested[account_id] }, + requested_by: { account_id => requested_by[account_id] }, domain_blocking: { account_id => domain_blocking[account_id] }, endorsed: { account_id => endorsed[account_id] }, account_note: { account_id => account_note[account_id] }, diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb index 31fc60eb2..b53387401 100644 --- a/app/serializers/rest/relationship_serializer.rb +++ b/app/serializers/rest/relationship_serializer.rb @@ -2,8 +2,8 @@ class REST::RelationshipSerializer < ActiveModel::Serializer attributes :id, :following, :showing_reblogs, :notifying, :languages, :followed_by, - :blocking, :blocked_by, :muting, :muting_notifications, :requested, - :domain_blocking, :endorsed, :note + :blocking, :blocked_by, :muting, :muting_notifications, + :requested, :requested_by, :domain_blocking, :endorsed, :note def id object.id.to_s @@ -54,6 +54,10 @@ class REST::RelationshipSerializer < ActiveModel::Serializer instance_options[:relationships].requested[object.id] ? true : false end + def requested_by + instance_options[:relationships].requested_by[object.id] ? true : false + end + def domain_blocking instance_options[:relationships].domain_blocking[object.id] || false end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index c9d782cee..6cd769dc8 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -658,6 +658,12 @@ RSpec.describe Account, type: :model do end end + describe '.requested_by_map' do + it 'returns an hash' do + expect(Account.requested_by_map([], 1)).to be_a Hash + end + end + describe 'MENTION_RE' do subject { Account::MENTION_RE } diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb index edfbbb354..8a485d2b9 100644 --- a/spec/presenters/account_relationships_presenter_spec.rb +++ b/spec/presenters/account_relationships_presenter_spec.rb @@ -10,6 +10,7 @@ RSpec.describe AccountRelationshipsPresenter do allow(Account).to receive(:blocking_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:muting_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:requested_map).with(account_ids, current_account_id).and_return(default_map) + allow(Account).to receive(:requested_by_map).with(account_ids, current_account_id).and_return(default_map) allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map) end @@ -71,6 +72,14 @@ RSpec.describe AccountRelationshipsPresenter do end end + context 'options[:requested_by_map] is set' do + let(:options) { { requested_by_map: { 6 => true } } } + + it 'sets @requested merged with default_map and options[:requested_by_map]' do + expect(presenter.requested_by).to eq default_map.merge(options[:requested_by_map]) + end + end + context 'options[:domain_blocking_map] is set' do let(:options) { { domain_blocking_map: { 7 => true } } } -- cgit From 115ab2869b2742f0cc68116a8c03359d220fd608 Mon Sep 17 00:00:00 2001 From: Partho Ghosh Date: Tue, 3 Jan 2023 17:12:48 -0800 Subject: Fix ・ detection in hashtag regex to construct hashtag correctly (#22888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix ・ detection in hashtag regex to construct hashtag correctly * Fixed rubocop liniting issues * More rubocop linting fix --- app/models/tag.rb | 22 ++++++++++++++----- spec/models/tag_spec.rb | 57 +++++++++++++++++++++++++++---------------------- 2 files changed, 49 insertions(+), 30 deletions(-) (limited to 'spec') diff --git a/app/models/tag.rb b/app/models/tag.rb index b66f85423..47a05d00a 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -26,8 +26,12 @@ class Tag < ApplicationRecord has_many :featured_tags, dependent: :destroy, inverse_of: :tag has_many :followers, through: :passive_relationships, source: :account - HASHTAG_SEPARATORS = "_\u00B7\u200c" - HASHTAG_NAME_PAT = "([[:word:]_][[:word:]#{HASHTAG_SEPARATORS}]*[[:alpha:]#{HASHTAG_SEPARATORS}][[:word:]#{HASHTAG_SEPARATORS}]*[[:word:]_])|([[:word:]_]*[[:alpha:]][[:word:]_]*)" + HASHTAG_SEPARATORS = "_\u00B7\u30FB\u200c" + HASHTAG_FIRST_SEQUENCE_CHUNK_ONE = "[[:word:]_][[:word:]#{HASHTAG_SEPARATORS}]*[[:alpha:]#{HASHTAG_SEPARATORS}]" + HASHTAG_FIRST_SEQUENCE_CHUNK_TWO = "[[:word:]#{HASHTAG_SEPARATORS}]*[[:word:]_]" + HASHTAG_FIRST_SEQUENCE = "(#{HASHTAG_FIRST_SEQUENCE_CHUNK_ONE}#{HASHTAG_FIRST_SEQUENCE_CHUNK_TWO})" + HASTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)' + HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASTAG_LAST_SEQUENCE}" HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_PAT})/i HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i @@ -45,7 +49,11 @@ class Tag < ApplicationRecord scope :listable, -> { where(listable: [true, nil]) } scope :trendable, -> { Setting.trendable_by_default ? where(trendable: [true, nil]) : where(trendable: true) } scope :not_trendable, -> { where(trendable: false) } - scope :recently_used, ->(account) { joins(:statuses).where(statuses: { id: account.statuses.select(:id).limit(1000) }).group(:id).order(Arel.sql('count(*) desc')) } + scope :recently_used, ->(account) { + joins(:statuses) + .where(statuses: { id: account.statuses.select(:id).limit(1000) }) + .group(:id).order(Arel.sql('count(*) desc')) + } scope :matches_name, ->(term) { where(arel_table[:name].lower.matches(arel_table.lower("#{sanitize_sql_like(Tag.normalize(term))}%"), nil, true)) } # Search with case-sensitive to use B-tree index update_index('tags', :self) @@ -105,7 +113,8 @@ class Tag < ApplicationRecord names = Array(name_or_names).map { |str| [normalize(str), str] }.uniq(&:first) names.map do |(normalized_name, display_name)| - tag = matching_name(normalized_name).first || create(name: normalized_name, display_name: display_name.gsub(HASHTAG_INVALID_CHARS_RE, '')) + tag = matching_name(normalized_name).first || create(name: normalized_name, + display_name: display_name.gsub(HASHTAG_INVALID_CHARS_RE, '')) yield tag if block_given? @@ -154,6 +163,9 @@ class Tag < ApplicationRecord end def validate_display_name_change - errors.add(:display_name, I18n.t('tags.does_not_match_previous_name')) unless HashtagNormalizer.new.normalize(display_name).casecmp(name.mb_chars).zero? + unless HashtagNormalizer.new.normalize(display_name).casecmp(name.mb_chars).zero? + errors.add(:display_name, + I18n.t('tags.does_not_match_previous_name')) + end end end diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index b16f99a79..102d2f625 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -1,21 +1,22 @@ +# frozen_string_literal: true require 'rails_helper' -RSpec.describe Tag, type: :model do +RSpec.describe Tag do describe 'validations' do it 'invalid with #' do - expect(Tag.new(name: '#hello_world')).to_not be_valid + expect(described_class.new(name: '#hello_world')).not_to be_valid end it 'invalid with .' do - expect(Tag.new(name: '.abcdef123')).to_not be_valid + expect(described_class.new(name: '.abcdef123')).not_to be_valid end it 'invalid with spaces' do - expect(Tag.new(name: 'hello world')).to_not be_valid + expect(described_class.new(name: 'hello world')).not_to be_valid end it 'valid with aesthetic' do - expect(Tag.new(name: 'aesthetic')).to be_valid + expect(described_class.new(name: 'aesthetic')).to be_valid end end @@ -62,6 +63,10 @@ RSpec.describe Tag, type: :model do expect(subject.match('hello #one·two·three').to_s).to eq ' #one·two·three' end + it 'matches ・unicode in ぼっち・ざ・ろっく correctly' do + expect(subject.match('testing #ぼっち・ざ・ろっく').to_s).to eq ' #ぼっち・ざ・ろっく' + end + it 'matches ZWNJ' do expect(subject.match('just add #نرم‌افزار and').to_s).to eq ' #نرم‌افزار' end @@ -89,44 +94,46 @@ RSpec.describe Tag, type: :model do describe '.find_normalized' do it 'returns tag for a multibyte case-insensitive name' do upcase_string = 'abcABCabcABCやゆよ' - downcase_string = 'abcabcabcabcやゆよ'; + downcase_string = 'abcabcabcabcやゆよ' tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string)) - expect(Tag.find_normalized(upcase_string)).to eq tag + expect(described_class.find_normalized(upcase_string)).to eq tag end end describe '.matches_name' do it 'returns tags for multibyte case-insensitive names' do upcase_string = 'abcABCabcABCやゆよ' - downcase_string = 'abcabcabcabcやゆよ'; + downcase_string = 'abcabcabcabcやゆよ' tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string)) - expect(Tag.matches_name(upcase_string)).to eq [tag] + expect(described_class.matches_name(upcase_string)).to eq [tag] end it 'uses the LIKE operator' do - expect(Tag.matches_name('100%abc').to_sql).to eq %q[SELECT "tags".* FROM "tags" WHERE LOWER("tags"."name") LIKE LOWER('100abc%')] + result = %q[SELECT "tags".* FROM "tags" WHERE LOWER("tags"."name") LIKE LOWER('100abc%')] + expect(described_class.matches_name('100%abc').to_sql).to eq result end end describe '.matching_name' do it 'returns tags for multibyte case-insensitive names' do upcase_string = 'abcABCabcABCやゆよ' - downcase_string = 'abcabcabcabcやゆよ'; + downcase_string = 'abcabcabcabcやゆよ' tag = Fabricate(:tag, name: HashtagNormalizer.new.normalize(downcase_string)) - expect(Tag.matching_name(upcase_string)).to eq [tag] + expect(described_class.matching_name(upcase_string)).to eq [tag] end end describe '.find_or_create_by_names' do + let(:upcase_string) { 'abcABCabcABCやゆよ' } + let(:downcase_string) { 'abcabcabcabcやゆよ' } + it 'runs a passed block once per tag regardless of duplicates' do - upcase_string = 'abcABCabcABCやゆよ' - downcase_string = 'abcabcabcabcやゆよ'; - count = 0 + count = 0 - Tag.find_or_create_by_names([upcase_string, downcase_string]) do |tag| + described_class.find_or_create_by_names([upcase_string, downcase_string]) do |_tag| count += 1 end @@ -136,28 +143,28 @@ RSpec.describe Tag, type: :model do describe '.search_for' do it 'finds tag records with matching names' do - tag = Fabricate(:tag, name: "match") - _miss_tag = Fabricate(:tag, name: "miss") + tag = Fabricate(:tag, name: 'match') + _miss_tag = Fabricate(:tag, name: 'miss') - results = Tag.search_for("match") + results = described_class.search_for('match') expect(results).to eq [tag] end it 'finds tag records in case insensitive' do - tag = Fabricate(:tag, name: "MATCH") - _miss_tag = Fabricate(:tag, name: "miss") + tag = Fabricate(:tag, name: 'MATCH') + _miss_tag = Fabricate(:tag, name: 'miss') - results = Tag.search_for("match") + results = described_class.search_for('match') expect(results).to eq [tag] end it 'finds the exact matching tag as the first item' do - similar_tag = Fabricate(:tag, name: "matchlater", reviewed_at: Time.now.utc) - tag = Fabricate(:tag, name: "match", reviewed_at: Time.now.utc) + similar_tag = Fabricate(:tag, name: 'matchlater', reviewed_at: Time.now.utc) + tag = Fabricate(:tag, name: 'match', reviewed_at: Time.now.utc) - results = Tag.search_for("match") + results = described_class.search_for('match') expect(results).to eq [tag, similar_tag] end -- cgit From fdd1facba16db75e425c02807323eb2666688652 Mon Sep 17 00:00:00 2001 From: Jeong Arm Date: Thu, 5 Jan 2023 21:30:38 +0900 Subject: Fix home TL could contain post from who blocked me (#22849) * Fix home tl contains post from who blocked me * Add test * Fix feed_manager's build_crutches blocked_by was not includes status' owner * Add test for status from I blocked * Fix typo --- app/lib/feed_manager.rb | 3 ++- spec/lib/feed_manager_spec.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) (limited to 'spec') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 510667558..b9c5bc2cd 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -365,6 +365,7 @@ class FeedManager end return true if check_for_blocks.any? { |target_account_id| crutches[:blocking][target_account_id] || crutches[:muting][target_account_id] } + return true if crutches[:blocked_by][status.account_id] if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply should_filter = !crutches[:following][status.in_reply_to_account_id] # and I'm not following the person it's a reply to @@ -548,7 +549,7 @@ class FeedManager crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true) - crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| s.reblog&.account_id }.compact).pluck(:account_id).index_with(true) + crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true) crutches end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 0f3b05e5a..eb55c3983 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -39,6 +39,18 @@ RSpec.describe FeedManager do expect(FeedManager.instance.filter?(:home, reblog, bob)).to be false end + it 'returns true for post from account who blocked me' do + status = Fabricate(:status, text: 'Hello, World', account: alice) + alice.block!(bob) + expect(FeedManager.instance.filter?(:home, status, bob)).to be true + end + + it 'returns true for post from blocked account' do + status = Fabricate(:status, text: 'Hello, World', account: alice) + bob.block!(alice) + expect(FeedManager.instance.filter?(:home, status, bob)).to be true + end + it 'returns true for reblog by followee of blocked account' do status = Fabricate(:status, text: 'Hello world', account: jeff) reblog = Fabricate(:status, reblog: status, account: alice) -- cgit From 18fb01ef7ca3829b07bb8ea101bdd073da72cfbc Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 5 Jan 2023 13:47:21 +0100 Subject: Fix possible race conditions when suspending/unsuspending accounts (#22363) * Fix possible race conditions when suspending/unsuspending accounts * Fix tests Tests were assuming SuspensionWorker and UnsuspensionWorker would do the suspending/unsuspending themselves, but this has changed. --- app/services/suspend_account_service.rb | 9 ++++----- app/services/unsuspend_account_service.rb | 8 +++----- spec/services/suspend_account_service_spec.rb | 6 ++++-- spec/services/unsuspend_account_service_spec.rb | 14 +++++++------- 4 files changed, 18 insertions(+), 19 deletions(-) (limited to 'spec') diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 6856c2c51..211544fea 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -3,10 +3,13 @@ class SuspendAccountService < BaseService include Payloadable + # Carry out the suspension of a recently-suspended account + # @param [Account] account Account to suspend def call(account) + return unless account.suspended? + @account = account - suspend! reject_remote_follows! distribute_update_actor! unmerge_from_home_timelines! @@ -16,10 +19,6 @@ class SuspendAccountService < BaseService private - def suspend! - @account.suspend! unless @account.suspended? - end - def reject_remote_follows! return if @account.local? || !@account.activitypub? diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb index 534203dce..70667308e 100644 --- a/app/services/unsuspend_account_service.rb +++ b/app/services/unsuspend_account_service.rb @@ -2,10 +2,12 @@ class UnsuspendAccountService < BaseService include Payloadable + + # Restores a recently-unsuspended account + # @param [Account] account Account to restore def call(account) @account = account - unsuspend! refresh_remote_account! return if @account.nil? || @account.suspended? @@ -18,10 +20,6 @@ class UnsuspendAccountService < BaseService private - def unsuspend! - @account.unsuspend! if @account.suspended? - end - def refresh_remote_account! return if @account.local? diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb index 5d45e4ffd..126b13986 100644 --- a/spec/services/suspend_account_service_spec.rb +++ b/spec/services/suspend_account_service_spec.rb @@ -13,6 +13,8 @@ RSpec.describe SuspendAccountService, type: :service do local_follower.follow!(account) list.accounts << account + + account.suspend! end it "unmerges from local followers' feeds" do @@ -21,8 +23,8 @@ RSpec.describe SuspendAccountService, type: :service do expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list) end - it 'marks account as suspended' do - expect { subject }.to change { account.suspended? }.from(false).to(true) + it 'does not change the “suspended” flag' do + expect { subject }.to_not change { account.suspended? } end end diff --git a/spec/services/unsuspend_account_service_spec.rb b/spec/services/unsuspend_account_service_spec.rb index 3ac4cc085..987eb09e2 100644 --- a/spec/services/unsuspend_account_service_spec.rb +++ b/spec/services/unsuspend_account_service_spec.rb @@ -14,7 +14,7 @@ RSpec.describe UnsuspendAccountService, type: :service do local_follower.follow!(account) list.accounts << account - account.suspend!(origin: :local) + account.unsuspend! end end @@ -30,8 +30,8 @@ RSpec.describe UnsuspendAccountService, type: :service do stub_request(:post, 'https://bob.com/inbox').to_return(status: 201) end - it 'marks account as unsuspended' do - expect { subject }.to change { account.suspended? }.from(true).to(false) + it 'does not change the “suspended” flag' do + expect { subject }.to_not change { account.suspended? } end include_examples 'common behavior' do @@ -83,8 +83,8 @@ RSpec.describe UnsuspendAccountService, type: :service do expect(FeedManager.instance).to have_received(:merge_into_list).with(account, list) end - it 'marks account as unsuspended' do - expect { subject }.to change { account.suspended? }.from(true).to(false) + it 'does not change the “suspended” flag' do + expect { subject }.to_not change { account.suspended? } end end @@ -107,8 +107,8 @@ RSpec.describe UnsuspendAccountService, type: :service do expect(FeedManager.instance).to_not have_received(:merge_into_list).with(account, list) end - it 'does not mark the account as unsuspended' do - expect { subject }.not_to change { account.suspended? } + it 'marks account as suspended' do + expect { subject }.to change { account.suspended? }.from(false).to(true) end end -- cgit From 0c689b9d014324aba5b8751dacec4c0fc20b2038 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 11 Jan 2023 21:59:13 +0100 Subject: fix: allow verification when page size exceeds 1MB (using HTML5 parser) (#22879) * fix: allow verification when page size exceeds 1MB Truncates the page after 1MB instead Closes #15316 * switch to HTML5 parser, fix rubocop errors * undo rubocop fixes Co-authored-by: Chris Zubak-Skees --- app/lib/request.rb | 14 ++++++++++---- app/services/verify_link_service.rb | 2 +- spec/lib/request_spec.rb | 5 +++++ spec/services/verify_link_service_spec.rb | 27 +++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 5 deletions(-) (limited to 'spec') diff --git a/app/lib/request.rb b/app/lib/request.rb index b2819c8ed..0508169dc 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -154,9 +154,7 @@ class Request end module ClientLimit - def body_with_limit(limit = 1.megabyte) - raise Mastodon::LengthValidationError if content_length.present? && content_length > limit - + def truncated_body(limit = 1.megabyte) if charset.nil? encoding = Encoding::BINARY else @@ -173,11 +171,19 @@ class Request contents << chunk chunk.clear - raise Mastodon::LengthValidationError if contents.bytesize > limit + break if contents.bytesize > limit end contents end + + def body_with_limit(limit = 1.megabyte) + raise Mastodon::LengthValidationError if content_length.present? && content_length > limit + + contents = truncated_body(limit) + raise Mastodon::LengthValidationError if contents.bytesize > limit + contents + end end if ::HTTP::Response.methods.include?(:body_with_limit) && !Rails.env.production? diff --git a/app/services/verify_link_service.rb b/app/services/verify_link_service.rb index 7496fe2d5..d049b52d1 100644 --- a/app/services/verify_link_service.rb +++ b/app/services/verify_link_service.rb @@ -26,7 +26,7 @@ class VerifyLinkService < BaseService def link_back_present? return false if @body.blank? - links = Nokogiri::HTML(@body).xpath('//a[contains(concat(" ", normalize-space(@rel), " "), " me ")]|//link[contains(concat(" ", normalize-space(@rel), " "), " me ")]') + links = Nokogiri::HTML5(@body).xpath('//a[contains(concat(" ", normalize-space(@rel), " "), " me ")]|//link[contains(concat(" ", normalize-space(@rel), " "), " me ")]') if links.any? { |link| link['href']&.downcase == @link_back.downcase } true diff --git a/spec/lib/request_spec.rb b/spec/lib/request_spec.rb index 5eccf3201..8539944e2 100644 --- a/spec/lib/request_spec.rb +++ b/spec/lib/request_spec.rb @@ -120,6 +120,11 @@ describe Request do expect { subject.perform { |response| response.body_with_limit } }.to raise_error Mastodon::LengthValidationError end + it 'truncates large monolithic body' do + stub_request(:any, 'http://example.com').to_return(body: SecureRandom.random_bytes(2.megabytes), headers: { 'Content-Length' => 2.megabytes }) + expect(subject.perform { |response| response.truncated_body.bytesize }).to be < 2.megabytes + end + it 'uses binary encoding if Content-Type does not tell encoding' do stub_request(:any, 'http://example.com').to_return(body: '', headers: { 'Content-Type' => 'text/html' }) expect(subject.perform { |response| response.body_with_limit.encoding }).to eq Encoding::BINARY diff --git a/spec/services/verify_link_service_spec.rb b/spec/services/verify_link_service_spec.rb index 52ba454cc..391560f1c 100644 --- a/spec/services/verify_link_service_spec.rb +++ b/spec/services/verify_link_service_spec.rb @@ -73,6 +73,33 @@ RSpec.describe VerifyLinkService, type: :service do end end + context 'when a document is truncated but the link back is valid' do + let(:html) do + " + + + + + Date: Wed, 11 Jan 2023 22:21:10 +0100 Subject: Fix sanitizer parsing link text as HTML when stripping unsupported links (#22558) --- lib/sanitize_ext/sanitize_config.rb | 2 +- spec/lib/sanitize_config_spec.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'spec') diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb index a2e1d9d01..baf652662 100644 --- a/lib/sanitize_ext/sanitize_config.rb +++ b/lib/sanitize_ext/sanitize_config.rb @@ -49,7 +49,7 @@ class Sanitize end end - current_node.replace(current_node.text) unless LINK_PROTOCOLS.include?(scheme) + current_node.replace(Nokogiri::XML::Text.new(current_node.text, current_node.document)) unless LINK_PROTOCOLS.include?(scheme) end UNSUPPORTED_ELEMENTS_TRANSFORMER = lambda do |env| diff --git a/spec/lib/sanitize_config_spec.rb b/spec/lib/sanitize_config_spec.rb index 747d81158..c9543ceb0 100644 --- a/spec/lib/sanitize_config_spec.rb +++ b/spec/lib/sanitize_config_spec.rb @@ -38,6 +38,10 @@ describe Sanitize::Config do expect(Sanitize.fragment('Test', subject)).to eq 'Test' end + it 'does not re-interpret HTML when removing unsupported links' do + expect(Sanitize.fragment('Test<a href="https://example.com">test</a>', subject)).to eq 'Test<a href="https://example.com">test</a>' + end + it 'keeps a with href' do expect(Sanitize.fragment('Test', subject)).to eq 'Test' end -- cgit From 21a1a8ee887f82cb36b3d21011a0235e7bfc8e45 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 13 Jan 2023 10:46:52 +0100 Subject: Fix crash when marking statuses as sensitive while some statuses are deleted (#22134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Do not offer to mark statuses as sensitive if there is no undeleted status with media attachments * Fix crash when marking statuses as sensitive while some statuses are deleted Fixes #21910 * Fix multiple strikes being created for a single report when selecting “Mark as sensitive” * Add tests --- app/models/admin/status_batch_action.rb | 16 ++++----- app/views/admin/reports/_actions.html.haml | 2 +- .../admin/reports/actions_controller_spec.rb | 42 ++++++++++++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 spec/controllers/admin/reports/actions_controller_spec.rb (limited to 'spec') diff --git a/app/models/admin/status_batch_action.rb b/app/models/admin/status_batch_action.rb index 0f019b854..39cd7d0eb 100644 --- a/app/models/admin/status_batch_action.rb +++ b/app/models/admin/status_batch_action.rb @@ -73,7 +73,7 @@ class Admin::StatusBatchAction # Can't use a transaction here because UpdateStatusService queues # Sidekiq jobs statuses.includes(:media_attachments, :preview_cards).find_each do |status| - next unless status.with_media? || status.with_preview_card? + next if status.discarded? || !(status.with_media? || status.with_preview_card?) authorize([:admin, status], :update?) @@ -89,15 +89,15 @@ class Admin::StatusBatchAction report.resolve!(current_account) log_action(:resolve, report) end - - @warning = target_account.strikes.create!( - action: :mark_statuses_as_sensitive, - account: current_account, - report: report, - status_ids: status_ids - ) end + @warning = target_account.strikes.create!( + action: :mark_statuses_as_sensitive, + account: current_account, + report: report, + status_ids: status_ids + ) + UserMailer.warning(target_account.user, @warning).deliver_later! if warnable? end diff --git a/app/views/admin/reports/_actions.html.haml b/app/views/admin/reports/_actions.html.haml index 404d53a77..486eb486c 100644 --- a/app/views/admin/reports/_actions.html.haml +++ b/app/views/admin/reports/_actions.html.haml @@ -5,7 +5,7 @@ = link_to t('admin.reports.mark_as_resolved'), resolve_admin_report_path(@report), method: :post, class: 'button' .report-actions__item__description = t('admin.reports.actions.resolve_description_html') - - if @statuses.any? { |status| status.with_media? || status.with_preview_card? } + - if @statuses.any? { |status| (status.with_media? || status.with_preview_card?) && !status.discarded? } .report-actions__item .report-actions__item__button = button_tag t('admin.reports.mark_as_sensitive'), name: :mark_as_sensitive, class: 'button' diff --git a/spec/controllers/admin/reports/actions_controller_spec.rb b/spec/controllers/admin/reports/actions_controller_spec.rb new file mode 100644 index 000000000..6609798dc --- /dev/null +++ b/spec/controllers/admin/reports/actions_controller_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +describe Admin::Reports::ActionsController do + render_views + + let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) } + let(:account) { Fabricate(:account) } + let!(:status) { Fabricate(:status, account: account) } + let(:media_attached_status) { Fabricate(:status, account: account) } + let!(:media_attachment) { Fabricate(:media_attachment, account: account, status: media_attached_status) } + let(:media_attached_deleted_status) { Fabricate(:status, account: account, deleted_at: 1.day.ago) } + let!(:media_attachment2) { Fabricate(:media_attachment, account: account, status: media_attached_deleted_status) } + let(:last_media_attached_status) { Fabricate(:status, account: account) } + let!(:last_media_attachment) { Fabricate(:media_attachment, account: account, status: last_media_attached_status) } + let!(:last_status) { Fabricate(:status, account: account) } + + before do + sign_in user, scope: :user + end + + describe 'POST #create' do + let(:report) { Fabricate(:report, status_ids: status_ids, account: user.account, target_account: account) } + let(:status_ids) { [media_attached_status.id, media_attached_deleted_status.id] } + + before do + post :create, params: { report_id: report.id, action => '' } + end + + context 'when action is mark_as_sensitive' do + + let(:action) { 'mark_as_sensitive' } + + it 'resolves the report' do + expect(report.reload.action_taken_at).to_not be_nil + end + + it 'marks the non-deleted as sensitive' do + expect(media_attached_status.reload.sensitive).to eq true + end + end + end +end -- cgit