# frozen_string_literal: true require 'current_user' require_dependency 'canonical_url' require_dependency 'discourse' require_dependency 'custom_renderer' require_dependency 'archetype' require_dependency 'rate_limiter' require_dependency 'crawler_detection' require_dependency 'json_error' require_dependency 'letter_avatar' require_dependency 'distributed_cache' require_dependency 'global_path' require_dependency 'secure_session' require_dependency 'topic_query' require_dependency 'hijack' class ApplicationController < ActionController::Base include CurrentUser include CanonicalURL::ControllerExtensions include JsonError include GlobalPath include Hijack attr_reader :theme_ids 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 :check_readonly_mode before_action :handle_theme before_action :set_current_user_for_logs before_action :clear_notifications before_action :set_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 layout :set_layout if Rails.env == "development" after_action :remember_theme_id def remember_theme_id if @theme_ids.present? Stylesheet::Watcher.theme_id = @theme_ids.first if defined? Stylesheet::Watcher end end end def has_escaped_fragment? SiteSetting.enable_escaped_fragments? && params.key?("_escaped_fragment_") end def use_crawler_layout? @use_crawler_layout ||= request.user_agent && (request.content_type.blank? || request.content_type.include?('html')) && !['json', 'rss'].include?(params[:format]) && (has_escaped_fragment? || CrawlerDetection.crawler?(request.user_agent) || params.key?("print")) end def add_readonly_header response.headers['Discourse-Readonly'] = 'true' if @readonly_mode 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 end def set_layout use_crawler_layout? ? 'crawler' : 'application' end # Some exceptions class RenderEmpty < StandardError; end # Render nothing rescue_from RenderEmpty do render 'default/empty' end def render_rate_limit_error(e) retry_time_in_seconds = e&.available_in render_json_error( e.description, type: :rate_limit, status: 429, extras: { wait_seconds: retry_time_in_seconds }, headers: { 'Retry-After': retry_time_in_seconds }, ) 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 # If they hit the rate limiter rescue_from RateLimiter::LimitExceeded do |e| render_rate_limit_error(e) end rescue_from PG::ReadOnlySqlTransaction do |e| Discourse.received_readonly! Rails.logger.error("#{e.class} #{e.message}: #{e.backtrace.join("\n")}") raise Discourse::ReadOnly 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 ArgumentError do |e| if e.message == "string contains null byte" raise Discourse::InvalidParameters, e.message else raise e end end rescue_from Discourse::InvalidParameters do |e| message = I18n.t('invalid_params', message: e.message) if (request.format && request.format.json?) || request.xhr? || !request.get? rescue_discourse_actions(:invalid_parameters, 400, include_ember: true, custom_message_translated: message) else rescue_discourse_actions(:not_found, 400, custom_message_translated: message) end end rescue_from ActiveRecord::StatementInvalid do |e| Discourse.reset_active_record_cache_if_needed(e) raise e end class PluginDisabled < StandardError; 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 Discourse::NotFound do |e| rescue_discourse_actions( :not_found, e.status, check_permalinks: e.check_permalinks, original_path: e.original_path ) end rescue_from PluginDisabled, ActionController::RoutingError do rescue_discourse_actions(:not_found, 404) end rescue_from Discourse::InvalidAccess do |e| if e.opts[:delete_cookie].present? cookies.delete(e.opts[:delete_cookie]) end rescue_discourse_actions( :invalid_access, 403, include_ember: true, custom_message: e.custom_message ) end rescue_from Discourse::ReadOnly do render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 503 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 return end end message = opts[:custom_message_translated] || I18n.t(opts[:custom_message] || type) if show_json_errors # HACK: do not use render_json_error for topics#show if request.params[:controller] == 'topics' && request.params[:action] == 'show' return render status: status_code, layout: false, plain: (status_code == 404 || status_code == 410) ? build_not_found_page(status_code) : message end render_json_error message, type: type, status: status_code 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 render html: build_not_found_page(status_code, opts[:include_ember] ? 'application' : 'no_ember') end end # If a controller requires a plugin, it will raise an exception if that plugin is # disabled. This allows plugins to be disabled programatically. def self.requires_plugin(plugin_name) before_action do raise PluginDisabled.new if Discourse.disabled_plugin_names.include?(plugin_name) 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 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_uri if Discourse.base_uri.present? cookies.delete('cn', cookie_args) end end end def set_locale if !current_user if SiteSetting.set_locale_from_accept_language_header locale = locale_from_header else locale = SiteSetting.default_locale end else locale = current_user.effective_locale end I18n.locale = I18n.locale_available?(locale) ? locale : :en I18n.ensure_all_loaded! 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 safty deeper in the library or even in AM serializer @preloaded[key] = json.gsub("