discourse/app/controllers/application_controller.rb

267 lines
7.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
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
class NotLoggedIn < 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.html?
# for now do a simple remap, we may look at cleaner ways of doing the render
raise ActiveRecord::RecordNotFound
else
render file: 'public/404', formats: [:html], layout: false, status: 404
end
end
rescue_from Discourse::InvalidAccess do
render file: 'public/403', formats: [:html], layout: false, status: 403
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
if request.format && request.format.html?
if guardian.current_user
guardian.current_user.sync_notification_channel_position
end
store_preloaded("site", Site.cached_json)
if current_user.present?
store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, root: false)))
end
store_preloaded("siteSettings", SiteSetting.client_settings_json)
end
end
def inject_preview_style
style = request['preview-style']
session[:preview_style] = style if style
end
def guardian
@guardian ||= Guardian.new(current_user)
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={})
# 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
render_json_dump(ActiveModel::ArraySerializer.new(obj, serializer_opts).as_json)
else
render_json_dump(serializer.new(obj, serializer_opts).as_json)
end
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?
# 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
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).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: 'Site is currently undergoing 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
if request.referer.present?
parsed = URI.parse(request.referer)
if parsed.host != request.host
IncomingLink.create(url: request.url, referer: request.referer[0..999])
end
end
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
if !request.get? && request["api_key"]
return
end
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
end