discourse/app/controllers/application_controller.rb

479 lines
15 KiB
Ruby
Raw Normal View History

require 'current_user'
require_dependency 'canonical_url'
2013-02-05 13:16:51 -06:00
require_dependency 'discourse'
require_dependency 'custom_renderer'
require_dependency 'archetype'
2013-02-05 13:16:51 -06:00
require_dependency 'rate_limiter'
require_dependency 'crawler_detection'
require_dependency 'json_error'
require_dependency 'letter_avatar'
require_dependency 'distributed_cache'
require_dependency 'global_path'
2013-02-05 13:16:51 -06:00
class ApplicationController < ActionController::Base
include CurrentUser
include CanonicalURL::ControllerExtensions
include JsonError
include GlobalPath
2013-02-05 13:16:51 -06:00
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
2013-02-05 13:16:51 -06:00
before_filter :inject_preview_style
before_filter :disable_customization
2014-02-12 22:37:28 -06:00
before_filter :block_if_readonly_mode
2013-02-05 13:16:51 -06:00
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
2013-02-07 09:45:24 -06:00
layout :set_layout
def has_escaped_fragment?
SiteSetting.enable_escaped_fragments? && params.key?("_escaped_fragment_")
end
2014-10-30 13:26:35 -05:00
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
2014-10-30 13:26:35 -05:00
use_crawler_layout? ? 'crawler' : 'application'
end
2013-02-05 13:16:51 -06:00
# Some exceptions
class RenderEmpty < StandardError; end
2013-02-05 13:16:51 -06:00
# Render nothing
2013-02-05 13:16:51 -06:00
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
2013-02-07 09:45:24 -06:00
time_left = I18n.t("rate_limiter.minutes", count: (e.available_in / 1.minute.to_i))
2013-02-05 13:16:51 -06:00
else
2013-02-07 09:45:24 -06:00
time_left = I18n.t("rate_limiter.hours", count: (e.available_in / 1.hour.to_i))
2013-02-05 13:16:51 -06:00
end
2015-02-22 23:28:50 -06:00
render_json_error I18n.t("rate_limiter.too_many_requests", time_left: time_left), type: :rate_limit, status: 429
2013-02-05 13:16:51 -06:00
end
rescue_from PG::ReadOnlySqlTransaction do |e|
Discourse.received_readonly!
raise Discourse::ReadOnly
end
2013-02-05 13:16:51 -06:00
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
2013-02-05 13:16:51 -06:00
end
rescue_from Discourse::NotFound do
rescue_discourse_actions(:not_found, 404)
2013-02-05 13:16:51 -06:00
end
rescue_from Discourse::InvalidAccess do
rescue_discourse_actions(:invalid_access, 403, true)
end
2014-02-12 22:37:28 -06:00
rescue_from Discourse::ReadOnly do
2015-02-22 23:28:50 -06:00
render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 405
2014-02-12 22:37:28 -06:00
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
2015-02-22 23:28:50 -06:00
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
2013-02-05 13:16:51 -06:00
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)
2015-06-16 02:43:36 -05:00
response.headers["X-Discourse-Username"] = current_user.username
end
response.headers["X-Discourse-Route"] = "#{controller_name}/#{action_name}"
end
2013-02-28 13:31:39 -06:00
def set_locale
I18n.locale = if current_user
current_user.effective_locale
else
SiteSetting.default_locale
end
I18n.fallbacks.ensure_loaded!
2013-02-28 13:31:39 -06:00
end
2013-02-05 13:16:51 -06:00
def store_preloaded(key, json)
@preloaded ||= {}
2013-02-25 10:42:20 -06:00
# 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
2013-02-11 00:28:21 -06:00
# to inject this safty deeper in the library or even in AM serializer
@preloaded[key] = json.gsub("</", "<\\/")
2013-02-05 13:16:51 -06:00
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?
2013-02-05 13:16:51 -06:00
# 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
2013-08-05 15:25:44 -05:00
preload_anonymous_data
2013-08-05 15:25:44 -05:00
if current_user
preload_current_user_data
current_user.sync_notification_channel_position
2013-02-05 13:16:51 -06:00
end
end
def set_mobile_view
session[:mobile_view] = params[:mobile_view] if params.has_key?(:mobile_view)
end
2013-02-05 13:16:51 -06:00
def inject_preview_style
style = request['preview-style']
if style.nil?
session[:preview_style] = cookies[:preview_style]
else
cookies.delete(:preview_style)
if style.blank? || style == 'default'
session[:preview_style] = nil
else
session[:preview_style] = style
if request['sticky']
cookies[:preview_style] = style
end
end
end
2013-02-05 13:16:51 -06:00
end
def disable_customization
session[:disable_customization] = params[:customization] == "0" if params.has_key?(:customization)
end
2013-02-05 13:16:51 -06:00
def guardian
@guardian ||= Guardian.new(current_user)
end
def current_homepage
current_user ? SiteSetting.homepage : SiteSetting.anonymous_homepage
end
def serialize_data(obj, serializer, opts=nil)
2013-02-05 13:16:51 -06:00
# 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)
2013-02-05 13:16:51 -06:00
serializer_opts[:each_serializer] = serializer
ActiveModel::ArraySerializer.new(obj.to_ary, serializer_opts).as_json
2013-02-07 09:45:24 -06:00
else
serializer.new(obj, serializer_opts).as_json
2013-02-05 13:16:51 -06:00
end
end
2013-02-05 13:16:51 -06:00
# 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)
2013-02-05 13:16:51 -06:00
end
def render_json_dump(obj, opts=nil)
opts ||= {}
2015-04-27 12:52:37 -05:00
if opts[:rest_serializer]
obj['__rest_serializer'] = "1"
opts.each do |k, v|
obj[k] = v if k.to_s.start_with?("refresh_")
end
end
render json: MultiJson.dump(obj), status: opts[:status] || 200
2013-02-05 13:16:51 -06:00
end
def can_cache_content?
!current_user.present?
end
# Our custom cache method
def discourse_expires_in(time_length)
return unless can_cache_content?
2014-01-04 01:53:27 -06:00
Middleware::AnonymousCache.anon_cache(request.env, time_length)
2013-02-05 13:16:51 -06:00
end
def fetch_user_from_params(opts=nil)
opts ||= {}
user = if params[:username]
username_lower = params[:username].downcase
username_lower.gsub!(/\.json$/, '')
find_opts = {username_lower: username_lower}
find_opts[:active] = true unless opts[:include_inactive]
User.find_by(find_opts)
elsif params[:external_id]
external_id = params[:external_id].gsub(/\.json$/, '')
SingleSignOnRecord.find_by(external_id: external_id).try(:user)
end
2015-05-06 20:00:51 -05:00
raise Discourse::NotFound if user.blank?
guardian.ensure_can_see!(user)
user
end
2013-09-04 10:53:00 -05:00
def post_ids_including_replies
post_ids = params[:post_ids].map {|p| p.to_i}
if params[:reply_post_ids]
post_ids << PostReply.where(post_id: params[:reply_post_ids].map {|p| p.to_i}).pluck(:reply_id)
post_ids.flatten!
post_ids.uniq!
end
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
2013-02-05 13:16:51 -06:00
private
2013-08-05 15:25:44 -05:00
def preload_anonymous_data
store_preloaded("site", Site.json_for(guardian))
2013-08-05 15:25:44 -05:00
store_preloaded("siteSettings", SiteSetting.client_settings_json)
store_preloaded("customHTML", custom_html_json)
2014-06-18 13:04:10 -05:00
store_preloaded("banner", banner_json)
2014-12-22 18:12:26 -06:00
store_preloaded("customEmoji", custom_emoji)
2013-08-05 15:25:44 -05:00
end
def preload_current_user_data
store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false)))
serializer = ActiveModel::ArraySerializer.new(TopicTrackingState.report(current_user.id), each_serializer: TopicTrackingStateSerializer)
2013-08-05 15:25:44 -05:00
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
2014-06-18 13:04:10 -05:00
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
2014-06-18 13:04:10 -05:00
end
2014-12-22 18:12:26 -06:00
def custom_emoji
serializer = ActiveModel::ArraySerializer.new(Emoji.custom, each_serializer: EmojiSerializer)
MultiJson.dump(serializer)
end
# Render action for a JSON error.
#
2015-02-22 23:28:50 -06:00
# 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)
2015-02-22 23:28:50 -06:00
render json: MultiJson.dump(create_errors_json(obj, opts[:type])), status: opts[:status] || 422
2013-02-05 13:16:51 -06:00
end
def success_json
2014-06-18 13:04:10 -05:00
{ success: 'OK' }
2013-02-05 13:16:51 -06:00
end
def failed_json
2014-06-18 13:04:10 -05:00
{ failed: 'FAILED' }
2013-02-05 13:16:51 -06:00
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
2013-02-05 13:16:51 -06:00
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)
2013-02-05 13:16:51 -06:00
end
2013-02-07 09:45:24 -06:00
end
2013-02-05 13:16:51 -06:00
def mini_profiler_enabled?
defined?(Rack::MiniProfiler) && guardian.is_developer?
2013-02-05 13:16:51 -06:00
end
def authorize_mini_profiler
return unless mini_profiler_enabled?
Rack::MiniProfiler.authorize_request
end
2013-02-07 09:45:24 -06:00
def check_xhr
2013-08-05 15:25:44 -05:00
# 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?)
2013-02-05 13:16:51 -06:00
end
def ensure_logged_in
raise Discourse::NotLoggedIn.new unless current_user.present?
end
2013-02-07 09:45:24 -06:00
2015-04-10 16:00:50 -05:00
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
2014-02-12 22:37:28 -06:00
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?
2014-02-12 22:37:28 -06:00
end
def build_not_found_page(status=404, layout=false)
category_topic_ids = Category.pluck(:topic_id).compact
2015-07-28 03:02:39 -05:00
@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?
2013-10-22 14:53:08 -05:00
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
2013-02-05 13:16:51 -06:00
end