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' class ApplicationController < ActionController::Base include CurrentUser include CanonicalURL::ControllerExtensions include JsonError include GlobalPath 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? super clear_current_user render text: "['BAD CSRF']", status: 403 end end before_filter :set_current_user_for_logs before_filter :set_locale before_filter :set_mobile_view before_filter :inject_preview_style before_filter :disable_customization before_filter :block_if_readonly_mode before_filter :authorize_mini_profiler before_filter :preload_json before_filter :redirect_to_login_if_required before_filter :check_xhr after_filter :add_readonly_header layout :set_layout def has_escaped_fragment? SiteSetting.enable_escaped_fragments? && params.key?("_escaped_fragment_") end def use_crawler_layout? @use_crawler_layout ||= (has_escaped_fragment? || CrawlerDetection.crawler?(request.user_agent)) end def add_readonly_header response.headers['Discourse-Readonly'] = 'true' if Discourse.readonly_mode? end def slow_platform? request.user_agent =~ /Android/ 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 # If they hit the rate limiter rescue_from RateLimiter::LimitExceeded do |e| time_left = "" if e.available_in < 1.minute.to_i time_left = I18n.t("rate_limiter.seconds", count: e.available_in) elsif e.available_in < 1.hour.to_i time_left = I18n.t("rate_limiter.minutes", count: (e.available_in / 1.minute.to_i)) else time_left = I18n.t("rate_limiter.hours", count: (e.available_in / 1.hour.to_i)) end render_json_error I18n.t("rate_limiter.too_many_requests", time_left: time_left), type: :rate_limit, status: 429 end rescue_from PG::ReadOnlySqlTransaction do |e| Discourse.received_readonly! raise Discourse::ReadOnly end rescue_from Discourse::NotLoggedIn do |e| raise e if Rails.env.test? if (request.format && request.format.json?) || request.xhr? || !request.get? rescue_discourse_actions(:not_logged_in, 403, true) else redirect_to path("/") end end rescue_from Discourse::NotFound do rescue_discourse_actions(:not_found, 404) end rescue_from Discourse::InvalidAccess do rescue_discourse_actions(:invalid_access, 403, true) end rescue_from Discourse::ReadOnly do render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 405 end def rescue_discourse_actions(type, status_code, include_ember=false) if (request.format && request.format.json?) || (request.xhr?) # 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, text: (status_code == 404) ? build_not_found_page(status_code) : I18n.t(type) end render_json_error I18n.t(type), type: type, status: status_code else render text: build_not_found_page(status_code, include_ember ? 'application' : 'no_ember') end end class PluginDisabled < StandardError; 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_filter 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 set_locale I18n.locale = if current_user current_user.effective_locale else SiteSetting.default_locale end I18n.fallbacks.ensure_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("= SiteSetting.max_tracked_new_unread.to_i TopicUser.cap_unread_later(current_user.id) end serializer = ActiveModel::ArraySerializer.new(report, each_serializer: TopicTrackingStateSerializer) store_preloaded("topicTrackingStates", MultiJson.dump(serializer)) end def custom_html_json target = view_context.mobile_view? ? :mobile : :desktop data = { top: SiteCustomization.custom_top(session[:preview_style], target), footer: SiteCustomization.custom_footer(session[:preview_style], target) } if DiscoursePluginRegistry.custom_html data.merge! DiscoursePluginRegistry.custom_html end MultiJson.dump(data) end def self.banner_json_cache @banner_json_cache ||= DistributedCache.new("banner_json") end def banner_json json = ApplicationController.banner_json_cache["json"] unless json topic = Topic.where(archetype: Archetype.banner).limit(1).first banner = topic.present? ? topic.banner : {} ApplicationController.banner_json_cache["json"] = json = MultiJson.dump(banner) end json 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 def render_json_error(obj, opts={}) opts = { status: opts } if opts.is_a?(Fixnum) render json: MultiJson.dump(create_errors_json(obj, opts[:type])), status: opts[:status] || 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.send(o) target && target.errors.present? end error_obj = obj.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? 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? && api_key_valid? raise RenderEmpty.new unless ((request.format && request.format.json?) || request.xhr?) 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 destination_url request.original_url unless request.original_url =~ /uploads/ end def redirect_to_login_if_required return if current_user || (request.format.json? && api_key_valid?) # redirect user to the SSO page if we need to log in AND SSO is enabled if SiteSetting.login_required? if SiteSetting.enable_sso? # save original URL in a session so we can redirect after login session[:destination_url] = destination_url redirect_to path('/session/sso') else # save original URL in a cookie (javascript redirects after login in this case) cookies[:destination_url] = destination_url redirect_to :login end end end def block_if_readonly_mode return if request.fullpath.start_with?(path "/admin/backups") raise Discourse::ReadOnly.new if !(request.get? || request.head?) && Discourse.readonly_mode? end def build_not_found_page(status=404, layout=false) category_topic_ids = Category.pluck(:topic_id).compact @container_class = "wrap not-found-container" @top_viewed = Topic.where.not(id: category_topic_ids).top_viewed(10) @recent = Topic.where.not(id: category_topic_ids).recent(10) @slug = params[:slug].class == String ? params[:slug] : '' @slug = (params[:id].class == String ? params[:id] : '') if @slug.blank? @slug.gsub!('-',' ') render_to_string status: status, layout: layout, formats: [:html], template: '/exceptions/not_found' end protected 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 def api_key_valid? request["api_key"] && ApiKey.where(key: request["api_key"]).exists? end # returns an array of integers given a param key # returns nil if key is not found def param_to_integer_list(key, delimiter = ',') if params[key] params[key].split(delimiter).map(&:to_i) end end end