2013-10-09 17:22:41 +11:00
require 'current_user'
2013-10-09 15:10:37 +11:00
require_dependency 'canonical_url'
2013-02-05 14:16:51 -05:00
require_dependency 'discourse'
require_dependency 'custom_renderer'
2013-10-09 15:10:37 +11:00
require_dependency 'archetype'
2013-02-05 14:16:51 -05:00
require_dependency 'rate_limiter'
2014-02-20 16:07:02 -05:00
require_dependency 'crawler_detection'
2014-04-02 13:22:10 -04:00
require_dependency 'json_error'
2014-05-30 14:17:35 +10:00
require_dependency 'letter_avatar'
2014-11-14 15:39:17 +11:00
require_dependency 'distributed_cache'
2015-03-09 15:24:16 -04:00
require_dependency 'global_path'
2013-02-05 14:16:51 -05:00
class ApplicationController < ActionController :: Base
include CurrentUser
2013-02-13 19:04:43 +08:00
include CanonicalURL :: ControllerExtensions
2014-04-02 13:22:10 -04:00
include JsonError
2015-03-09 15:24:16 -04:00
include GlobalPath
2013-02-05 14:16:51 -05:00
serialization_scope :guardian
protect_from_forgery
2013-07-29 15:13:13 +10:00
# 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
2013-08-27 15:56:12 +10:00
render text : " ['BAD CSRF'] " , status : 403
2013-07-29 15:13:13 +10:00
end
end
2014-05-12 15:27:58 +10:00
before_filter :set_current_user_for_logs
2016-02-15 19:29:35 +11:00
before_filter :clear_notifications
2013-12-04 20:49:54 +09:00
before_filter :set_locale
2013-12-18 14:47:22 -05:00
before_filter :set_mobile_view
2013-02-05 14:16:51 -05:00
before_filter :inject_preview_style
2013-11-12 18:13:17 +01:00
before_filter :disable_customization
2014-02-12 20:37:28 -08:00
before_filter :block_if_readonly_mode
2013-02-05 14:16:51 -05:00
before_filter :authorize_mini_profiler
before_filter :preload_json
2013-06-04 15:32:36 -07:00
before_filter :redirect_to_login_if_required
2015-05-21 14:37:49 -04:00
before_filter :check_xhr
2015-04-24 14:32:18 -04:00
after_filter :add_readonly_header
2013-02-07 16:45:24 +01:00
2014-02-14 17:10:08 -05:00
layout :set_layout
2014-02-20 17:02:26 -05:00
def has_escaped_fragment?
SiteSetting . enable_escaped_fragments? && params . key? ( " _escaped_fragment_ " )
end
2014-10-30 14:26:35 -04:00
def use_crawler_layout?
@use_crawler_layout || = ( has_escaped_fragment? || CrawlerDetection . crawler? ( request . user_agent ) )
end
2015-04-24 14:32:18 -04:00
def add_readonly_header
response . headers [ 'Discourse-Readonly' ] = 'true' if Discourse . readonly_mode?
end
2014-12-15 11:54:26 -05:00
def slow_platform?
request . user_agent =~ / Android /
end
2014-02-14 17:10:08 -05:00
def set_layout
2014-10-30 14:26:35 -04:00
use_crawler_layout? ? 'crawler' : 'application'
2014-02-14 17:10:08 -05:00
end
2013-02-05 14:16:51 -05:00
# Some exceptions
2015-03-23 12:20:50 +11:00
class RenderEmpty < StandardError ; end
2013-02-05 14:16:51 -05:00
2015-02-08 12:25:03 -08:00
# Render nothing
2013-02-05 14:16:51 -05:00
rescue_from RenderEmpty do
render 'default/empty'
end
# If they hit the rate limiter
rescue_from RateLimiter :: LimitExceeded do | e |
2015-09-24 12:25:09 -04:00
render_json_error e . description , type : :rate_limit , status : 429
2013-02-05 14:16:51 -05:00
end
2015-04-29 11:49:58 -04:00
rescue_from PG :: ReadOnlySqlTransaction do | e |
Discourse . received_readonly!
raise Discourse :: ReadOnly
end
2013-02-05 14:16:51 -05:00
rescue_from Discourse :: NotLoggedIn do | e |
raise e if Rails . env . test?
2015-02-08 13:35:09 -08:00
if ( request . format && request . format . json? ) || request . xhr? || ! request . get?
2015-02-09 10:19:47 -08:00
rescue_discourse_actions ( :not_logged_in , 403 , true )
2014-03-05 11:48:06 -05:00
else
2015-10-30 17:21:16 +05:30
rescue_discourse_actions ( :not_found , 404 )
2014-03-05 11:48:06 -05:00
end
2013-02-05 14:16:51 -05:00
end
rescue_from Discourse :: NotFound do
2015-02-09 10:19:47 -08:00
rescue_discourse_actions ( :not_found , 404 )
2013-02-05 14:16:51 -05:00
end
rescue_from Discourse :: InvalidAccess do
2015-02-09 10:19:47 -08:00
rescue_discourse_actions ( :invalid_access , 403 , true )
2013-06-20 21:17:33 +05:30
end
2014-02-12 20:37:28 -08:00
rescue_from Discourse :: ReadOnly do
2015-02-22 21:28:50 -08:00
render_json_error I18n . t ( 'read_only_mode_enabled' ) , type : :read_only , status : 405
2014-02-12 20:37:28 -08:00
end
2015-02-08 12:25:03 -08:00
def rescue_discourse_actions ( type , status_code , include_ember = false )
2015-02-08 13:35:09 -08:00
2015-02-08 12:25:03 -08:00
if ( request . format && request . format . json? ) || ( request . xhr? )
2015-02-08 13:35:09 -08:00
# HACK: do not use render_json_error for topics#show
if request . params [ :controller ] == 'topics' && request . params [ :action ] == 'show'
2015-12-30 15:45:54 +05:00
return render status : status_code , layout : false , text : ( status_code == 404 || status_code == 410 ) ? build_not_found_page ( status_code ) : I18n . t ( type )
2015-02-08 13:35:09 -08:00
end
2015-02-22 21:28:50 -08:00
render_json_error I18n . t ( type ) , type : type , status : status_code
2013-05-30 14:46:02 -04:00
else
2015-02-08 12:25:03 -08:00
render text : build_not_found_page ( status_code , include_ember ? 'application' : 'no_ember' )
2013-05-30 14:46:02 -04:00
end
2013-02-05 14:16:51 -05:00
end
2015-03-23 12:20:50 +11:00
class PluginDisabled < StandardError ; end
2015-02-04 16:23:39 -05:00
# 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
2014-05-12 15:27:58 +10:00
def set_current_user_for_logs
if current_user
Logster . add_to_env ( request . env , " username " , current_user . username )
2015-06-16 17:43:36 +10:00
response . headers [ " X-Discourse-Username " ] = current_user . username
2014-05-12 15:27:58 +10:00
end
2015-06-16 10:27:42 +10:00
response . headers [ " X-Discourse-Route " ] = " #{ controller_name } / #{ action_name } "
2014-05-12 15:27:58 +10:00
end
2016-02-15 19:29:35 +11:00
def clear_notifications
if current_user && ! Discourse . readonly_mode?
cookie_notifications = cookies [ 'cn' . freeze ]
notifications = request . headers [ 'Discourse-Clear-Notifications' . freeze ]
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 . where ( user_id : current_user . id , id : notification_ids ) . update_all ( read : true )
cookies . delete ( 'cn' )
end
end
end
2013-02-28 14:31:39 -05:00
def set_locale
2015-11-13 15:42:01 -05:00
I18n . locale = current_user . try ( :effective_locale ) || SiteSetting . default_locale
I18n . ensure_all_loaded!
2013-02-28 14:31:39 -05:00
end
2013-02-05 14:16:51 -05:00
def store_preloaded ( key , json )
@preloaded || = { }
2013-02-25 19:42:20 +03: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 17:28:21 +11:00
# to inject this safty deeper in the library or even in AM serializer
@preloaded [ key ] = json . gsub ( " </ " , " < \\ / " )
2013-02-05 14:16:51 -05:00
end
# If we are rendering HTML, preload the session data
def preload_json
2013-06-04 12:56:12 -04:00
# We don't preload JSON on xhr or JSON request
2015-01-23 21:03:44 -08:00
return if request . xhr? || request . format . json?
2013-02-05 14:16:51 -05:00
2015-05-20 17:12:16 +10: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-06 06:25:44 +10:00
preload_anonymous_data
2013-06-04 12:56:12 -04:00
2013-08-06 06:25:44 +10:00
if current_user
preload_current_user_data
current_user . sync_notification_channel_position
2013-02-05 14:16:51 -05:00
end
end
2013-12-18 14:47:22 -05:00
def set_mobile_view
session [ :mobile_view ] = params [ :mobile_view ] if params . has_key? ( :mobile_view )
end
2013-02-05 14:16:51 -05:00
def inject_preview_style
style = request [ 'preview-style' ]
2015-01-06 17:39:08 +11:00
if style . nil?
session [ :preview_style ] = cookies [ :preview_style ]
2014-06-20 09:06:36 -07:00
else
2015-01-06 17:39:08 +11:00
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
2014-06-20 09:06:36 -07:00
end
2015-01-06 17:39:08 +11:00
2013-02-05 14:16:51 -05:00
end
2013-11-12 18:13:17 +01:00
def disable_customization
session [ :disable_customization ] = params [ :customization ] == " 0 " if params . has_key? ( :customization )
end
2013-02-05 14:16:51 -05:00
def guardian
@guardian || = Guardian . new ( current_user )
end
2015-06-08 12:07:35 -04:00
def current_homepage
current_user ? SiteSetting . homepage : SiteSetting . anonymous_homepage
end
2015-03-31 12:58:56 -04:00
def serialize_data ( obj , serializer , opts = nil )
2013-02-05 14:16:51 -05:00
# If it's an array, apply the serializer as an each_serializer to the elements
2015-03-31 12:58:56 -04:00
serializer_opts = { scope : guardian } . merge! ( opts || { } )
2014-04-16 20:48:09 +05:30
if obj . respond_to? ( :to_ary )
2013-02-05 14:16:51 -05:00
serializer_opts [ :each_serializer ] = serializer
2014-04-16 20:48:09 +05:30
ActiveModel :: ArraySerializer . new ( obj . to_ary , serializer_opts ) . as_json
2013-02-07 16:45:24 +01:00
else
2013-05-29 16:49:34 -04:00
serializer . new ( obj , serializer_opts ) . as_json
2013-02-05 14:16:51 -05:00
end
2013-05-29 16:49:34 -04:00
end
2013-02-05 14:16:51 -05:00
2013-05-29 16:49:34 -04: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.
2015-03-31 12:58:56 -04:00
def render_serialized ( obj , serializer , opts = nil )
render_json_dump ( serialize_data ( obj , serializer , opts ) , opts )
2013-02-05 14:16:51 -05:00
end
2015-03-31 12:58:56 -04:00
def render_json_dump ( obj , opts = nil )
opts || = { }
2015-04-27 13:52:37 -04: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
2015-11-30 15:22:58 -05:00
obj [ 'extras' ] = opts [ :extras ] if opts [ :extras ]
2015-04-27 13:52:37 -04:00
end
2015-03-31 12:58:56 -04:00
render json : MultiJson . dump ( obj ) , status : opts [ :status ] || 200
2013-02-05 14:16:51 -05:00
end
2013-02-06 11:55:54 -05:00
def can_cache_content?
2015-10-28 15:11:36 -04:00
current_user . blank? && flash [ :authentication_data ] . blank?
2013-02-06 11:55:54 -05:00
end
# Our custom cache method
def discourse_expires_in ( time_length )
return unless can_cache_content?
2014-01-04 08:53:27 +01:00
Middleware :: AnonymousCache . anon_cache ( request . env , time_length )
2013-02-05 14:16:51 -05:00
end
2014-08-28 12:07:13 -04:00
def fetch_user_from_params ( opts = nil )
opts || = { }
2014-06-18 14:40:15 -04:00
user = if params [ :username ]
username_lower = params [ :username ] . downcase
username_lower . gsub! ( / \ .json$ / , '' )
2016-01-15 12:16:00 +01:00
find_opts = { username_lower : username_lower }
2016-01-15 12:34:28 +01:00
find_opts [ :active ] = true unless opts [ :include_inactive ] || current_user . try ( :staff? )
2014-08-28 12:07:13 -04:00
User . find_by ( find_opts )
2014-06-18 14:40:15 -04:00
elsif params [ :external_id ]
2015-02-02 12:58:02 -05:00
external_id = params [ :external_id ] . gsub ( / \ .json$ / , '' )
SingleSignOnRecord . find_by ( external_id : external_id ) . try ( :user )
2014-06-18 14:40:15 -04:00
end
2015-05-07 11:00:51 +10:00
raise Discourse :: NotFound if user . blank?
2013-05-22 11:20:16 -04:00
guardian . ensure_can_see! ( user )
user
end
2013-09-04 11:53:00 -04: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
2015-05-22 16:15:46 +10:00
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 14:16:51 -05:00
private
2013-08-06 06:25:44 +10:00
def preload_anonymous_data
2014-02-24 14:24:18 -05:00
store_preloaded ( " site " , Site . json_for ( guardian ) )
2013-08-06 06:25:44 +10:00
store_preloaded ( " siteSettings " , SiteSetting . client_settings_json )
2013-12-17 18:49:22 +01:00
store_preloaded ( " customHTML " , custom_html_json )
2014-06-18 20:04:10 +02:00
store_preloaded ( " banner " , banner_json )
2014-12-23 01:12:26 +01:00
store_preloaded ( " customEmoji " , custom_emoji )
2015-12-23 12:09:18 -05:00
store_preloaded ( " translationOverrides " , I18n . client_overrides_json ( I18n . locale ) )
2013-08-06 06:25:44 +10:00
end
def preload_current_user_data
2013-11-06 12:56:26 -05:00
store_preloaded ( " currentUser " , MultiJson . dump ( CurrentUserSerializer . new ( current_user , scope : guardian , root : false ) ) )
2015-09-07 11:57:50 +10:00
report = TopicTrackingState . report ( current_user . id )
serializer = ActiveModel :: ArraySerializer . new ( report , each_serializer : TopicTrackingStateSerializer )
2013-08-06 06:25:44 +10:00
store_preloaded ( " topicTrackingStates " , MultiJson . dump ( serializer ) )
end
2013-12-17 18:49:22 +01:00
def custom_html_json
2015-02-10 00:48:42 +08:00
target = view_context . mobile_view? ? :mobile : :desktop
2015-01-12 20:18:52 -05:00
data = {
2015-02-10 00:48:42 +08:00
top : SiteCustomization . custom_top ( session [ :preview_style ] , target ) ,
footer : SiteCustomization . custom_footer ( session [ :preview_style ] , target )
2015-01-12 20:18:52 -05:00
}
2014-06-05 11:39:33 +10:00
if DiscoursePluginRegistry . custom_html
data . merge! DiscoursePluginRegistry . custom_html
end
MultiJson . dump ( data )
2013-12-17 18:25:27 +01:00
end
2014-11-14 15:39:17 +11:00
def self . banner_json_cache
@banner_json_cache || = DistributedCache . new ( " banner_json " )
end
2014-06-18 20:04:10 +02:00
def banner_json
2014-11-14 15:39:17 +11:00
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 20:04:10 +02:00
end
2014-12-23 01:12:26 +01:00
def custom_emoji
serializer = ActiveModel :: ArraySerializer . new ( Emoji . custom , each_serializer : EmojiSerializer )
MultiJson . dump ( serializer )
end
2015-02-08 12:25:03 -08:00
# Render action for a JSON error.
#
2015-02-22 21:28:50 -08: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 = { } )
2015-03-19 12:22:56 +01:00
opts = { status : opts } if opts . is_a? ( Fixnum )
2015-02-22 21:28:50 -08:00
render json : MultiJson . dump ( create_errors_json ( obj , opts [ :type ] ) ) , status : opts [ :status ] || 422
2013-02-05 14:16:51 -05:00
end
def success_json
2014-06-18 20:04:10 +02:00
{ success : 'OK' }
2013-02-05 14:16:51 -05:00
end
def failed_json
2014-06-18 20:04:10 +02:00
{ failed : 'FAILED' }
2013-02-05 14:16:51 -05: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?
2013-04-12 10:07:46 +10:00
json [ obj . class . name . underscore ] = opts [ :serializer ] . new ( obj , scope : guardian ) . serializable_hash
2013-02-05 14:16:51 -05:00
end
render json : MultiJson . dump ( json )
else
2014-09-08 15:17:31 -04:00
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 14:16:51 -05:00
end
2013-02-07 16:45:24 +01:00
end
2013-02-05 14:16:51 -05:00
def mini_profiler_enabled?
2014-07-17 08:34:30 +10:00
defined? ( Rack :: MiniProfiler ) && guardian . is_developer?
2013-02-05 14:16:51 -05:00
end
def authorize_mini_profiler
return unless mini_profiler_enabled?
Rack :: MiniProfiler . authorize_request
end
2013-02-07 16:45:24 +01:00
def check_xhr
2013-08-06 06:25:44 +10: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 14:16:51 -05:00
end
def ensure_logged_in
raise Discourse :: NotLoggedIn . new unless current_user . present?
end
2013-02-07 16:45:24 +01:00
2015-04-10 17:00:50 -04:00
def ensure_staff
raise Discourse :: InvalidAccess . new unless current_user && current_user . staff?
end
2015-08-11 16:27:56 +01:00
def destination_url
request . original_url unless request . original_url =~ / uploads /
end
2013-06-04 15:32:36 -07:00
def redirect_to_login_if_required
2014-01-24 13:47:35 +01:00
return if current_user || ( request . format . json? && api_key_valid? )
2015-02-23 21:20:36 +00:00
# redirect user to the SSO page if we need to log in AND SSO is enabled
2015-02-23 22:10:44 +00:00
if SiteSetting . login_required?
if SiteSetting . enable_sso?
2015-08-11 16:27:56 +01:00
# save original URL in a session so we can redirect after login
session [ :destination_url ] = destination_url
2015-03-09 11:45:36 +11:00
redirect_to path ( '/session/sso' )
2015-02-23 22:10:44 +00:00
else
2015-08-11 16:27:56 +01:00
# save original URL in a cookie (javascript redirects after login in this case)
cookies [ :destination_url ] = destination_url
2015-02-23 22:10:44 +00:00
redirect_to :login
end
2015-02-23 21:20:36 +00:00
end
2013-06-04 15:32:36 -07:00
end
2014-02-12 20:37:28 -08:00
def block_if_readonly_mode
2015-03-09 11:45:36 +11:00
return if request . fullpath . start_with? ( path " /admin/backups " )
2015-04-24 14:32:18 -04:00
raise Discourse :: ReadOnly . new if ! ( request . get? || request . head? ) && Discourse . readonly_mode?
2014-02-12 20:37:28 -08:00
end
2013-07-11 16:38:46 -04:00
def build_not_found_page ( status = 404 , layout = false )
2014-07-04 16:18:09 -04:00
category_topic_ids = Category . pluck ( :topic_id ) . compact
2015-07-28 13:32:39 +05:30
@container_class = " wrap not-found-container "
2014-07-04 16:18:09 -04:00
@top_viewed = Topic . where . not ( id : category_topic_ids ) . top_viewed ( 10 )
@recent = Topic . where . not ( id : category_topic_ids ) . recent ( 10 )
2013-05-30 14:46:02 -04:00
@slug = params [ :slug ] . class == String ? params [ :slug ] : ''
2013-06-06 14:41:27 -04:00
@slug = ( params [ :id ] . class == String ? params [ :id ] : '' ) if @slug . blank?
2013-05-30 14:46:02 -04:00
@slug . gsub! ( '-' , ' ' )
2013-07-11 16:38:46 -04:00
render_to_string status : status , layout : layout , formats : [ :html ] , template : '/exceptions/not_found'
2013-05-30 14:46:02 -04:00
end
2013-06-19 19:11:14 -07:00
protected
2013-06-17 16:09:59 +10:00
2014-09-25 12:02:41 +10:00
def render_post_json ( post , add_raw = true )
2014-09-02 17:37:19 -04:00
post_serializer = PostSerializer . new ( post , scope : guardian , root : false )
2014-09-25 12:02:41 +10:00
post_serializer . add_raw = add_raw
2014-09-02 17:37:19 -04:00
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
2013-06-17 16:09:59 +10:00
def api_key_valid?
2013-10-22 15:53:08 -04:00
request [ " api_key " ] && ApiKey . where ( key : request [ " api_key " ] ) . exists?
2013-06-17 16:09:59 +10:00
end
2013-06-19 19:11:14 -07:00
# 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 14:16:51 -05:00
end