mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Fix potential sql injection
This commit is contained in:
parent
dcd1c7ab2b
commit
1394d6202c
9 changed files with 115 additions and 63 deletions
|
|
@ -55,13 +55,7 @@ module StatsHelper
|
||||||
|
|
||||||
def distance_traveled(user, stat)
|
def distance_traveled(user, stat)
|
||||||
distance_unit = user.safe_settings.distance_unit
|
distance_unit = user.safe_settings.distance_unit
|
||||||
|
value = Stat.convert_distance(stat.distance, distance_unit).round
|
||||||
value =
|
|
||||||
if distance_unit == 'mi'
|
|
||||||
(stat.distance / 1609.34).round(2)
|
|
||||||
else
|
|
||||||
(stat.distance / 1000).round(2)
|
|
||||||
end
|
|
||||||
|
|
||||||
"#{number_with_delimiter(value)} #{distance_unit}"
|
"#{number_with_delimiter(value)} #{distance_unit}"
|
||||||
end
|
end
|
||||||
|
|
@ -103,7 +97,7 @@ module StatsHelper
|
||||||
stat.toponyms.count { _1['country'] }
|
stat.toponyms.count { _1['country'] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def x_than_prevopis_countries_visited(stat, previous_stat)
|
def x_than_previous_countries_visited(stat, previous_stat)
|
||||||
return '' unless previous_stat
|
return '' unless previous_stat
|
||||||
|
|
||||||
previous_countries = previous_stat.toponyms.count { _1['country'] }
|
previous_countries = previous_stat.toponyms.count { _1['country'] }
|
||||||
|
|
@ -123,16 +117,9 @@ module StatsHelper
|
||||||
return 'N/A' unless peak && peak[1].positive?
|
return 'N/A' unless peak && peak[1].positive?
|
||||||
|
|
||||||
date = Date.new(stat.year, stat.month, peak[0])
|
date = Date.new(stat.year, stat.month, peak[0])
|
||||||
distance_km = (peak[1] / 1000).round(2)
|
|
||||||
distance_unit = stat.user.safe_settings.distance_unit
|
distance_unit = stat.user.safe_settings.distance_unit
|
||||||
|
|
||||||
distance_value =
|
distance_value = Stat.convert_distance(peak[1], distance_unit).round
|
||||||
if distance_unit == 'mi'
|
|
||||||
(peak[1] / 1609.34).round(2)
|
|
||||||
else
|
|
||||||
distance_km
|
|
||||||
end
|
|
||||||
|
|
||||||
text = "#{date.strftime('%B %d')} (#{distance_value} #{distance_unit})"
|
text = "#{date.strftime('%B %d')} (#{distance_value} #{distance_unit})"
|
||||||
|
|
||||||
link_to text, map_url(start_at: date.beginning_of_day, end_at: date.end_of_day), class: 'underline'
|
link_to text, map_url(start_at: date.beginning_of_day, end_at: date.end_of_day), class: 'underline'
|
||||||
|
|
|
||||||
6
app/jobs/cache/preheating_job.rb
vendored
6
app/jobs/cache/preheating_job.rb
vendored
|
|
@ -13,19 +13,19 @@ class Cache::PreheatingJob < ApplicationJob
|
||||||
|
|
||||||
Rails.cache.write(
|
Rails.cache.write(
|
||||||
"dawarich/user_#{user.id}_points_geocoded_stats",
|
"dawarich/user_#{user.id}_points_geocoded_stats",
|
||||||
StatsQuery.new(user).send(:cached_points_geocoded_stats),
|
StatsQuery.new(user).cached_points_geocoded_stats,
|
||||||
expires_in: 1.day
|
expires_in: 1.day
|
||||||
)
|
)
|
||||||
|
|
||||||
Rails.cache.write(
|
Rails.cache.write(
|
||||||
"dawarich/user_#{user.id}_countries_visited",
|
"dawarich/user_#{user.id}_countries_visited",
|
||||||
user.send(:countries_visited_uncached),
|
user.countries_visited_uncached,
|
||||||
expires_in: 1.day
|
expires_in: 1.day
|
||||||
)
|
)
|
||||||
|
|
||||||
Rails.cache.write(
|
Rails.cache.write(
|
||||||
"dawarich/user_#{user.id}_cities_visited",
|
"dawarich/user_#{user.id}_cities_visited",
|
||||||
user.send(:cities_visited_uncached),
|
user.cities_visited_uncached,
|
||||||
expires_in: 1.day
|
expires_in: 1.day
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,19 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
||||||
Time.zone.name
|
Time.zone.name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def countries_visited_uncached
|
||||||
|
points
|
||||||
|
.without_raw_data
|
||||||
|
.where.not(country_name: [nil, ''])
|
||||||
|
.distinct
|
||||||
|
.pluck(:country_name)
|
||||||
|
.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def cities_visited_uncached
|
||||||
|
points.where.not(city: [nil, '']).distinct.pluck(:city).compact
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_api_key
|
def create_api_key
|
||||||
|
|
@ -168,17 +181,4 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
||||||
Users::MailerSendingJob.set(wait: 9.days).perform_later(id, 'post_trial_reminder_early')
|
Users::MailerSendingJob.set(wait: 9.days).perform_later(id, 'post_trial_reminder_early')
|
||||||
Users::MailerSendingJob.set(wait: 14.days).perform_later(id, 'post_trial_reminder_late')
|
Users::MailerSendingJob.set(wait: 14.days).perform_later(id, 'post_trial_reminder_late')
|
||||||
end
|
end
|
||||||
|
|
||||||
def countries_visited_uncached
|
|
||||||
points
|
|
||||||
.without_raw_data
|
|
||||||
.where.not(country_name: [nil, ''])
|
|
||||||
.distinct
|
|
||||||
.pluck(:country_name)
|
|
||||||
.compact
|
|
||||||
end
|
|
||||||
|
|
||||||
def cities_visited_uncached
|
|
||||||
points.where.not(city: [nil, '']).distinct.pluck(:city).compact
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,21 @@ class HexagonQuery
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
ActiveRecord::Base.connection.execute(build_hexagon_sql)
|
binds = []
|
||||||
|
user_sql = build_user_filter(binds)
|
||||||
|
date_filter = build_date_filter(binds)
|
||||||
|
|
||||||
|
sql = build_hexagon_sql(user_sql, date_filter)
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection.exec_query(sql, 'hexagon_sql', binds)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_hexagon_sql
|
def build_hexagon_sql(user_sql, date_filter)
|
||||||
user_filter = user_id ? "user_id = #{user_id}" : '1=1'
|
|
||||||
date_filter = build_date_filter
|
|
||||||
|
|
||||||
<<~SQL
|
<<~SQL
|
||||||
WITH bbox_geom AS (
|
WITH bbox_geom AS (
|
||||||
SELECT ST_MakeEnvelope(#{min_lon}, #{min_lat}, #{max_lon}, #{max_lat}, 4326) as geom
|
SELECT ST_MakeEnvelope($1, $2, $3, $4, 4326) as geom
|
||||||
),
|
),
|
||||||
bbox_utm AS (
|
bbox_utm AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -44,7 +47,7 @@ class HexagonQuery
|
||||||
id,
|
id,
|
||||||
timestamp
|
timestamp
|
||||||
FROM points
|
FROM points
|
||||||
WHERE #{user_filter}
|
WHERE #{user_sql}
|
||||||
#{date_filter}
|
#{date_filter}
|
||||||
AND ST_Intersects(
|
AND ST_Intersects(
|
||||||
lonlat,
|
lonlat,
|
||||||
|
|
@ -53,9 +56,9 @@ class HexagonQuery
|
||||||
),
|
),
|
||||||
hex_grid AS (
|
hex_grid AS (
|
||||||
SELECT
|
SELECT
|
||||||
(ST_HexagonGrid(#{hex_size}, bbox_utm.geom_utm)).geom as hex_geom_utm,
|
(ST_HexagonGrid($5, bbox_utm.geom_utm)).geom as hex_geom_utm,
|
||||||
(ST_HexagonGrid(#{hex_size}, bbox_utm.geom_utm)).i as hex_i,
|
(ST_HexagonGrid($5, bbox_utm.geom_utm)).i as hex_i,
|
||||||
(ST_HexagonGrid(#{hex_size}, bbox_utm.geom_utm)).j as hex_j
|
(ST_HexagonGrid($5, bbox_utm.geom_utm)).j as hex_j
|
||||||
FROM bbox_utm
|
FROM bbox_utm
|
||||||
),
|
),
|
||||||
hexagons_with_points AS (
|
hexagons_with_points AS (
|
||||||
|
|
@ -88,17 +91,58 @@ class HexagonQuery
|
||||||
row_number() OVER (ORDER BY point_count DESC) as id
|
row_number() OVER (ORDER BY point_count DESC) as id
|
||||||
FROM hexagon_stats
|
FROM hexagon_stats
|
||||||
ORDER BY point_count DESC
|
ORDER BY point_count DESC
|
||||||
LIMIT #{MAX_HEXAGONS_PER_REQUEST};
|
LIMIT $6;
|
||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_date_filter
|
def build_user_filter(binds)
|
||||||
|
# Add bbox coordinates: min_lon, min_lat, max_lon, max_lat
|
||||||
|
binds << min_lon
|
||||||
|
binds << min_lat
|
||||||
|
binds << max_lon
|
||||||
|
binds << max_lat
|
||||||
|
|
||||||
|
# Add hex_size
|
||||||
|
binds << hex_size
|
||||||
|
|
||||||
|
# Add limit
|
||||||
|
binds << MAX_HEXAGONS_PER_REQUEST
|
||||||
|
|
||||||
|
if user_id
|
||||||
|
binds << user_id
|
||||||
|
'user_id = $7'
|
||||||
|
else
|
||||||
|
'1=1'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_date_filter(binds)
|
||||||
return '' unless start_date || end_date
|
return '' unless start_date || end_date
|
||||||
|
|
||||||
conditions = []
|
conditions = []
|
||||||
conditions << "timestamp >= EXTRACT(EPOCH FROM '#{start_date}'::timestamp)" if start_date
|
current_param_index = user_id ? 8 : 7 # Account for bbox, hex_size, limit, and potential user_id
|
||||||
conditions << "timestamp <= EXTRACT(EPOCH FROM '#{end_date}'::timestamp)" if end_date
|
|
||||||
|
if start_date
|
||||||
|
start_timestamp = parse_date_to_timestamp(start_date)
|
||||||
|
binds << start_timestamp
|
||||||
|
conditions << "timestamp >= $#{current_param_index}"
|
||||||
|
current_param_index += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if end_date
|
||||||
|
end_timestamp = parse_date_to_timestamp(end_date)
|
||||||
|
binds << end_timestamp
|
||||||
|
conditions << "timestamp <= $#{current_param_index}"
|
||||||
|
end
|
||||||
|
|
||||||
conditions.any? ? "AND #{conditions.join(' AND ')}" : ''
|
conditions.any? ? "AND #{conditions.join(' AND ')}" : ''
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def parse_date_to_timestamp(date_string)
|
||||||
|
# Convert ISO date string to timestamp integer
|
||||||
|
Time.parse(date_string).to_i
|
||||||
|
rescue ArgumentError => e
|
||||||
|
ExceptionReporter.call(e, "Invalid date format: #{date_string}")
|
||||||
|
raise ArgumentError, "Invalid date format: #{date_string}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,9 @@ class StatsQuery
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
attr_reader :user
|
|
||||||
|
|
||||||
def cached_points_geocoded_stats
|
def cached_points_geocoded_stats
|
||||||
sql = ActiveRecord::Base.sanitize_sql_array([
|
sql = ActiveRecord::Base.sanitize_sql_array(
|
||||||
|
[
|
||||||
<<~SQL.squish,
|
<<~SQL.squish,
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(reverse_geocoded_at) as geocoded,
|
COUNT(reverse_geocoded_at) as geocoded,
|
||||||
|
|
@ -31,7 +28,8 @@ class StatsQuery
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
SQL
|
SQL
|
||||||
user.id
|
user.id
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
result = Point.connection.select_one(sql)
|
result = Point.connection.select_one(sql)
|
||||||
|
|
||||||
|
|
@ -40,4 +38,8 @@ class StatsQuery
|
||||||
without_data: result['without_data'].to_i
|
without_data: result['without_data'].to_i
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :user
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
<%= countries_visited(stat) %>
|
<%= countries_visited(stat) %>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-desc">
|
<div class="stat-desc">
|
||||||
<%= x_than_prevopis_countries_visited(stat, previous_stat) %>
|
<%= x_than_previous_countries_visited(stat, previous_stat) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
get 'stats/:year', to: 'stats#show', constraints: { year: /\d{4}/ }
|
get 'stats/:year', to: 'stats#show', constraints: { year: /\d{4}/ }
|
||||||
get 'stats/:year/:month', to: 'stats#month', constraints: { year: /\d{4}/, month: /\d{1,2}/ }
|
get 'stats/:year/:month', to: 'stats#month', constraints: { year: /\d{4}/, month: /(0?[1-9]|1[0-2])/ }
|
||||||
put 'stats/:year/:month/update',
|
put 'stats/:year/:month/update',
|
||||||
to: 'stats#update',
|
to: 'stats#update',
|
||||||
as: :update_year_month_stats,
|
as: :update_year_month_stats,
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ RSpec.describe Users::MailerSendingJob, type: :job do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is deleted' do
|
context 'when user is deleted' do
|
||||||
it 'raises ActiveRecord::RecordNotFound' do
|
it 'does not raise an error' do
|
||||||
user.destroy
|
user.destroy
|
||||||
|
|
||||||
expect do
|
expect do
|
||||||
|
|
|
||||||
19
spec/services/cache/clean_spec.rb
vendored
19
spec/services/cache/clean_spec.rb
vendored
|
|
@ -59,6 +59,25 @@ RSpec.describe Cache::Clean do
|
||||||
expect(Rails.cache.exist?(user_2_points_geocoded_stats_key)).to be false
|
expect(Rails.cache.exist?(user_2_points_geocoded_stats_key)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'deletes countries and cities cache for all users' do
|
||||||
|
Rails.cache.write(user_1_countries_key, %w[USA Canada])
|
||||||
|
Rails.cache.write(user_2_countries_key, %w[France Germany])
|
||||||
|
Rails.cache.write(user_1_cities_key, ['New York', 'Toronto'])
|
||||||
|
Rails.cache.write(user_2_cities_key, %w[Paris Berlin])
|
||||||
|
|
||||||
|
expect(Rails.cache.exist?(user_1_countries_key)).to be true
|
||||||
|
expect(Rails.cache.exist?(user_2_countries_key)).to be true
|
||||||
|
expect(Rails.cache.exist?(user_1_cities_key)).to be true
|
||||||
|
expect(Rails.cache.exist?(user_2_cities_key)).to be true
|
||||||
|
|
||||||
|
described_class.call
|
||||||
|
|
||||||
|
expect(Rails.cache.exist?(user_1_countries_key)).to be false
|
||||||
|
expect(Rails.cache.exist?(user_2_countries_key)).to be false
|
||||||
|
expect(Rails.cache.exist?(user_1_cities_key)).to be false
|
||||||
|
expect(Rails.cache.exist?(user_2_cities_key)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
it 'logs cache cleaning process' do
|
it 'logs cache cleaning process' do
|
||||||
expect(Rails.logger).to receive(:info).with('Cleaning cache...')
|
expect(Rails.logger).to receive(:info).with('Cleaning cache...')
|
||||||
expect(Rails.logger).to receive(:info).with('Cache cleaned')
|
expect(Rails.logger).to receive(:info).with('Cache cleaned')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue