# frozen_string_literal: true require "current_user" class ApplicationController < ActionController::Base include CurrentUser include CanonicalURL::ControllerExtensions include JsonError include GlobalPath include Hijack include ReadOnlyMixin include VaryHeader attr_reader :theme_id serialization_scope :guardian protect_from_forgery # Default Rails 3.2 lets the request through with a blank session # we are being more pedantic here and nulling session / current_user # and then raising a CSRF exception def handle_unverified_request # NOTE: API key is secret, having it invalidates the need for a CSRF token unless is_api? || is_user_api? super clear_current_user render plain: "[\"BAD CSRF\"]", status: 403 end end before_action :rate_limit_crawlers before_action :check_readonly_mode before_action :handle_theme before_action :set_current_user_for_logs before_action :set_mp_snapshot_fields before_action :clear_notifications around_action :with_resolved_locale before_action :set_mobile_view before_action :block_if_readonly_mode before_action :authorize_mini_profiler before_action :redirect_to_login_if_required before_action :block_if_requires_login before_action :preload_json before_action :check_xhr after_action :add_readonly_header after_action :perform_refresh_session after_action :dont_cache_page after_action :conditionally_allow_site_embedding after_action :ensure_vary_header after_action :add_noindex_header, if: -> { is_feed_request? || !SiteSetting.allow_index_in_robots_txt } after_action :add_noindex_header_to_non_canonical, if: :spa_boot_request? after_action :set_cross_origin_opener_policy_header, if: :spa_boot_request? after_action :clean_xml, if: :is_feed_response? around_action :link_preload, if: -> { spa_boot_request? && GlobalSetting.preload_link_header } HONEYPOT_KEY ||= "HONEYPOT_KEY" CHALLENGE_KEY ||= "CHALLENGE_KEY" layout :set_layout def has_escaped_fragment? SiteSetting.enable_escaped_fragments? && params.key?("_escaped_fragment_") end def show_browser_update? @show_browser_update ||= CrawlerDetection.show_browser_update?(request.user_agent) end helper_method :show_browser_update? def use_crawler_layout? @use_crawler_layout ||= request.user_agent && (request.media_type.blank? || request.media_type.include?("html")) && !%w[json rss].include?(params[:format]) && ( has_escaped_fragment? || params.key?("print") || show_browser_update? || CrawlerDetection.crawler?(request.user_agent, request.headers["HTTP_VIA"]) ) end def perform_refresh_session refresh_session(current_user) unless @readonly_mode end def immutable_for(duration) response.cache_control[:max_age] = duration.to_i response.cache_control[:public] = true response.cache_control[:extras] = ["immutable"] end def dont_cache_page if !response.headers["Cache-Control"] && response.cache_control.blank? response.cache_control[:no_cache] = true response.cache_control[:extras] = ["no-store"] end response.headers["Discourse-No-Onebox"] = "1" if SiteSetting.login_required end def conditionally_allow_site_embedding response.headers.delete("X-Frame-Options") if SiteSetting.allow_embedding_site_in_an_iframe end def ember_cli_required? Rails.env.development? && ENV["ALLOW_EMBER_CLI_PROXY_BYPASS"] != "1" && request.headers["X-Discourse-Ember-CLI"] != "true" end def application_layout ember_cli_required? ? "ember_cli" : "application" end def set_layout case request.headers["Discourse-Render"] when "desktop" return application_layout when "crawler" return "crawler" end use_crawler_layout? ? "crawler" : application_layout end class RenderEmpty < StandardError end class PluginDisabled < StandardError end rescue_from RenderEmpty do with_resolved_locale { render "default/empty" } end rescue_from ArgumentError do |e| if e.message == "string contains null byte" raise Discourse::InvalidParameters, e.message else raise e end end rescue_from PG::ReadOnlySqlTransaction do |e| Discourse.received_postgres_readonly! Rails.logger.error("#{e.class} #{e.message}: #{e.backtrace.join("\n")}") rescue_with_handler(Discourse::ReadOnly.new) || raise end rescue_from ActionController::ParameterMissing do |e| render_json_error e.message, status: 400 end rescue_from Discourse::SiteSettingMissing do |e| render_json_error I18n.t("site_setting_missing", name: e.message), status: 500 end rescue_from ActionController::RoutingError, PluginDisabled do rescue_discourse_actions(:not_found, 404) end # Handles requests for giant IDs that throw pg exceptions rescue_from ActiveModel::RangeError do |e| if e.message =~ /ActiveModel::Type::Integer/ rescue_discourse_actions(:not_found, 404) else raise e end end rescue_from ActiveRecord::RecordInvalid do |e| if request.format && request.format.json? render_json_error e, type: :record_invalid, status: 422 else raise e end end rescue_from ActiveRecord::StatementInvalid do |e| Discourse.reset_active_record_cache_if_needed(e) raise e end # If they hit the rate limiter rescue_from RateLimiter::LimitExceeded do |e| retry_time_in_seconds = e&.available_in response_headers = { "Retry-After": retry_time_in_seconds.to_s } response_headers["Discourse-Rate-Limit-Error-Code"] = e.error_code if e&.error_code with_resolved_locale do render_json_error( e.description, type: :rate_limit, status: 429, extras: { wait_seconds: retry_time_in_seconds, time_left: e&.time_left, }, headers: response_headers, ) end end rescue_from Discourse::NotLoggedIn do |e| if (request.format && request.format.json?) || request.xhr? || !request.get? rescue_discourse_actions(:not_logged_in, 403, include_ember: true) else rescue_discourse_actions(:not_found, 404) end end rescue_from Discourse::InvalidParameters do |e| opts = { custom_message: "invalid_params", custom_message_params: { message: e.message } } if (request.format && request.format.json?) || request.xhr? || !request.get? rescue_discourse_actions(:invalid_parameters, 400, opts.merge(include_ember: true)) else rescue_discourse_actions(:not_found, 400, opts) end end rescue_from Discourse::NotFound do |e| rescue_discourse_actions( :not_found, e.status, check_permalinks: e.check_permalinks, original_path: e.original_path, custom_message: e.custom_message, ) end rescue_from Discourse::InvalidAccess do |e| cookies.delete(e.opts[:delete_cookie]) if e.opts[:delete_cookie].present? rescue_discourse_actions( :invalid_access, 403, include_ember: true, custom_message: e.custom_message, custom_message_params: e.custom_message_params, group: e.group, ) end rescue_from Discourse::ReadOnly do unless response_body respond_to do |format| format.json do render_json_error I18n.t("read_only_mode_enabled"), type: :read_only, status: 503 end format.html { render status: 503, layout: "no_ember", template: "exceptions/read_only" } end end end rescue_from SecondFactor::AuthManager::SecondFactorRequired do |e| if request.xhr? render json: { second_factor_challenge_nonce: e.nonce }, status: 403 else redirect_to session_2fa_path(nonce: e.nonce) end end rescue_from SecondFactor::BadChallenge do |e| render json: { error: I18n.t(e.error_translation_key) }, status: e.status_code end def redirect_with_client_support(url, options = {}) if request.xhr? response.headers["Discourse-Xhr-Redirect"] = "true" render plain: url else redirect_to url, options end end def rescue_discourse_actions(type, status_code, opts = nil) opts ||= {} show_json_errors = (request.format && request.format.json?) || (request.xhr?) || ((params[:external_id] || "").ends_with? ".json") if type == :not_found && opts[:check_permalinks] url = opts[:original_path] || request.fullpath permalink = Permalink.find_by_url(url) # there are some cases where we have a permalink but no url # cause category / topic was deleted if permalink.present? && permalink.target_url # permalink present, redirect to that URL redirect_with_client_support permalink.target_url, status: :moved_permanently, allow_other_host: true return end end message = title = nil with_resolved_locale(check_current_user: false) do if opts[:custom_message] title = message = I18n.t(opts[:custom_message], opts[:custom_message_params] || {}) else message = I18n.t(type) if status_code == 403 title = I18n.t("page_forbidden.title") else title = I18n.t("page_not_found.title") end end end error_page_opts = { title: title, status: status_code, group: opts[:group] } if show_json_errors opts = { type: type, status: status_code } with_resolved_locale(check_current_user: false) do # Include error in HTML format for topics#show. if (request.params[:controller] == "topics" && request.params[:action] == "show") || ( request.params[:controller] == "categories" && request.params[:action] == "find_by_slug" ) opts[:extras] = { title: I18n.t("page_not_found.page_title"), html: build_not_found_page(error_page_opts), group: error_page_opts[:group], } end end render_json_error message, opts else begin # 404 pages won't have the session and theme_keys without these: current_user handle_theme rescue Discourse::InvalidAccess return render plain: message, status: status_code end with_resolved_locale do error_page_opts[:layout] = (opts[:include_ember] && @preloaded) ? "application" : "no_ember" render html: build_not_found_page(error_page_opts) end end end # If a controller requires a plugin, it will raise an exception if that plugin is # disabled. This allows plugins to be disabled programmatically. def self.requires_plugin(plugin_name) before_action do if plugin = Discourse.plugins_by_name[plugin_name] raise PluginDisabled.new if !plugin.enabled? elsif Rails.env.test? raise "Required plugin '#{plugin_name}' not found. The string passed to requires_plugin should match the plugin's name at the top of plugin.rb" else Rails.logger.warn("Required plugin '#{plugin_name}' not found") end end end def set_current_user_for_logs if current_user Logster.add_to_env(request.env, "username", current_user.username) response.headers["X-Discourse-Username"] = current_user.username end response.headers["X-Discourse-Route"] = "#{controller_name}/#{action_name}" end def set_mp_snapshot_fields if defined?(Rack::MiniProfiler) Rack::MiniProfiler.add_snapshot_custom_field("Application version", Discourse.git_version) if Rack::MiniProfiler.snapshots_transporter? Rack::MiniProfiler.add_snapshot_custom_field("Site", Discourse.current_hostname) end end end def clear_notifications if current_user && !@readonly_mode cookie_notifications = cookies["cn"] notifications = request.headers["Discourse-Clear-Notifications"] if cookie_notifications if notifications.present? notifications += ",#{cookie_notifications}" else notifications = cookie_notifications end end if notifications.present? notification_ids = notifications.split(",").map(&:to_i) Notification.read(current_user, notification_ids) current_user.reload current_user.publish_notifications_state cookie_args = {} cookie_args[:path] = Discourse.base_path if Discourse.base_path.present? cookies.delete("cn", cookie_args) end end end def with_resolved_locale(check_current_user: true) if check_current_user && ( user = begin current_user rescue StandardError nil end ) locale = user.effective_locale else locale = Discourse.anonymous_locale(request) locale ||= SiteSetting.default_locale end locale = SiteSettings::DefaultsProvider::DEFAULT_LOCALE if !I18n.locale_available?(locale) I18n.ensure_all_loaded! I18n.with_locale(locale) { yield } end def store_preloaded(key, json) @preloaded ||= {} # I dislike that there is a gsub as opposed to a gsub! # but we can not be mucking with user input, I wonder if there is a way # to inject this safety deeper in the library or even in AM serializer @preloaded[key] = json.gsub("</", "<\\/") end # If we are rendering HTML, preload the session data def preload_json # We don't preload JSON on xhr or JSON request return if request.xhr? || request.format.json? # if we are posting in makes no sense to preload return if request.method != "GET" # TODO should not be invoked on redirection so this should be further deferred preload_anonymous_data if current_user current_user.sync_notification_channel_position preload_current_user_data end end def set_mobile_view session[:mobile_view] = params[:mobile_view] if params.has_key?(:mobile_view) end NO_THEMES = "no_themes" NO_PLUGINS = "no_plugins" NO_UNOFFICIAL_PLUGINS = "no_unofficial_plugins" SAFE_MODE = "safe_mode" LEGACY_NO_THEMES = "no_custom" LEGACY_NO_UNOFFICIAL_PLUGINS = "only_official" def resolve_safe_mode return unless guardian.can_enable_safe_mode? safe_mode = params[SAFE_MODE] if safe_mode.is_a?(String) safe_mode = safe_mode.split(",") request.env[NO_THEMES] = safe_mode.include?(NO_THEMES) || safe_mode.include?(LEGACY_NO_THEMES) request.env[NO_PLUGINS] = safe_mode.include?(NO_PLUGINS) request.env[NO_UNOFFICIAL_PLUGINS] = safe_mode.include?(NO_UNOFFICIAL_PLUGINS) || safe_mode.include?(LEGACY_NO_UNOFFICIAL_PLUGINS) end end def handle_theme return if request.format == "js" resolve_safe_mode return if request.env[NO_THEMES] theme_id = nil if (preview_theme_id = request[:preview_theme_id]&.to_i) && guardian.allow_themes?([preview_theme_id], include_preview: true) theme_id = preview_theme_id end user_option = current_user&.user_option if theme_id.blank? ids, seq = cookies[:theme_ids]&.split("|") id = ids&.split(",")&.map(&:to_i)&.first if id.present? && seq && seq.to_i == user_option&.theme_key_seq.to_i theme_id = id if guardian.allow_themes?([id]) end end if theme_id.blank? ids = user_option&.theme_ids || [] theme_id = ids.first if guardian.allow_themes?(ids) end if theme_id.blank? && SiteSetting.default_theme_id != -1 && guardian.allow_themes?([SiteSetting.default_theme_id]) theme_id = SiteSetting.default_theme_id end @theme_id = request.env[:resolved_theme_id] = theme_id end def guardian # sometimes we log on a user in the middle of a request so we should throw # away the cached guardian instance when we do that if (@guardian&.user).blank? && current_user.present? @guardian = Guardian.new(current_user, request) end @guardian ||= Guardian.new(current_user, request) end def current_homepage current_user&.user_option&.homepage || SiteSetting.anonymous_homepage end def serialize_data(obj, serializer, opts = nil) # If it's an array, apply the serializer as an each_serializer to the elements serializer_opts = { scope: guardian }.merge!(opts || {}) if obj.respond_to?(:to_ary) serializer_opts[:each_serializer] = serializer ActiveModel::ArraySerializer.new(obj.to_ary, serializer_opts).as_json else serializer.new(obj, serializer_opts).as_json end end # This is odd, but it seems that in Rails `render json: obj` is about # 20% slower than calling MultiJSON.dump ourselves. I'm not sure why # Rails doesn't call MultiJson.dump when you pass it json: obj but # it seems we don't need whatever Rails is doing. def render_serialized(obj, serializer, opts = nil) render_json_dump(serialize_data(obj, serializer, opts), opts) end def render_json_dump(obj, opts = nil) opts ||= {} if opts[:rest_serializer] obj["__rest_serializer"] = "1" opts.each { |k, v| obj[k] = v if k.to_s.start_with?("refresh_") } obj["extras"] = opts[:extras] if opts[:extras] obj["meta"] = opts[:meta] if opts[:meta] end render json: MultiJson.dump(obj), status: opts[:status] || 200 end def can_cache_content? current_user.blank? && cookies[:authentication_data].blank? end # Our custom cache method def discourse_expires_in(time_length) return unless can_cache_content? Middleware::AnonymousCache.anon_cache(request.env, time_length) end def fetch_user_from_params(opts = nil, eager_load = []) opts ||= {} user = if params[:username] username_lower = params[:username].downcase.chomp(".json") if current_user && current_user.username_lower == username_lower current_user else find_opts = { username_lower: username_lower } find_opts[:active] = true unless opts[:include_inactive] || current_user.try(:staff?) result = User (result = result.includes(*eager_load)) if !eager_load.empty? result.find_by(find_opts) end elsif params[:external_id] external_id = params[:external_id].chomp(".json") if provider_name = params[:external_provider] raise Discourse::InvalidAccess unless guardian.is_admin? # external_id might be something sensitive provider = Discourse.enabled_authenticators.find { |a| a.name == provider_name } raise Discourse::NotFound if !provider&.is_managed? # Only managed authenticators use UserAssociatedAccount UserAssociatedAccount.find_by( provider_name: provider_name, provider_uid: external_id, )&.user else SingleSignOnRecord.find_by(external_id: external_id).try(:user) end end raise Discourse::NotFound if user.blank? guardian.ensure_can_see!(user) user end def post_ids_including_replies post_ids = params[:post_ids].map(&:to_i) post_ids |= PostReply.where(post_id: params[:reply_post_ids]).pluck(:reply_post_id) if params[ :reply_post_ids ] post_ids end def no_cookies # do your best to ensure response has no cookies # longer term we may want to push this into middleware headers.delete "Set-Cookie" request.session_options[:skip] = true end def secure_session SecureSession.new(session["secure_session_id"] ||= SecureRandom.hex) end def handle_permalink(path) permalink = Permalink.find_by_url(path) if permalink && permalink.target_url redirect_to permalink.target_url, status: :moved_permanently end end def rate_limit_second_factor!(user) return if params[:second_factor_token].blank? RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 6, 1.minute).performed! RateLimiter.new(nil, "second-factor-min-#{user.username}", 6, 1.minute).performed! if user end private def preload_anonymous_data store_preloaded("site", Site.json_for(guardian)) store_preloaded("siteSettings", SiteSetting.client_settings_json) store_preloaded("customHTML", custom_html_json) store_preloaded("banner", banner_json) store_preloaded("customEmoji", custom_emoji) store_preloaded("isReadOnly", @readonly_mode.to_s) store_preloaded("isStaffWritesOnly", @staff_writes_only_mode.to_s) store_preloaded("activatedThemes", activated_themes_json) end def preload_current_user_data store_preloaded( "currentUser", MultiJson.dump( CurrentUserSerializer.new( current_user, scope: guardian, root: false, navigation_menu_param: params[:navigation_menu], ), ), ) report = TopicTrackingState.report(current_user) serializer = TopicTrackingStateSerializer.new(report, scope: guardian, root: false) hash = serializer.as_json store_preloaded("topicTrackingStates", MultiJson.dump(hash[:data])) store_preloaded("topicTrackingStateMeta", MultiJson.dump(hash[:meta])) # This is used in the wizard so we can preload fonts using the FontMap JS API. store_preloaded("fontMap", MultiJson.dump(load_font_map)) if current_user.admin? end def custom_html_json target = view_context.mobile_view? ? :mobile : :desktop data = if @theme_id.present? { top: Theme.lookup_field(@theme_id, target, "after_header"), footer: Theme.lookup_field(@theme_id, target, "footer"), } else {} end data.merge! DiscoursePluginRegistry.custom_html if DiscoursePluginRegistry.custom_html DiscoursePluginRegistry.html_builders.each do |name, _| if name.start_with?("client:") data[name.sub(/\Aclient:/, "")] = DiscoursePluginRegistry.build_html(name, self) end end MultiJson.dump(data) end def self.banner_json_cache @banner_json_cache ||= DistributedCache.new("banner_json") end def banner_json return "{}" if !current_user && SiteSetting.login_required? ApplicationController .banner_json_cache .defer_get_set("json") do topic = Topic.where(archetype: Archetype.banner).first banner = topic.present? ? topic.banner : {} MultiJson.dump(banner) end end def custom_emoji serializer = ActiveModel::ArraySerializer.new(Emoji.custom, each_serializer: EmojiSerializer) MultiJson.dump(serializer) end # Render action for a JSON error. # # obj - a translated string, an ActiveRecord model, or an array of translated strings # opts: # type - a machine-readable description of the error # status - HTTP status code to return # headers - extra headers for the response def render_json_error(obj, opts = {}) opts = { status: opts } if opts.is_a?(Integer) opts.fetch(:headers, {}).each { |name, value| headers[name.to_s] = value } render( json: MultiJson.dump(create_errors_json(obj, opts)), status: opts[:status] || status_code(obj), ) end def status_code(obj) return 403 if obj.try(:forbidden) return 404 if obj.try(:not_found) 422 end def success_json { success: "OK" } end def failed_json { failed: "FAILED" } end def json_result(obj, opts = {}) if yield(obj) json = success_json # If we were given a serializer, add the class to the json that comes back if opts[:serializer].present? json[obj.class.name.underscore] = opts[:serializer].new( obj, scope: guardian, ).serializable_hash end render json: MultiJson.dump(json) else error_obj = nil if opts[:additional_errors] error_target = opts[:additional_errors].find do |o| target = obj.public_send(o) target && target.errors.present? end error_obj = obj.public_send(error_target) if error_target end render_json_error(error_obj || obj) end end def mini_profiler_enabled? defined?(Rack::MiniProfiler) && (guardian.is_developer? || Rails.env.development?) end def authorize_mini_profiler return unless mini_profiler_enabled? Rack::MiniProfiler.authorize_request end def check_xhr # bypass xhr check on PUT / POST / DELETE provided api key is there, otherwise calling api is annoying return if !request.get? && (is_api? || is_user_api?) unless ((request.format && request.format.json?) || request.xhr?) raise ApplicationController::RenderEmpty.new end end def apply_cdn_headers if Discourse.is_cdn_request?(request.env, request.method) Discourse.apply_cdn_headers(response.headers) end end def self.requires_login(arg = {}) @requires_login_arg = arg end def self.requires_login_arg @requires_login_arg end def block_if_requires_login if arg = self.class.requires_login_arg check = if except = arg[:except] !except.include?(action_name.to_sym) elsif only = arg[:only] only.include?(action_name.to_sym) else true end ensure_logged_in if check end end def ensure_logged_in raise Discourse::NotLoggedIn.new unless current_user.present? end def ensure_staff raise Discourse::InvalidAccess.new unless current_user && current_user.staff? end def ensure_admin raise Discourse::InvalidAccess.new unless current_user && current_user.admin? end def ensure_wizard_enabled raise Discourse::InvalidAccess.new unless SiteSetting.wizard_enabled? end def destination_url request.original_url unless request.original_url =~ /uploads/ end def redirect_to_login dont_cache_page if SiteSetting.auth_immediately && SiteSetting.enable_discourse_connect? # save original URL in a session so we can redirect after login session[:destination_url] = destination_url redirect_to path("/session/sso") elsif SiteSetting.auth_immediately && !SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1 && !cookies[:authentication_data] # Only one authentication provider, direct straight to it. # If authentication_data is present, then we are halfway though registration. Don't redirect offsite cookies[:destination_url] = destination_url redirect_to path("/auth/#{Discourse.enabled_authenticators.first.name}") else # save original URL in a cookie (javascript redirects after login in this case) cookies[:destination_url] = destination_url redirect_to path("/login") end end def redirect_to_login_if_required return if request.format.json? && is_api? # Used by clients authenticated via user API. # Redirects to provided URL scheme if # - request uses a valid public key and auth_redirect scheme # - one_time_password scope is allowed if !current_user && params.has_key?(:user_api_public_key) && params.has_key?(:auth_redirect) begin OpenSSL::PKey::RSA.new(params[:user_api_public_key]) rescue OpenSSL::PKey::RSAError return render plain: I18n.t("user_api_key.invalid_public_key") end if UserApiKey.invalid_auth_redirect?(params[:auth_redirect]) return render plain: I18n.t("user_api_key.invalid_auth_redirect") end if UserApiKey.allowed_scopes.superset?(Set.new(["one_time_password"])) redirect_to("#{params[:auth_redirect]}?otp=true", allow_other_host: true) return end end if !current_user && SiteSetting.login_required? flash.keep if (request.format && request.format.json?) || request.xhr? || !request.get? ensure_logged_in else redirect_to_login end return end return if !current_user return if !should_enforce_2fa? redirect_path = path("/u/#{current_user.encoded_username}/preferences/second-factor") if !request.fullpath.start_with?(redirect_path) redirect_to path(redirect_path) nil end end def should_enforce_2fa? disqualified_from_2fa_enforcement = request.format.json? || is_api? || current_user.anonymous? enforcing_2fa = ( (SiteSetting.enforce_second_factor == "staff" && current_user.staff?) || SiteSetting.enforce_second_factor == "all" ) !disqualified_from_2fa_enforcement && enforcing_2fa && !current_user.has_any_second_factor_methods_enabled? end def build_not_found_page(opts = {}) if SiteSetting.bootstrap_error_pages? preload_json opts[:layout] = "application" if opts[:layout] == "no_ember" end @current_user = begin current_user rescue StandardError nil end if !SiteSetting.login_required? || @current_user key = "page_not_found_topics:#{I18n.locale}" @topics_partial = Discourse .cache .fetch(key, expires_in: 10.minutes) do category_topic_ids = Category.select(:topic_id).where.not(topic_id: nil) @top_viewed = TopicQuery .new(nil, except_topic_ids: category_topic_ids) .list_top_for("monthly") .topics .first(10) @recent = Topic.includes(:category).where.not(id: category_topic_ids).recent(10) render_to_string partial: "/exceptions/not_found_topics", formats: [:html] end .html_safe end @container_class = "wrap not-found-container" @page_title = I18n.t("page_not_found.page_title") @title = opts[:title] || I18n.t("page_not_found.title") @group = opts[:group] @hide_search = true if SiteSetting.login_required params[:slug] = params[:slug].first if params[:slug].kind_of?(Array) params[:id] = params[:id].first if params[:id].kind_of?(Array) @slug = (params[:slug].presence || params[:id].presence || "").to_s.tr("-", " ") render_to_string status: opts[:status], layout: opts[:layout], formats: [:html], template: "/exceptions/not_found" end def is_asset_path request.env["DISCOURSE_IS_ASSET_PATH"] = 1 end def is_feed_request? request.format.atom? || request.format.rss? end def is_feed_response? request.get? && response&.content_type&.match?(/(rss|atom)/) end def add_noindex_header if request.get? && !response.headers["X-Robots-Tag"] if SiteSetting.allow_index_in_robots_txt response.headers["X-Robots-Tag"] = "noindex" else response.headers["X-Robots-Tag"] = "noindex, nofollow" end end end def add_noindex_header_to_non_canonical canonical = (@canonical_url || @default_canonical) if canonical.present? && canonical != request.url && !SiteSetting.allow_indexing_non_canonical_urls response.headers["X-Robots-Tag"] ||= "noindex" end end def set_cross_origin_opener_policy_header if SiteSetting.cross_origin_opener_policy_header != "unsafe-none" response.headers["Cross-Origin-Opener-Policy"] = SiteSetting.cross_origin_opener_policy_header end end protected def honeypot_value secure_session[HONEYPOT_KEY] ||= SecureRandom.hex end def challenge_value secure_session[CHALLENGE_KEY] ||= SecureRandom.hex end def render_post_json(post, add_raw: true) post_serializer = PostSerializer.new(post, scope: guardian, root: false) post_serializer.add_raw = add_raw counts = PostAction.counts_for([post], current_user) if counts && counts = counts[post.id] post_serializer.post_actions = counts end render_json_dump(post_serializer) end # returns an array of integers given a param key # returns nil if key is not found def param_to_integer_list(key, delimiter = ",") case params[key] when String params[key].split(delimiter).map(&:to_i) when Array params[key].map(&:to_i) end end def activated_themes_json id = @theme_id return "{}" if id.blank? ids = Theme.transform_ids(id) Theme.where(id: ids).pluck(:id, :name).to_h.to_json end def rate_limit_crawlers return if current_user.present? return if SiteSetting.slow_down_crawler_user_agents.blank? user_agent = request.user_agent&.downcase return if user_agent.blank? SiteSetting .slow_down_crawler_user_agents .downcase .split("|") .each do |crawler| if user_agent.include?(crawler) key = "#{crawler}_crawler_rate_limit" limiter = RateLimiter.new(nil, key, 1, SiteSetting.slow_down_crawler_rate, error_code: key) limiter.performed! break end end end def run_second_factor!(action_class, action_data = nil) action = action_class.new(guardian, request, action_data) manager = SecondFactor::AuthManager.new(guardian, action) yield(manager) if block_given? result = manager.run!(request, params, secure_session) if !result.no_second_factors_enabled? && !result.second_factor_auth_completed? && !result.second_factor_auth_skipped? # should never happen, but I want to know if somehow it does! (osama) raise "2fa process ended up in a bad state!" end result end def link_preload @links_to_preload = [] yield response.headers["Link"] = @links_to_preload.join(", ") if !@links_to_preload.empty? end def spa_boot_request? request.get? && !(request.format && request.format.json?) && !request.xhr? end def load_font_map DiscourseFonts .fonts .each_with_object({}) do |font, font_map| next if !font[:variants] font_map[font[:key]] = font[:variants].map do |v| { url: "#{Discourse.base_url}/fonts/#{v[:filename]}?v=#{DiscourseFonts::VERSION}", weight: v[:weight], } end end end def fetch_limit_from_params(params: self.params, default:, max:) fetch_int_from_params(:limit, params: params, default: default, max: max) end def fetch_int_from_params(key, params: self.params, default:, min: 0, max: nil) key = key.to_sym if default.present? && ((max.present? && default > max) || (min.present? && default < min)) raise "default #{key.inspect} is not between #{min.inspect} and #{max.inspect}" end if params.has_key?(key) value = begin Integer(params[key]) rescue ArgumentError raise Discourse::InvalidParameters.new(key) end if (min.present? && value < min) || (max.present? && value > max) raise Discourse::InvalidParameters.new(key) end value else default end end def clean_xml response.body.gsub!(XmlCleaner::INVALID_CHARACTERS, "") end end