Websites/app/controllers/application_controller.rb

264 lines
7.5 KiB
Ruby

# frozen_string_literal: true
class ApplicationController < ActionController::Base
class ReadOnlyError < StandardError; end
class FeatureUnavailableError < StandardError; end
before_action :set_view_path
before_action :initialize_session
before_action :normalize_search
before_action :set_common_headers
helper_method :plausible_domain, :site_domain, :assets_path, :safe_site_name, :site_title, :site_color, :controller_param, :action_param, :body_class, :stimulus_class
skip_before_action :verify_authenticity_token
rescue_from Exception, with: :rescue_exception
def set_view_path
prepend_view_path(Rails.root.join("app", "views", safe_site_name))
end
def set_common_headers
Websites.config.common_headers(request.domain).each do |key, value|
response.headers[key] = value
end
end
def initialize_session
WebLogger.initialize(request)
CurrentUser.user = APIUser.anonymous
CurrentUser.ip_addr = request.remote_ip
end
def plausible_domain
nil
end
def site_domain
"unknown"
end
def assets_path
site_domain
end
def site_title
"Unknown"
end
def site_color
"#2C2F33"
end
def safe_site_name
site_domain.parameterize.dasherize
end
def controller_param
return "unknown" unless params[:controller]
name = params[:controller].parameterize.dasherize
name = name.gsub("#{safe_site_name}-", "") if name.include?(safe_site_name)
name
end
def action_param
return "unknown" unless params[:action]
params[:action]
end
def body_class
"s-#{safe_site_name} c-#{controller_param} a-#{action_param}"
end
def stimulus_class
params[:controller].gsub("/", "--").dasherize
end
def track_usage(service)
APIUsage.create!(
user_id: CurrentUser.is_anonymous? ? nil : CurrentUser.id,
api_key: @apikey.nil? || @apikey.is_anon? ? nil : @apikey,
user_agent: request.headers["user-agent"] || "",
method: request.method,
path: request.path,
params: request.params.to_json,
service: service,
ip_addr: request.remote_ip,
)
end
module CommonAssetRoutes
def manifest
render(partial: "manifest", layout: false)
end
def browserconfig
render(partial: "browserconfig", layout: false)
end
end
module CommonRoutes
def online
render(json: {
success: true,
uptime: Time.now - Websites::STARTED_AT,
version: Websites.config.version,
})
end
def access_denied(message: nil, code: nil)
@message = message.present? ? "Access Denied: #{message}" : "Access Denied"
@code = code
respond_to do |fmt|
fmt.html { render("static/access_denied", status: 403) }
fmt.json do
render(json: {
success: false,
message: @message,
code: @code,
}, status: 403)
end
end
end
def not_found(code: nil)
return online if params[:other] == "status"
@code = code
respond_to do |fmt|
fmt.any { render("static/not_found", status: 404) }
fmt.json do
render(json: {
success: false,
message: "Not found.",
code: @code,
}, status: 404)
end
end
end
def readonly
respond_to do |fmt|
fmt.html { render("static/readonly", status: YiffyAPIErrorCodes::READONLY.status) }
fmt.json { render_error(YiffyAPIErrorCodes::READONLY, message: "This service is currently in read only mode. Try again later.") }
end
end
private
def render_error(*, **)
extend(YiffyAPIUtil).render_error(*, **)
end
end
module RenderMethods
def handle_error
@exception = request.env["action_dispatch.exception"]
@status_code = @exception.try(:status_code) ||
ActionDispatch::ExceptionWrapper.new(
request.env, @exception
).status_code
return not_found if @status_code == 404
return rescue_exception(@exception) if @exception
render_error_page(@status_code, Exception.new("An unexpected error occurred."))
end
def rescue_exception(exception)
@exception = exception
case exception
when ActiveRecord::RecordNotFound
not_found
when ReadOnlyError
readonly
when PG::Error
render_error_page(503, exception, message: "The database is unavailable. Try again later.")
when ActionController::ParameterMissing, ActionController::UnpermittedParameters, ActiveRecord::RecordInvalid
render_expected_error(400, exception.message)
when ActionController::RoutingError
render_error_page(405, exception)
when ActionController::UnknownFormat, ActionView::MissingTemplate
render_unsupported_format
when ActionController::InvalidAuthenticityToken
access_denied(message: "Invalid CSRF Token")
else
render_error_page(500, exception)
end
end
def render_unsupported_format
return not_found if request.format.nil?
render_expected_error(406, "#{request.format} is not a supported format for this page", format: :html)
end
def render_expected_error(status, message, format: request.format.symbol)
format = :html unless format.in?(%i[html json])
@message = message
@log_code = nil
render("static/error", status: status, formats: format)
end
def render_error_page(status, exception, message: exception.message, format: request.format.symbol)
@exception = exception
@expected = status < 500
@message = message.encode("utf-8", invalid: :replace, undef: :replace)
@backtrace = Rails.backtrace_cleaner.clean(@exception.backtrace)
format = :html unless format.in?(%i[html json])
@message = "An unexpected error occurred." if !(Rails.env.development? || CurrentUser.is_admin?) && message == exception.message
WebLogger.log_exception(@exception, expected: @expected)
log = ExceptionLog.add(exception, request: request) unless @expected
@log_code = log&.code
render("static/error", status: status, formats: format)
end
end
module SearchMethods
def normalize_search
return unless request.get? || request.head?
params[:search] ||= ActionController::Parameters.new
deep_reject_blank = ->(hash) do
hash.reject { |_k, v| v.blank? || (v.is_a?(Hash) && deep_reject_blank.call(v).blank?) }
end
if params[:search].is_a?(ActionController::Parameters)
nonblank_search_params = deep_reject_blank.call(params[:search])
else
nonblank_search_params = ActionController::Parameters.new
end
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
def permit_search_params(permitted_params)
params.fetch(:search, {}).permit(%i[id created_at updated_at] + permitted_params)
end
end
module ReadonlyMethods
extend ActiveSupport::Concern
def enforce_readonly
return unless Websites.config.readonly?
raise(ReadOnlyError) unless allowed_readonly_actions.include?(action_name)
end
def allowed_readonly_actions
%w[index show]
end
included do
before_action :enforce_readonly
end
end
include CommonRoutes
include RenderMethods
include SearchMethods
include Pagy::Backend
end