From f16908e9b96c4e1c8554d1e2f01faa39dcd456aa Mon Sep 17 00:00:00 2001 From: Monica Cellio Date: Tue, 28 Jul 2026 19:07:21 -0400 Subject: [PATCH 1/6] fixed voting access for restricted users --- app/controllers/votes_controller.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/controllers/votes_controller.rb b/app/controllers/votes_controller.rb index c270bcb1a..8016cdc4f 100644 --- a/app/controllers/votes_controller.rb +++ b/app/controllers/votes_controller.rb @@ -11,6 +11,12 @@ def create return end + if !current_user.privilege?('unrestricted') && !current_user.owns_post_or_parent?(post) + render(json: { status: 'failed', message: 'You must have the Participate Everywhere ability to vote on this post.' }, + status: :forbidden) + return + end + recent_votes = current_user.recent_votes_count max_votes_per_day = current_user.max_votes_per_day From ed002f423af776d9d7f3f5c7c41180c9ed833e15 Mon Sep 17 00:00:00 2001 From: Monica Cellio Date: Tue, 28 Jul 2026 19:54:48 -0400 Subject: [PATCH 2/6] localize strings for voting problems --- app/controllers/votes_controller.rb | 5 +++-- config/locales/strings/en.votes.yml | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/controllers/votes_controller.rb b/app/controllers/votes_controller.rb index 8016cdc4f..90dbab2c1 100644 --- a/app/controllers/votes_controller.rb +++ b/app/controllers/votes_controller.rb @@ -7,12 +7,13 @@ def create post = Post.find(params[:post_id]) if post.user == current_user && !SiteSetting['AllowSelfVotes'] - render(json: { status: 'failed', message: 'You may not vote on your own posts.' }, status: :forbidden) + render(json: { status: 'failed', message: I18n.t('votes.limits.own_post') }, + status: :forbidden) return end if !current_user.privilege?('unrestricted') && !current_user.owns_post_or_parent?(post) - render(json: { status: 'failed', message: 'You must have the Participate Everywhere ability to vote on this post.' }, + render(json: { status: 'failed', message: I18n.t('votes.limits.restricted_ability') }, status: :forbidden) return end diff --git a/config/locales/strings/en.votes.yml b/config/locales/strings/en.votes.yml index 485ef2f6f..4bd316b3e 100644 --- a/config/locales/strings/en.votes.yml +++ b/config/locales/strings/en.votes.yml @@ -1,4 +1,7 @@ en: votes: summary: - post_missing: 'Post not found' \ No newline at end of file + post_missing: 'Post not found' + limits: + own_post: 'You may not vote on your own posts.' + restricted_ability: 'You must have the Participate Everywhere ability to vote on this post.' \ No newline at end of file From 501bc27a220cd2131177827a5a420dd3b7ca06ad Mon Sep 17 00:00:00 2001 From: Oleg Valter Date: Sun, 2 Aug 2026 00:52:22 +0300 Subject: [PATCH 3/6] Add tests for the new restiction --- test/controllers/votes_controller_test.rb | 39 ++++++++++++++++------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/test/controllers/votes_controller_test.rb b/test/controllers/votes_controller_test.rb index aabc1df04..c328c5521 100644 --- a/test/controllers/votes_controller_test.rb +++ b/test/controllers/votes_controller_test.rb @@ -6,7 +6,7 @@ class VotesControllerTest < ActionController::TestCase test 'should cast upvote' do sign_in users(:standard_user) - post :create, params: { post_id: posts(:question_without_votes).id, vote_type: 1 } + try_create_vote posts(:question_without_votes), 1 assert_json_success end @@ -14,29 +14,29 @@ class VotesControllerTest < ActionController::TestCase test 'should cast downvote' do sign_in users(:standard_user) - post :create, params: { post_id: posts(:question_without_votes).id, vote_type: -1 } + try_create_vote posts(:question_without_votes), -1 assert_json_success end test 'should return correct modified status' do - post_id = posts(:question_without_votes).id + post = posts(:question_without_votes) sign_in users(:standard_user) - post :create, params: { post_id: post_id, vote_type: 1 } - post :create, params: { post_id: post_id, vote_type: -1 } + try_create_vote post, 1 + try_create_vote post, 1 assert_json_success(status: 'modified') end test 'should silently accept duplicate votes' do - post_id = posts(:question_without_votes).id + post = posts(:question_without_votes) sign_in users(:standard_user) - post :create, params: { post_id: post_id, vote_type: 1 } - post :create, params: { post_id: post_id, vote_type: 1 } + try_create_vote post, 1 + try_create_vote post, 1 assert_json_success(status: 'modified') end @@ -44,7 +44,7 @@ class VotesControllerTest < ActionController::TestCase test 'should prevent self voting' do sign_in users(:editor) - post :create, params: { post_id: posts(:question_without_votes).id, vote_type: 1 } + try_create_vote posts(:question_without_votes), 1 assert_response(:forbidden) assert_valid_json_response @@ -92,7 +92,7 @@ class VotesControllerTest < ActionController::TestCase test 'should prevent deleted account casting votes' do sign_in users(:deleted_account) - post :create, params: { post_id: posts(:question_without_votes).id, vote_type: 1 } + try_create_vote posts(:question_without_votes), 1 assert_response(:forbidden) assert_valid_json_response @@ -102,10 +102,27 @@ class VotesControllerTest < ActionController::TestCase test 'should prevent deleted profile casting votes' do sign_in users(:deleted_profile) - post :create, params: { post_id: posts(:question_without_votes).id, vote_type: 1 } + try_create_vote posts(:question_without_votes), 1 assert_response(:forbidden) assert_valid_json_response assert_json_response_message('You must be logged in to vote.') end + + test 'should not allow restricted users to vote' do + sign_in users(:basic_user) + + try_create_vote posts(:question_without_votes), 1 + + assert_response(:forbidden) + assert_valid_json_response + assert_json_response_message(I18n.t('votes.limits.restricted_ability')) + end + + private + + def try_create_vote(target_post, vote_type) + post :create, params: { post_id: target_post.id, + vote_type: vote_type } + end end From 719cce080de1abace2f8a61b81659ee3a23b5fb9 Mon Sep 17 00:00:00 2001 From: Monica Cellio Date: Sat, 1 Aug 2026 23:27:09 -0400 Subject: [PATCH 4/6] moved 'can this user vote on this post?' check into user model --- app/controllers/votes_controller.rb | 2 +- app/models/user.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/controllers/votes_controller.rb b/app/controllers/votes_controller.rb index 90dbab2c1..03010de12 100644 --- a/app/controllers/votes_controller.rb +++ b/app/controllers/votes_controller.rb @@ -12,7 +12,7 @@ def create return end - if !current_user.privilege?('unrestricted') && !current_user.owns_post_or_parent?(post) + if !current_user.can_vote_on?(post) render(json: { status: 'failed', message: I18n.t('votes.limits.restricted_ability') }, status: :forbidden) return diff --git a/app/models/user.rb b/app/models/user.rb index 67f7dd5f0..25dd82fdd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -99,6 +99,15 @@ def post_privilege?(name, post) post.user == self || privilege?(name) end + # Can the user vote on this post? + # This post does not check for the 'AllowSelfVotes' site setting, only + # whether this user has the ability to vote on this post. + # @param post [Post] to check + # @return [Boolean] check result + def can_vote_on?(post) + privilege?('unrestricted') || owns_post_or_parent?(post) + end + # Can the user rename a given comment thread? # @param thread [CommentThread] thread to rename # @return [Boolean] check result From 3f59b3c765d2e4851ecc4b53d553f841228a03af Mon Sep 17 00:00:00 2001 From: Monica Cellio Date: Sat, 1 Aug 2026 23:31:40 -0400 Subject: [PATCH 5/6] rubocop --- app/controllers/votes_controller.rb | 2 +- app/models/user.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/votes_controller.rb b/app/controllers/votes_controller.rb index 03010de12..9a171907f 100644 --- a/app/controllers/votes_controller.rb +++ b/app/controllers/votes_controller.rb @@ -12,7 +12,7 @@ def create return end - if !current_user.can_vote_on?(post) + unless current_user.can_vote_on?(post) render(json: { status: 'failed', message: I18n.t('votes.limits.restricted_ability') }, status: :forbidden) return diff --git a/app/models/user.rb b/app/models/user.rb index 25dd82fdd..92d546b4f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -107,7 +107,7 @@ def post_privilege?(name, post) def can_vote_on?(post) privilege?('unrestricted') || owns_post_or_parent?(post) end - + # Can the user rename a given comment thread? # @param thread [CommentThread] thread to rename # @return [Boolean] check result From 0163d5f9726a4c3476f0a25befd4963aa5c8fed9 Mon Sep 17 00:00:00 2001 From: Monica Cellio Date: Sun, 2 Aug 2026 13:37:06 -0400 Subject: [PATCH 6/6] disable rubocop length rule until we can refactor this class --- app/models/user.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 92d546b4f..769d53983 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,4 @@ +# rubocop:disable Metrics/ClassLength class User < ApplicationRecord include EmailValidations include UsernameValidations @@ -560,3 +561,4 @@ def votes_by_post_type votes.joins(:post).group(Arel.sql('posts.post_type_id')).count(Arel.sql('posts.post_type_id')) end end +# rubocop:enable Metrics/ClassLength