264 lines
7.5 KiB
Ruby
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
|