From 0cf5ba9708f48757a4a4bbc65d3176b2a04d2411 Mon Sep 17 00:00:00 2001 From: Pablo Brasero Date: Wed, 14 Jan 2026 16:16:16 +0000 Subject: [PATCH] Block creation of notes within moderation zones --- app/controllers/api/notes_controller.rb | 2 ++ app/models/moderation_zone.rb | 9 ++++++ lib/osm.rb | 14 +++++++-- test/controllers/api/notes_controller_test.rb | 31 +++++++++++++++++++ test/factories/moderation_zones.rb | 26 ++++++++++++++++ test/models/moderation_zone_test.rb | 12 +++++++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 test/factories/moderation_zones.rb diff --git a/app/controllers/api/notes_controller.rb b/app/controllers/api/notes_controller.rb index 1a000e243..bc62bdafd 100644 --- a/app/controllers/api/notes_controller.rb +++ b/app/controllers/api/notes_controller.rb @@ -89,6 +89,8 @@ module Api lat = OSM.parse_float(params[:lat], OSM::APIBadUserInput, "lat was not a number") description = params[:text] + raise OSM::APIModerationZoneError if current_user.nil? && ModerationZone.falls_within_any?(:lon => lon, :lat => lat) + # Get note's author info (for logged in users - user_id, for logged out users - IP address) note_author_info = author_info diff --git a/app/models/moderation_zone.rb b/app/models/moderation_zone.rb index 2a26771db..d69c58062 100644 --- a/app/models/moderation_zone.rb +++ b/app/models/moderation_zone.rb @@ -32,4 +32,13 @@ class ModerationZone < ApplicationRecord validates :name, :presence => true validates :reason, :presence => true validates :zone, :presence => true + + def self.falls_within_any?(lon:, lat:) + factory = RGeo::Cartesian.simple_factory(:srid => 4326) + point = factory.point(lon, lat) + + where( + arel_table[:zone].st_contains(point) + ).exists? + end end diff --git a/lib/osm.rb b/lib/osm.rb index d812edacd..e707bbecb 100644 --- a/lib/osm.rb +++ b/lib/osm.rb @@ -19,8 +19,18 @@ module OSM # Raised when access is denied. class APIAccessDenied < APIError - def initialize - super("Access denied") + def initialize(message = "Access denied") + super + end + + def status + :forbidden + end + end + + class APIModerationZoneError < APIAccessDenied + def initialize(message = "You don't have permissions to make changes in this zone, as it is currently protected by moderators") + super end def status diff --git a/test/controllers/api/notes_controller_test.rb b/test/controllers/api/notes_controller_test.rb index d04f3888c..70f2100d5 100644 --- a/test/controllers/api/notes_controller_test.rb +++ b/test/controllers/api/notes_controller_test.rb @@ -204,6 +204,18 @@ module Api assert_response :bad_request end + def test_create_anonymous_in_moderation_zone + point = coordinates_inside_seville_cathedral + create(:moderation_zone, :seville_cathedral) + assert_no_difference "Note.count" do + assert_no_difference "NoteComment.count" do + post api_notes_path(:lat => point.lat, :lon => point.lon, :text => "Down with this sort of thing") + end + end + assert_response :forbidden + assert_equal "You don't have permissions to make changes in this zone, as it is currently protected by moderators", response.headers["Error"] + end + def test_create_success user = create(:user) auth_header = bearer_authorization_header user @@ -232,6 +244,21 @@ module Api assert_equal note, subscription.note end + def test_create_success_in_moderation_zone + point = coordinates_inside_seville_cathedral + create(:moderation_zone, :seville_cathedral) + user = create(:user) + auth_header = bearer_authorization_header user + assert_difference "Note.count", 1 do + assert_difference "NoteComment.count", 1 do + assert_difference "NoteSubscription.count", 1 do + post api_notes_path(:lat => point.lat, :lon => point.lon, :text => "This is a comment", :format => "json"), :headers => auth_header + end + end + end + assert_response :success + end + def test_create_no_scope_fail user = create(:user) auth_header = bearer_authorization_header user, :scopes => %w[read_prefs] @@ -1254,5 +1281,9 @@ module Api assert_select "osm>note:nth-child(#{index + 1})>id", :text => note.id.to_s, :count => 1 end end + + def coordinates_inside_seville_cathedral + Struct.new(:lat, :lon).new(37.385972, -5.993149) + end end end diff --git a/test/factories/moderation_zones.rb b/test/factories/moderation_zones.rb new file mode 100644 index 000000000..acbb910f7 --- /dev/null +++ b/test/factories/moderation_zones.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :moderation_zone do + sequence(:name) { |n| "Moderation Zone #{n}" } + sequence(:reason) { |n| "Reason #{n}" } + creator { association :user } + + trait :seville_cathedral do + zone do + <<~GEOMETRY + POLYGON(( + -5.994110 37.386714, + -5.993954 37.385371, + -5.993621 37.385124, + -5.992162 37.385239, + -5.992264 37.386309, + -5.992506 37.386467, + -5.992559 37.386842, + -5.994110 37.386714 + )) + GEOMETRY + end + end + end +end diff --git a/test/models/moderation_zone_test.rb b/test/models/moderation_zone_test.rb index e927b8aea..b935a0c84 100644 --- a/test/models/moderation_zone_test.rb +++ b/test/models/moderation_zone_test.rb @@ -3,4 +3,16 @@ require "test_helper" class ModerationZoneTest < ActiveSupport::TestCase + def test_falls_within_any + create(:moderation_zone, :seville_cathedral) + + # Dead center + assert ModerationZone.falls_within_any?(:lat => 37.385972, :lon => -5.993149) + + # Inside, near the boundary + assert ModerationZone.falls_within_any?(:lat => 37.386658, :lon => -5.994024) + + # Outside, near the boundary + assert_not ModerationZone.falls_within_any?(:lat => 37.386769, :lon => -5.994185) + end end -- 2.47.3