0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 11:36:31 +08:00
discourse/plugins/discourse-cakeday/app/controllers/discourse_cakeday/cakeday_controller.rb
Régis Hanol 6b4d70b0e0
DEV: Properly quote timezone values in cakeday SQL queries (#36804)
The anniversaries controller was interpolating user timezone values
directly into SQL via string interpolation. As a best practice, values
should be properly escaped at the point of SQL construction.

Moved timezone handling into cakedays_by() with a new apply_timezone
parameter and used ActiveRecord::Base.connection.quote() to properly
escape the value. This follows the same pattern used in
discourse-rewind.

Internal ref - t/102422
2025-12-19 14:54:52 +01:00

100 lines
2.9 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscourseCakeday
class CakedayController < ::ApplicationController
requires_plugin PLUGIN_NAME
before_action :ensure_logged_in
before_action :setup_params
private
PAGE_SIZE = 48
def setup_params
@page = params[:page].to_i.clamp(0..)
@month = params[:month].to_i.clamp(1..12)
@users =
User
.real
.activated
.not_staged
.not_silenced
.not_suspended
.joins(:user_option)
.where("user_options.hide_profile = ?", false)
@timezone = current_user&.user_option&.timezone
end
def cakedays_by(column_sql, at_least_one_year_old: false, apply_timezone: false)
more_params = { page: @page + 1, filter: params[:filter] }
today =
begin
Time.zone.now.in_time_zone(@timezone)
rescue ArgumentError
Time.zone.now
end.to_date
if apply_timezone && @timezone.present? && @timezone != "UTC"
if (iana_timezone = ActiveSupport::TimeZone[@timezone]&.tzinfo&.identifier)
quoted_timezone = ActiveRecord::Base.connection.quote(iana_timezone)
column_sql = "#{column_sql} AT TIME ZONE 'UTC' AT TIME ZONE #{quoted_timezone}"
end
end
if at_least_one_year_old
@users = @users.where("EXTRACT(YEAR FROM #{column_sql}) < ?", today.year)
end
# when the current year is not a leap year, we consider February 29th to be March 1st
date_sql = <<~SQL
TO_CHAR(#{column_sql}, 'MMDD') = :date OR (
NOT :leap AND
:date = '0301' AND
TO_CHAR(#{column_sql}, 'MMDD') = '0229'
)
SQL
@users =
case params[:filter]
when "today"
@users.where(date_sql, leap: today.leap?, date: mmdd(today))
when "tomorrow"
tomorrow = today + 1.day
@users.where(date_sql, leap: tomorrow.leap?, date: mmdd(tomorrow))
when "upcoming"
from = today + 2.days
to = from + 1.week
@users.where("TO_CHAR(#{column_sql}, 'MMDD') BETWEEN ? AND ?", mmdd(from), mmdd(to))
else # month
more_params[:month] = @month
@users.where("EXTRACT(MONTH FROM #{column_sql}) = ?", @month)
end
total = @users.count
# when the cakedate is the same, we order based on how the data is displayed
tie_breaker =
if SiteSetting.prioritize_username_in_ux
:username_lower
else
"COALESCE(NULLIF(LOWER(TRIM(users.name)), ''), users.username_lower) ASC"
end
@users =
@users
.select(:id, :username, :name, :title, :uploaded_avatar_id, "#{column_sql} cakedate")
.order("TO_CHAR(#{column_sql}, 'MMDDYYYY') ASC")
.order(tie_breaker)
.limit(PAGE_SIZE)
.offset(PAGE_SIZE * @page)
[@users, total, more_params]
end
def mmdd(date)
date.strftime("%m%d")
end
end
end