discourse/app/controllers/application_controller.rb
Chris Hunt 92a4828f72 Redirect all controllers to login if required
We want to skip the filter for sessions controller so that we can login
and we want to skip the filter for static pages because those should be
visible to visitors.
2013-06-04 16:10:10 -07:00

297 lines
8.4 KiB
Ruby

require 'current_user'
require 'canonical_url'
require_dependency 'discourse'
require_dependency 'custom_renderer'
require 'archetype'
require_dependency 'rate_limiter'
class ApplicationController < ActionController::Base
include CurrentUser
include CanonicalURL::ControllerExtensions
serialization_scope :guardian
protect_from_forgery
before_filter :inject_preview_style
before_filter :block_if_maintenance_mode
before_filter :check_restricted_access
before_filter :authorize_mini_profiler
before_filter :store_incoming_links
before_filter :preload_json
before_filter :check_xhr
before_filter :set_locale
before_filter :redirect_to_login_if_required
rescue_from Exception do |exception|
unless [ ActiveRecord::RecordNotFound, ActionController::RoutingError,
ActionController::UnknownController, AbstractController::ActionNotFound].include? exception.class
begin
ErrorLog.report_async!(exception, self, request, current_user)
rescue
# dont care give up
end
end
raise
end
# Some exceptions
class RenderEmpty < Exception; end
# Render nothing unless we are an xhr request
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: {errors: [I18n.t("rate_limiter.too_many_requests", time_left: time_left)]}, status: 429
end
rescue_from Discourse::NotLoggedIn do |e|
raise e if Rails.env.test?
redirect_to root_path
end
rescue_from Discourse::NotFound do
if request.format && request.format.json?
render status: 404, layout: false, text: "[error: 'not found']"
else
render_not_found_page(404)
end
end
rescue_from Discourse::InvalidAccess do
if request.format && request.format.json?
render status: 403, layout: false, text: "[error: 'invalid access']"
else
render_not_found_page(403)
end
end
def set_locale
I18n.locale = SiteSetting.default_locale
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("</", "<\\/")
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?
if guardian.current_user
guardian.current_user.sync_notification_channel_position
end
store_preloaded("site", Site.cached_json(current_user))
if current_user.present?
store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, root: false)))
serializer = ActiveModel::ArraySerializer.new(TopicTrackingState.report([current_user.id]), each_serializer: TopicTrackingStateSerializer)
store_preloaded("topicTrackingStates", MultiJson.dump(serializer))
end
store_preloaded("siteSettings", SiteSetting.client_settings_json)
end
def inject_preview_style
style = request['preview-style']
session[:preview_style] = style if style
end
def guardian
@guardian ||= Guardian.new(current_user)
end
def serialize_data(obj, serializer, opts={})
# If it's an array, apply the serializer as an each_serializer to the elements
serializer_opts = {scope: guardian}.merge!(opts)
if obj.is_a?(Array)
serializer_opts[:each_serializer] = serializer
ActiveModel::ArraySerializer.new(obj, 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={})
render_json_dump(serialize_data(obj, serializer, opts))
end
def render_json_dump(obj)
render json: MultiJson.dump(obj)
end
def can_cache_content?
# Don't cache unless we're in production mode
return false unless Rails.env.production? || Rails.env == "profile"
# Don't cache logged in users
return false if current_user.present?
# Don't cache if there's restricted access
return false if SiteSetting.access_password.present?
true
end
# Our custom cache method
def discourse_expires_in(time_length)
return unless can_cache_content?
expires_in time_length, public: true
end
# Helper method - if no logged in user (anonymous), use Rails' conditional GET
# support. Should be very fast behind a cache.
def anonymous_etag(*args)
if can_cache_content?
yield if stale?(*args)
# Add a one minute expiry
expires_in 1.minute, public: true
else
yield
end
end
def fetch_user_from_params
username_lower = params[:username].downcase
username_lower.gsub!(/\.json$/, '')
user = User.where(username_lower: username_lower).first
raise Discourse::NotFound.new if user.blank?
guardian.ensure_can_see!(user)
user
end
private
def render_json_error(obj)
if obj.present?
render json: MultiJson.dump(errors: obj.errors.full_messages), status: 422
else
render json: MultiJson.dump(errors: [I18n.t('js.generic_error')]), status: 422
end
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
render_json_error(obj)
end
end
def block_if_maintenance_mode
if Discourse.maintenance_mode?
if request.format.json?
render status: 503, json: failed_json.merge(message: I18n.t('site_under_maintenance'))
else
render status: 503, file: File.join( Rails.root, 'public', '503.html' ), layout: false
end
end
end
def check_restricted_access
# note current_user is defined in the CurrentUser mixin
if SiteSetting.access_password.present? && cookies[:_access] != SiteSetting.access_password
redirect_to request_access_path(return_path: request.fullpath)
return false
end
end
def mini_profiler_enabled?
defined?(Rack::MiniProfiler) && current_user.try(:admin?)
end
def authorize_mini_profiler
return unless mini_profiler_enabled?
Rack::MiniProfiler.authorize_request
end
def requires_parameters(*required)
required.each do |p|
raise Discourse::InvalidParameters.new(p) unless params.has_key?(p)
end
end
alias :requires_parameter :requires_parameters
def store_incoming_links
IncomingLink.add(request,current_user) unless request.xhr?
end
def check_xhr
unless (controller_name == 'forums' || controller_name == 'user_open_ids')
# bypass xhr check on PUT / POST / DELETE provided api key is there, otherwise calling api is annoying
return if !request.get? && request["api_key"] && SiteSetting.api_key_valid?(request["api_key"])
raise RenderEmpty.new unless ((request.format && request.format.json?) || request.xhr?)
end
end
def ensure_logged_in
raise Discourse::NotLoggedIn.new unless current_user.present?
end
def redirect_to_login_if_required
redirect_to :login if SiteSetting.login_required? && !current_user
end
def render_not_found_page(status=404)
f = Topic.where(deleted_at: nil, archetype: "regular")
@latest = f.order('views desc').take(10)
@recent = f.order('created_at desc').take(10)
@slug = params[:slug].class == String ? params[:slug] : ''
@slug.gsub!('-',' ')
render status: status, layout: 'no_js', template: '/exceptions/not_found'
end
end