mirror of
https://github.com/discourse/discourse.git
synced 2025-05-28 13:51:18 +08:00
SECURITY: Preload data only when rendering application layout
This commit drops the `before_action :preload_json` callback in `ApplicationController` as it adds unnecessary complexity to `ApplicationController` as well as other controllers which has to skip this callback. The source of the complexity comes mainly from the following two conditionals in the `preload_json` method: ``` # We don't preload JSON on xhr or JSON request return if request.xhr? || request.format.json? # if we are posting in makes no sense to preload return if request.method != "GET" ``` Basically, the conditionals solely exists for optimization purposes to ensure that we don't run the preloading code when the request is not a GET request and the response is not expected to be HTML. The key problem here is that the conditionals are trying to expect what the content type of the response will be and this has proven to be hard to get right. Instead, we can simplify this problem by running the preloading code in a more deterministic way which is to preload only when the `application` layout is being rendered and this is main change that this commit introduces.
This commit is contained in:

committed by
Roman Rizzi

parent
14d1d11536
commit
17e1bfe069
@ -43,6 +43,7 @@ class ApplicationController < ActionController::Base
|
||||
before_action :block_if_requires_login
|
||||
before_action :redirect_to_profile_if_required
|
||||
before_action :preload_json
|
||||
before_action :initialize_application_layout_preloader
|
||||
before_action :check_xhr
|
||||
after_action :add_readonly_header
|
||||
after_action :perform_refresh_session
|
||||
@ -277,6 +278,7 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
def rescue_discourse_actions(type, status_code, opts = nil)
|
||||
opts ||= {}
|
||||
|
||||
show_json_errors =
|
||||
(request.format && request.format.json?) || (request.xhr?) ||
|
||||
((params[:external_id] || "").ends_with? ".json")
|
||||
@ -339,8 +341,9 @@ class ApplicationController < ActionController::Base
|
||||
rescue Discourse::InvalidAccess
|
||||
return render plain: message, status: status_code
|
||||
end
|
||||
|
||||
with_resolved_locale do
|
||||
error_page_opts[:layout] = (opts[:include_ember] && @preloaded) ? set_layout : "no_ember"
|
||||
error_page_opts[:layout] = opts[:include_ember] && @_preloaded ? set_layout : "no_ember"
|
||||
render html: build_not_found_page(error_page_opts)
|
||||
end
|
||||
end
|
||||
@ -424,31 +427,6 @@ class ApplicationController < ActionController::Base
|
||||
I18n.with_locale(locale) { yield }
|
||||
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 safety 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? || request.format.json?
|
||||
|
||||
# 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
|
||||
preload_anonymous_data
|
||||
|
||||
if current_user
|
||||
current_user.sync_notification_channel_position
|
||||
preload_current_user_data
|
||||
end
|
||||
end
|
||||
|
||||
def set_mobile_view
|
||||
session[:mobile_view] = params[:mobile_view] if params.has_key?(:mobile_view)
|
||||
end
|
||||
@ -608,110 +586,36 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
def login_method
|
||||
return if current_user.anonymous?
|
||||
return if !current_user || current_user.anonymous?
|
||||
current_user.authenticated_with_oauth ? Auth::LOGIN_METHOD_OAUTH : Auth::LOGIN_METHOD_LOCAL
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preload_anonymous_data
|
||||
store_preloaded("site", Site.json_for(guardian))
|
||||
store_preloaded("siteSettings", SiteSetting.client_settings_json)
|
||||
store_preloaded("customHTML", custom_html_json)
|
||||
store_preloaded("banner", banner_json)
|
||||
store_preloaded("customEmoji", custom_emoji)
|
||||
store_preloaded("isReadOnly", get_or_check_readonly_mode.to_json)
|
||||
store_preloaded("isStaffWritesOnly", get_or_check_staff_writes_only_mode.to_json)
|
||||
store_preloaded("activatedThemes", activated_themes_json)
|
||||
# This method is intended to be a no-op.
|
||||
# The only reason this `before_action` callback continues to exist is for backwards compatibility purposes which we cannot easily
|
||||
# solve at this point. In the `rescue_discourse_actions` method, the `@_preloaded` instance variable is used to determine
|
||||
# if the `no_ember` or `application` layout should be used. To use the `no_ember` layout, controllers have been
|
||||
# setting `skip_before_action :preload_json`. This is however a flawed implementation as which layout is used for rendering
|
||||
# errors should ideally not be set by skipping a `before_action` callback. To fix this properly will require some careful
|
||||
# planning which we do not intend to tackle at this point.
|
||||
def preload_json
|
||||
return if request.format&.json? || request.xhr? || !request.get?
|
||||
@_preloaded = {}
|
||||
end
|
||||
|
||||
def preload_current_user_data
|
||||
store_preloaded(
|
||||
"currentUser",
|
||||
MultiJson.dump(
|
||||
CurrentUserSerializer.new(
|
||||
current_user,
|
||||
scope: guardian,
|
||||
root: false,
|
||||
login_method: login_method,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
report = TopicTrackingState.report(current_user)
|
||||
serializer = TopicTrackingStateSerializer.new(report, scope: guardian, root: false)
|
||||
|
||||
hash = serializer.as_json
|
||||
|
||||
store_preloaded("topicTrackingStates", MultiJson.dump(hash[:data]))
|
||||
store_preloaded("topicTrackingStateMeta", MultiJson.dump(hash[:meta]))
|
||||
|
||||
if current_user.admin?
|
||||
# This is used in the wizard so we can preload fonts using the FontMap JS API.
|
||||
store_preloaded("fontMap", MultiJson.dump(load_font_map))
|
||||
|
||||
# Used to show plugin-specific admin routes in the sidebar.
|
||||
store_preloaded(
|
||||
"visiblePlugins",
|
||||
MultiJson.dump(
|
||||
Discourse
|
||||
.plugins_sorted_by_name(enabled_only: false)
|
||||
.map do |plugin|
|
||||
{
|
||||
name: plugin.name.downcase,
|
||||
humanized_name: plugin.humanized_name,
|
||||
admin_route: plugin.full_admin_route,
|
||||
enabled: plugin.enabled?,
|
||||
}
|
||||
end,
|
||||
),
|
||||
def initialize_application_layout_preloader
|
||||
@application_layout_preloader =
|
||||
ApplicationLayoutPreloader.new(
|
||||
guardian:,
|
||||
theme_id: @theme_id,
|
||||
theme_target: view_context.mobile_view? ? :mobile : :desktop,
|
||||
login_method:,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def custom_html_json
|
||||
target = view_context.mobile_view? ? :mobile : :desktop
|
||||
|
||||
data =
|
||||
if @theme_id.present?
|
||||
{
|
||||
top: Theme.lookup_field(@theme_id, target, "after_header"),
|
||||
footer: Theme.lookup_field(@theme_id, target, "footer"),
|
||||
}
|
||||
else
|
||||
{}
|
||||
end
|
||||
|
||||
data.merge! DiscoursePluginRegistry.custom_html if DiscoursePluginRegistry.custom_html
|
||||
|
||||
DiscoursePluginRegistry.html_builders.each do |name, _|
|
||||
if name.start_with?("client:")
|
||||
data[name.sub(/\Aclient:/, "")] = DiscoursePluginRegistry.build_html(name, self)
|
||||
end
|
||||
end
|
||||
|
||||
MultiJson.dump(data)
|
||||
end
|
||||
|
||||
def self.banner_json_cache
|
||||
@banner_json_cache ||= DistributedCache.new("banner_json")
|
||||
end
|
||||
|
||||
def banner_json
|
||||
return "{}" if !current_user && SiteSetting.login_required?
|
||||
|
||||
ApplicationController
|
||||
.banner_json_cache
|
||||
.defer_get_set("json") do
|
||||
topic = Topic.where(archetype: Archetype.banner).first
|
||||
banner = topic.present? ? topic.banner : {}
|
||||
MultiJson.dump(banner)
|
||||
end
|
||||
end
|
||||
|
||||
def custom_emoji
|
||||
serializer = ActiveModel::ArraySerializer.new(Emoji.custom, each_serializer: EmojiSerializer)
|
||||
MultiJson.dump(serializer)
|
||||
def store_preloaded(key, json)
|
||||
@application_layout_preloader.store_preloaded(key, json)
|
||||
end
|
||||
|
||||
# Render action for a JSON error.
|
||||
@ -947,7 +851,7 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
def build_not_found_page(opts = {})
|
||||
if SiteSetting.bootstrap_error_pages?
|
||||
preload_json
|
||||
initialize_application_layout_preloader
|
||||
opts[:layout] = "application" if opts[:layout] == "no_ember"
|
||||
end
|
||||
|
||||
@ -1064,13 +968,6 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
end
|
||||
|
||||
def activated_themes_json
|
||||
id = @theme_id
|
||||
return "{}" if id.blank?
|
||||
ids = Theme.transform_ids(id)
|
||||
Theme.where(id: ids).pluck(:id, :name).to_h.to_json
|
||||
end
|
||||
|
||||
def run_second_factor!(action_class, action_data: nil, target_user: current_user)
|
||||
if current_user && target_user != current_user
|
||||
# Anon can run 2fa against another target, but logged-in users should not.
|
||||
@ -1117,20 +1014,6 @@ class ApplicationController < ActionController::Base
|
||||
request.get? && !(request.format && request.format.json?) && !request.xhr?
|
||||
end
|
||||
|
||||
def load_font_map
|
||||
DiscourseFonts
|
||||
.fonts
|
||||
.each_with_object({}) do |font, font_map|
|
||||
next if !font[:variants]
|
||||
font_map[font[:key]] = font[:variants].map do |v|
|
||||
{
|
||||
url: "#{Discourse.base_url}/fonts/#{v[:filename]}?v=#{DiscourseFonts::VERSION}",
|
||||
weight: v[:weight],
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_limit_from_params(params: self.params, default:, max:)
|
||||
fetch_int_from_params(:limit, params: params, default: default, max: max)
|
||||
end
|
||||
|
Reference in New Issue
Block a user