# 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