Files
danbooru/app/controllers/application_controller.rb
evazion 99846b7e3d users: allow mods to change the names of other users.
Allow moderators to forcibly change the username of other users. This is
so mods can change abusive or invalid usernames.

* A mod can only change the username of Builder-level users and below.
* The user can't change their own name again until one week has passed.
* A modaction is logged when a mod changes a user's name.
* A dmail is sent to the user notifying them of the change.
* The dmail does not send the user an email notification. This is so we
  don't spam users if their name is changed after they're banned, or if
  they haven't visited the site in a long time.

The rename button is on the user's profile page, and when you hover over
the user's name and open the "..." menu.
2022-10-02 01:32:10 -05:00

268 lines
10 KiB
Ruby

# frozen_string_literal: true
class ApplicationController < ActionController::Base
class PageRemovedError < StandardError; end
class RequestBodyNotAllowedError < StandardError; end
include Pundit::Authorization
helper_method :search_params, :permitted_attributes
self.responder = ApplicationResponder
skip_forgery_protection if: -> { SessionLoader.new(request).has_api_authentication? }
before_action :check_get_body
before_action :reset_current_user
before_action :set_current_user
before_action :normalize_search
before_action :check_default_rate_limit
before_action :ip_ban_check
before_action :set_variant
before_action :add_headers
before_action :cause_error
before_action :redirect_if_name_invalid?
after_action :skip_session_if_publicly_cached
after_action :reset_current_user
layout "default"
rescue_from Exception, :with => :rescue_exception
def self.rescue_with(*klasses, status: 500)
rescue_from(*klasses) do |exception|
render_error_page(status, exception)
end
end
# Define a rate limit for the given controller action.
#
# @example
# rate_limit :index, 1.0/1.minute, 50, if: -> { request.format.atom? }
def self.rate_limit(action, rate:, burst:, key: "#{controller_name}:#{action}", if: nil)
if_proc = binding.local_variable_get(:if)
before_action(only: action, if: if_proc) do
key = "#{controller_name}:#{action}"
rate_limiter = RateLimiter.build(action: key, rate: rate, burst: burst, user: CurrentUser.user, ip_addr: request.remote_ip)
headers["X-Rate-Limit"] = rate_limiter.to_json
rate_limiter.limit!
end
skip_before_action :check_default_rate_limit, only: action, if: if_proc
end
private
def respond_with(subject, *args, model: model_name, **options, &block)
if params[:action] == "index" && is_redirect?(subject)
redirect_to_show(subject)
return
end
if subject.respond_to?(:includes) && (request.format.json? || request.format.xml?)
associations = ParameterBuilder.includes_parameters(params[:only], model)
subject = subject.includes(associations)
end
@current_item = subject
super
end
def set_version_comparison(default_type = "previous")
params[:type] = %w[previous current].include?(params[:type]) ? params[:type] : default_type
end
def model_name
controller_name.classify
end
def redirect_to_show(items)
redirect_to send("#{controller_path.singularize}_path", items.first, format: request.format.symbol)
end
def is_redirect?(items)
action_methods.include?("show") && params[:redirect].to_s.truthy? && items.one? && item_matches_params(items.first)
end
def item_matches_params(*)
true
end
protected
def add_headers
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["X-Git-Hash"] = Rails.application.config.x.git_hash
end
# Apply the default rate limit to all update actions (POST, PUT, or DELETE), unless the
# endpoint already declared a more specific rate limit using the `rate_limit` macro above.
def check_default_rate_limit
return if request.get? || request.head?
rate_limiter = RateLimiter.build(
action: "#{controller_name}:#{action_name}",
rate: CurrentUser.user.api_regen_multiplier,
burst: 200,
user: CurrentUser.user,
ip_addr: request.remote_ip,
)
headers["X-Rate-Limit"] = rate_limiter.to_json
rate_limiter.limit!
end
def rescue_exception(exception)
case exception
when ActionView::Template::Error
rescue_exception(exception.cause)
when ActiveRecord::QueryCanceled
render_error_page(500, exception, template: "static/search_timeout", message: "The database timed out running your query.")
when ActionController::BadRequest
render_error_page(400, exception, message: exception.message)
when RequestBodyNotAllowedError
render_error_page(400, exception, message: "Request body not allowed for #{request.method} request")
when SessionLoader::AuthenticationFailure
render_error_page(401, exception, message: exception.message, template: "sessions/new")
when ActionController::InvalidAuthenticityToken, ActionController::UnpermittedParameters, ActionController::InvalidCrossOriginRequest, ActionController::Redirecting::UnsafeRedirectError
render_error_page(403, exception, message: exception.message)
when ActiveSupport::MessageVerifier::InvalidSignature, # raised by `find_signed!`
User::PrivilegeError,
Pundit::NotAuthorizedError
render_error_page(403, exception, template: "static/access_denied", message: "Access denied")
when ActiveRecord::RecordNotFound
render_error_page(404, exception, message: "That record was not found.")
when ActionController::RoutingError
render_error_page(405, exception, message: exception.message)
when ActionController::UnknownFormat, ActionView::MissingTemplate
render_error_page(406, exception, message: "#{request.format} is not a supported format for this page")
when PaginationExtension::PaginationError
render_error_page(410, exception, template: "static/pagination_error", message: "You cannot go beyond page #{CurrentUser.user.page_limit}.")
when PostQuery::TagLimitError
render_error_page(422, exception, template: "static/tag_limit_error", message: "You cannot search for more than #{CurrentUser.tag_query_limit} tags at a time.")
when PostQuery::Error
render_error_page(422, exception, message: exception.message)
when UpgradeCode::InvalidCodeError, UpgradeCode::RedeemedCodeError, UpgradeCode::AlreadyUpgradedError
render_error_page(422, exception, message: exception.message)
when RateLimiter::RateLimitError
render_error_page(429, exception, message: "Rate limit exceeded. You're doing that too fast")
when PageRemovedError
render_error_page(451, exception, template: "static/page_removed_error", message: "This page has been removed because of a takedown request")
when Rack::Timeout::RequestTimeoutException
render_error_page(500, exception, message: "Your request took too long to complete and was canceled.")
when NotImplementedError
render_error_page(501, exception, message: "This feature isn't available: #{exception.message}")
when PG::ConnectionBad
render_error_page(503, exception, message: "The database is unavailable. Try again later.")
else
raise exception if Rails.env.development? || Danbooru.config.debug_mode
render_error_page(500, exception)
end
end
def render_error_page(status, exception = nil, message: "", template: "static/error", format: request.format.symbol)
@exception = exception
@expected = status < 500
@message = message.to_s.encode("utf-8", invalid: :replace, undef: :replace)
@backtrace = Rails.backtrace_cleaner.clean(@exception.backtrace) if @exception
format = :html unless format.in?(%i[html json xml js atom])
@api_response = { success: false, error: @exception.class.to_s, message: @message, backtrace: @backtrace }
# if InvalidAuthenticityToken was raised, CurrentUser isn't set so we have to use the blank layout.
layout = CurrentUser.user.present? ? "default" : "blank"
DanbooruLogger.log(@exception, expected: @expected) if @exception
render template, layout: layout, status: status, formats: format
rescue ActionView::MissingTemplate
render "static/error", layout: layout, status: status, formats: format
end
def set_current_user
SessionLoader.new(request).load
end
def reset_current_user
CurrentUser.user = nil
CurrentUser.safe_mode = false
end
# Skip setting the session cookie if the response is being publicly cached to
# prevent the user's session cookie from being leaked to other users.
def skip_session_if_publicly_cached
if response.cache_control[:public] == true
request.session_options[:skip] = true
end
end
def set_variant
request.variant = params[:variant].try(:to_sym)
end
def check_get_body
raise RequestBodyNotAllowedError if request.method.in?(%w[GET HEAD OPTIONS]) && request.body.size > 0
end
# allow api clients to force errors for testing purposes.
def cause_error
return unless params[:cause_error].present?
status = params[:cause_error].to_i
raise ArgumentError, "invalid status code" unless status.in?(400..599)
error = StandardError.new(params[:message])
error.set_backtrace(caller)
render_error_page(status, error)
end
def redirect_if_name_invalid?
if request.format.html? && !CurrentUser.user.is_anonymous? && CurrentUser.user.name_invalid?
flash[:notice] = "You must change your username to continue using #{Danbooru.config.app_name}"
redirect_to change_name_user_path(CurrentUser.user)
end
end
def ip_ban_check
raise User::PrivilegeError if !request.get? && IpBan.hit!(:full, request.remote_ip)
end
def pundit_user
CurrentUser.user
end
def pundit_params_for(record)
params.fetch(Pundit::PolicyFinder.new(record).param_key, {})
end
def requires_reauthentication
return if CurrentUser.user.is_anonymous?
last_authenticated_at = session[:last_authenticated_at]
if last_authenticated_at.blank? || Time.zone.parse(last_authenticated_at) < 60.minutes.ago
redirect_to confirm_password_session_path(url: request.fullpath)
end
end
# Remove blank `search` params from the url.
#
# /tags?search[name]=touhou&search[category]=&search[order]=
# => /tags?search[name]=touhou
def normalize_search
return unless request.get? || request.head?
params[:search] ||= ActionController::Parameters.new
deep_reject_blank = lambda do |hash|
hash.reject { |_k, v| v.blank? || (v.is_a?(Hash) && deep_reject_blank.call(v).blank?) }
end
nonblank_search_params = deep_reject_blank.call(params[:search])
if nonblank_search_params != params[:search]
params[:search] = nonblank_search_params
redirect_to url_for(params: params.except(:controller, :action, :index).permit!)
end
end
def search_params
params.fetch(:search, {}).permit!
end
end