2018-01-17 13:32:52 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2013-10-09 14:22:41 +08:00
|
|
|
require 'current_user'
|
2013-02-06 03:16:51 +08:00
|
|
|
|
|
|
|
class ApplicationController < ActionController::Base
|
|
|
|
include CurrentUser
|
2013-02-13 19:04:43 +08:00
|
|
|
include CanonicalURL::ControllerExtensions
|
2014-04-03 01:22:10 +08:00
|
|
|
include JsonError
|
2015-03-10 03:24:16 +08:00
|
|
|
include GlobalPath
|
2017-11-24 12:31:23 +08:00
|
|
|
include Hijack
|
2019-05-06 22:07:49 +08:00
|
|
|
include ReadOnlyHeader
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
attr_reader :theme_ids
|
2017-05-13 00:41:26 +08:00
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
serialization_scope :guardian
|
|
|
|
|
|
|
|
protect_from_forgery
|
|
|
|
|
2013-07-29 13:13:13 +08: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
|
2016-12-16 09:05:20 +08:00
|
|
|
unless is_api? || is_user_api?
|
2013-07-29 13:13:13 +08:00
|
|
|
super
|
|
|
|
clear_current_user
|
2017-04-10 20:01:25 +08:00
|
|
|
render plain: "[\"BAD CSRF\"]", status: 403
|
2013-07-29 13:13:13 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-09-07 13:29:30 +08:00
|
|
|
before_action :check_readonly_mode
|
2017-08-31 12:06:56 +08:00
|
|
|
before_action :handle_theme
|
|
|
|
before_action :set_current_user_for_logs
|
2020-09-17 23:18:35 +08:00
|
|
|
before_action :set_mp_snapshot_fields
|
2017-08-31 12:06:56 +08:00
|
|
|
before_action :clear_notifications
|
2020-07-23 00:30:26 +08:00
|
|
|
around_action :with_resolved_locale
|
2017-08-31 12:06:56 +08:00
|
|
|
before_action :set_mobile_view
|
|
|
|
before_action :block_if_readonly_mode
|
|
|
|
before_action :authorize_mini_profiler
|
|
|
|
before_action :redirect_to_login_if_required
|
2018-02-01 12:17:59 +08:00
|
|
|
before_action :block_if_requires_login
|
|
|
|
before_action :preload_json
|
2020-05-27 23:57:05 +08:00
|
|
|
before_action :add_noindex_header, if: -> { is_feed_request? || !SiteSetting.allow_index_in_robots_txt }
|
2017-08-31 12:06:56 +08:00
|
|
|
before_action :check_xhr
|
|
|
|
after_action :add_readonly_header
|
|
|
|
after_action :perform_refresh_session
|
|
|
|
after_action :dont_cache_page
|
2019-12-06 20:55:32 +08:00
|
|
|
after_action :conditionally_allow_site_embedding
|
2013-02-07 23:45:24 +08:00
|
|
|
|
2020-10-02 07:01:40 +08:00
|
|
|
HONEYPOT_KEY ||= 'HONEYPOT_KEY'
|
|
|
|
CHALLENGE_KEY ||= 'CHALLENGE_KEY'
|
|
|
|
|
2014-02-15 06:10:08 +08:00
|
|
|
layout :set_layout
|
|
|
|
|
2014-02-21 06:02:26 +08:00
|
|
|
def has_escaped_fragment?
|
|
|
|
SiteSetting.enable_escaped_fragments? && params.key?("_escaped_fragment_")
|
|
|
|
end
|
|
|
|
|
2021-03-23 01:41:42 +08:00
|
|
|
def show_browser_update?
|
|
|
|
@show_browser_update ||= CrawlerDetection.show_browser_update?(request.user_agent)
|
|
|
|
end
|
|
|
|
helper_method :show_browser_update?
|
|
|
|
|
2014-10-31 02:26:35 +08:00
|
|
|
def use_crawler_layout?
|
2018-01-16 13:28:11 +08:00
|
|
|
@use_crawler_layout ||=
|
|
|
|
request.user_agent &&
|
|
|
|
(request.content_type.blank? || request.content_type.include?('html')) &&
|
|
|
|
!['json', 'rss'].include?(params[:format]) &&
|
2021-03-23 01:41:42 +08:00
|
|
|
(has_escaped_fragment? || params.key?("print") || show_browser_update? ||
|
2019-06-03 10:13:32 +08:00
|
|
|
CrawlerDetection.crawler?(request.user_agent, request.headers["HTTP_VIA"])
|
|
|
|
)
|
2014-10-31 02:26:35 +08:00
|
|
|
end
|
|
|
|
|
2016-07-25 10:07:31 +08:00
|
|
|
def perform_refresh_session
|
2017-10-20 12:41:40 +08:00
|
|
|
refresh_session(current_user) unless @readonly_mode
|
2016-07-25 10:07:31 +08:00
|
|
|
end
|
|
|
|
|
2017-02-24 02:05:00 +08:00
|
|
|
def immutable_for(duration)
|
|
|
|
response.cache_control[:max_age] = duration.to_i
|
|
|
|
response.cache_control[:public] = true
|
|
|
|
response.cache_control[:extras] = ["immutable"]
|
|
|
|
end
|
|
|
|
|
2016-11-15 14:00:28 +08:00
|
|
|
def dont_cache_page
|
|
|
|
if !response.headers["Cache-Control"] && response.cache_control.blank?
|
2018-06-05 15:29:17 +08:00
|
|
|
response.cache_control[:no_cache] = true
|
|
|
|
response.cache_control[:extras] = ["no-store"]
|
2016-11-15 14:00:28 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-12-06 20:55:32 +08:00
|
|
|
def conditionally_allow_site_embedding
|
|
|
|
if SiteSetting.allow_embedding_site_in_an_iframe
|
|
|
|
response.headers.delete('X-Frame-Options')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-04-30 02:13:36 +08:00
|
|
|
def ember_cli_required?
|
2021-04-30 18:27:35 +08:00
|
|
|
Rails.env.development? && ENV['NO_EMBER_CLI'] != '1' && request.headers['X-Discourse-Ember-CLI'] != 'true'
|
2021-04-30 02:13:36 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def application_layout
|
|
|
|
ember_cli_required? ? "ember_cli" : "application"
|
|
|
|
end
|
|
|
|
|
2014-02-15 06:10:08 +08:00
|
|
|
def set_layout
|
2019-08-31 02:45:18 +08:00
|
|
|
case request.headers["Discourse-Render"]
|
|
|
|
when "desktop"
|
2021-04-30 02:13:36 +08:00
|
|
|
return application_layout
|
2019-08-31 02:45:18 +08:00
|
|
|
when "crawler"
|
|
|
|
return "crawler"
|
|
|
|
end
|
|
|
|
|
2021-04-30 02:13:36 +08:00
|
|
|
use_crawler_layout? ? 'crawler' : application_layout
|
2014-02-15 06:10:08 +08:00
|
|
|
end
|
|
|
|
|
2015-03-23 09:20:50 +08:00
|
|
|
class RenderEmpty < StandardError; end
|
2019-10-08 19:15:08 +08:00
|
|
|
class PluginDisabled < StandardError; end
|
2021-04-23 22:24:42 +08:00
|
|
|
class EmberCLIHijacked < StandardError; end
|
|
|
|
|
|
|
|
def catch_ember_cli_hijack
|
|
|
|
yield
|
|
|
|
rescue ActionView::Template::Error => ex
|
|
|
|
raise ex unless ex.cause.is_a?(EmberCLIHijacked)
|
|
|
|
send_ember_cli_bootstrap
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
|
|
|
|
rescue_from RenderEmpty do
|
2021-04-23 22:24:42 +08:00
|
|
|
catch_ember_cli_hijack do
|
|
|
|
with_resolved_locale { render 'default/empty' }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
rescue_from EmberCLIHijacked do
|
|
|
|
send_ember_cli_bootstrap
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2019-10-08 19:15:08 +08:00
|
|
|
rescue_from ArgumentError do |e|
|
|
|
|
if e.message == "string contains null byte"
|
|
|
|
raise Discourse::InvalidParameters, e.message
|
2018-08-21 09:54:34 +08:00
|
|
|
else
|
|
|
|
raise e
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-04-29 23:49:58 +08:00
|
|
|
rescue_from PG::ReadOnlySqlTransaction do |e|
|
2019-06-21 22:08:57 +08:00
|
|
|
Discourse.received_postgres_readonly!
|
2017-10-20 12:41:40 +08:00
|
|
|
Rails.logger.error("#{e.class} #{e.message}: #{e.backtrace.join("\n")}")
|
2020-05-26 22:43:29 +08:00
|
|
|
rescue_with_handler(Discourse::ReadOnly.new) || raise
|
2015-04-29 23:49:58 +08:00
|
|
|
end
|
|
|
|
|
2019-10-08 19:15:08 +08:00
|
|
|
rescue_from ActionController::ParameterMissing do |e|
|
|
|
|
render_json_error e.message, status: 400
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2021-05-11 16:36:57 +08:00
|
|
|
rescue_from Discourse::SiteSettingMissing do |e|
|
|
|
|
render_json_error I18n.t('site_setting_missing', name: e.message), status: 500
|
|
|
|
end
|
|
|
|
|
2019-10-08 19:15:08 +08:00
|
|
|
rescue_from ActionController::RoutingError, PluginDisabled do
|
|
|
|
rescue_discourse_actions(:not_found, 404)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Handles requests for giant IDs that throw pg exceptions
|
|
|
|
rescue_from ActiveModel::RangeError do |e|
|
|
|
|
if e.message =~ /ActiveModel::Type::Integer/
|
|
|
|
rescue_discourse_actions(:not_found, 404)
|
2018-09-04 10:11:42 +08:00
|
|
|
else
|
|
|
|
raise e
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-10-08 19:15:08 +08:00
|
|
|
rescue_from ActiveRecord::RecordInvalid do |e|
|
|
|
|
if request.format && request.format.json?
|
|
|
|
render_json_error e, type: :record_invalid, status: 422
|
2018-01-12 11:15:10 +08:00
|
|
|
else
|
2019-10-08 19:15:08 +08:00
|
|
|
raise e
|
2018-01-12 11:15:10 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-12-05 09:11:46 +08:00
|
|
|
rescue_from ActiveRecord::StatementInvalid do |e|
|
2017-02-18 01:09:53 +08:00
|
|
|
Discourse.reset_active_record_cache_if_needed(e)
|
2016-12-05 09:11:46 +08:00
|
|
|
raise e
|
|
|
|
end
|
|
|
|
|
2019-10-08 19:15:08 +08:00
|
|
|
# If they hit the rate limiter
|
|
|
|
rescue_from RateLimiter::LimitExceeded do |e|
|
|
|
|
retry_time_in_seconds = e&.available_in
|
2016-02-24 17:09:30 +08:00
|
|
|
|
2020-09-30 03:42:45 +08:00
|
|
|
with_resolved_locale do
|
|
|
|
render_json_error(
|
|
|
|
e.description,
|
|
|
|
type: :rate_limit,
|
|
|
|
status: 429,
|
|
|
|
extras: { wait_seconds: retry_time_in_seconds },
|
2021-03-24 03:32:36 +08:00
|
|
|
headers: { 'Retry-After': retry_time_in_seconds.to_s }
|
2020-09-30 03:42:45 +08:00
|
|
|
)
|
|
|
|
end
|
2019-10-08 19:15:08 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
rescue_from Discourse::NotLoggedIn do |e|
|
|
|
|
if (request.format && request.format.json?) || request.xhr? || !request.get?
|
|
|
|
rescue_discourse_actions(:not_logged_in, 403, include_ember: true)
|
|
|
|
else
|
2016-03-24 00:13:29 +08:00
|
|
|
rescue_discourse_actions(:not_found, 404)
|
2019-10-08 19:15:08 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
rescue_from Discourse::InvalidParameters do |e|
|
2020-09-30 03:42:45 +08:00
|
|
|
opts = {
|
|
|
|
custom_message: 'invalid_params',
|
|
|
|
custom_message_params: { message: e.message }
|
|
|
|
}
|
|
|
|
|
2019-10-08 19:15:08 +08:00
|
|
|
if (request.format && request.format.json?) || request.xhr? || !request.get?
|
2020-09-30 03:42:45 +08:00
|
|
|
rescue_discourse_actions(:invalid_parameters, 400, opts.merge(include_ember: true))
|
2016-03-24 00:13:29 +08:00
|
|
|
else
|
2020-09-30 03:42:45 +08:00
|
|
|
rescue_discourse_actions(:not_found, 400, opts)
|
2016-03-24 00:13:29 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-08-09 13:05:12 +08:00
|
|
|
rescue_from Discourse::NotFound do |e|
|
|
|
|
rescue_discourse_actions(
|
|
|
|
:not_found,
|
|
|
|
e.status,
|
|
|
|
check_permalinks: e.check_permalinks,
|
2019-10-08 19:15:08 +08:00
|
|
|
original_path: e.original_path,
|
|
|
|
custom_message: e.custom_message
|
2018-08-09 13:05:12 +08:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2017-09-23 22:39:58 +08:00
|
|
|
rescue_from Discourse::InvalidAccess do |e|
|
2018-02-10 08:09:54 +08:00
|
|
|
if e.opts[:delete_cookie].present?
|
|
|
|
cookies.delete(e.opts[:delete_cookie])
|
|
|
|
end
|
2019-10-08 19:15:08 +08:00
|
|
|
|
2017-09-23 22:39:58 +08:00
|
|
|
rescue_discourse_actions(
|
|
|
|
:invalid_access,
|
|
|
|
403,
|
|
|
|
include_ember: true,
|
2019-10-08 19:15:08 +08:00
|
|
|
custom_message: e.custom_message,
|
2020-11-24 19:06:52 +08:00
|
|
|
custom_message_params: e.custom_message_params,
|
2019-10-08 19:15:08 +08:00
|
|
|
group: e.group
|
2017-09-23 22:39:58 +08:00
|
|
|
)
|
2013-06-20 23:47:33 +08:00
|
|
|
end
|
|
|
|
|
2014-02-13 12:37:28 +08:00
|
|
|
rescue_from Discourse::ReadOnly do
|
2020-05-26 22:43:29 +08:00
|
|
|
unless response_body
|
|
|
|
render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 503
|
|
|
|
end
|
2014-02-13 12:37:28 +08:00
|
|
|
end
|
|
|
|
|
2018-08-09 13:05:12 +08:00
|
|
|
def redirect_with_client_support(url, options)
|
|
|
|
if request.xhr?
|
|
|
|
response.headers['Discourse-Xhr-Redirect'] = 'true'
|
|
|
|
render plain: url
|
|
|
|
else
|
|
|
|
redirect_to url, options
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-09-23 22:39:58 +08:00
|
|
|
def rescue_discourse_actions(type, status_code, opts = nil)
|
|
|
|
opts ||= {}
|
2017-04-05 04:22:14 +08:00
|
|
|
show_json_errors = (request.format && request.format.json?) ||
|
|
|
|
(request.xhr?) ||
|
2017-09-28 16:09:27 +08:00
|
|
|
((params[:external_id] || '').ends_with? '.json')
|
2017-04-05 04:22:14 +08:00
|
|
|
|
2018-08-09 13:05:12 +08:00
|
|
|
if type == :not_found && opts[:check_permalinks]
|
|
|
|
url = opts[:original_path] || request.fullpath
|
|
|
|
permalink = Permalink.find_by_url(url)
|
|
|
|
|
2018-08-20 11:10:49 +08:00
|
|
|
# there are some cases where we have a permalink but no url
|
|
|
|
# cause category / topic was deleted
|
|
|
|
if permalink.present? && permalink.target_url
|
2018-08-09 13:05:12 +08:00
|
|
|
# permalink present, redirect to that URL
|
2018-08-09 23:05:08 +08:00
|
|
|
redirect_with_client_support permalink.target_url, status: :moved_permanently
|
|
|
|
return
|
2018-08-09 13:05:12 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-07-23 00:30:26 +08:00
|
|
|
message = title = nil
|
|
|
|
with_resolved_locale(check_current_user: false) do
|
2020-09-30 03:42:45 +08:00
|
|
|
if opts[:custom_message]
|
|
|
|
title = message = I18n.t(opts[:custom_message], opts[:custom_message_params] || {})
|
2020-05-28 01:10:01 +08:00
|
|
|
else
|
2020-07-23 00:30:26 +08:00
|
|
|
message = I18n.t(type)
|
|
|
|
if status_code == 403
|
|
|
|
title = I18n.t("page_forbidden.title")
|
|
|
|
else
|
|
|
|
title = I18n.t("page_not_found.title")
|
|
|
|
end
|
2020-05-28 01:10:01 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
error_page_opts = { title: title, status: status_code, group: opts[:group] }
|
2018-01-12 11:15:10 +08:00
|
|
|
|
2017-09-28 21:50:01 +08:00
|
|
|
if show_json_errors
|
2019-10-08 19:15:08 +08:00
|
|
|
opts = { type: type, status: status_code }
|
|
|
|
|
2020-07-23 00:30:26 +08:00
|
|
|
with_resolved_locale(check_current_user: false) do
|
|
|
|
# Include error in HTML format for topics#show.
|
|
|
|
if (request.params[:controller] == 'topics' && request.params[:action] == 'show') || (request.params[:controller] == 'categories' && request.params[:action] == 'find_by_slug')
|
|
|
|
opts[:extras] = { html: build_not_found_page(error_page_opts) }
|
|
|
|
end
|
2015-02-09 05:35:09 +08:00
|
|
|
end
|
|
|
|
|
2019-10-08 19:15:08 +08:00
|
|
|
render_json_error message, opts
|
2013-05-31 02:46:02 +08:00
|
|
|
else
|
2017-09-29 08:35:23 +08:00
|
|
|
begin
|
2018-02-15 04:29:01 +08:00
|
|
|
# 404 pages won't have the session and theme_keys without these:
|
2017-09-29 08:35:23 +08:00
|
|
|
current_user
|
2018-02-15 04:29:01 +08:00
|
|
|
handle_theme
|
2017-09-29 08:35:23 +08:00
|
|
|
rescue Discourse::InvalidAccess
|
2018-01-12 11:15:10 +08:00
|
|
|
return render plain: message, status: status_code
|
2017-09-29 08:35:23 +08:00
|
|
|
end
|
2021-04-23 22:24:42 +08:00
|
|
|
catch_ember_cli_hijack do
|
|
|
|
with_resolved_locale do
|
|
|
|
error_page_opts[:layout] = opts[:include_ember] ? 'application' : 'no_ember'
|
|
|
|
render html: build_not_found_page(error_page_opts)
|
|
|
|
end
|
2020-07-23 00:30:26 +08:00
|
|
|
end
|
2013-05-31 02:46:02 +08:00
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2021-04-23 22:24:42 +08:00
|
|
|
def send_ember_cli_bootstrap
|
|
|
|
head 200, content_type: "text/html", "X-Discourse-Bootstrap-Required": true
|
|
|
|
end
|
|
|
|
|
2015-02-05 05:23:39 +08:00
|
|
|
# If a controller requires a plugin, it will raise an exception if that plugin is
|
2021-05-21 09:43:47 +08:00
|
|
|
# disabled. This allows plugins to be disabled programmatically.
|
2015-02-05 05:23:39 +08:00
|
|
|
def self.requires_plugin(plugin_name)
|
2017-08-31 12:06:56 +08:00
|
|
|
before_action do
|
2015-02-05 05:23:39 +08:00
|
|
|
raise PluginDisabled.new if Discourse.disabled_plugin_names.include?(plugin_name)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-05-12 13:27:58 +08:00
|
|
|
def set_current_user_for_logs
|
|
|
|
if current_user
|
|
|
|
Logster.add_to_env(request.env, "username", current_user.username)
|
2015-06-16 15:43:36 +08:00
|
|
|
response.headers["X-Discourse-Username"] = current_user.username
|
2014-05-12 13:27:58 +08:00
|
|
|
end
|
2015-06-16 08:27:42 +08:00
|
|
|
response.headers["X-Discourse-Route"] = "#{controller_name}/#{action_name}"
|
2014-05-12 13:27:58 +08:00
|
|
|
end
|
|
|
|
|
2020-09-17 23:18:35 +08:00
|
|
|
def set_mp_snapshot_fields
|
|
|
|
if defined?(Rack::MiniProfiler)
|
2020-10-22 00:37:28 +08:00
|
|
|
Rack::MiniProfiler.add_snapshot_custom_field("Application version", Discourse.git_version)
|
|
|
|
if Rack::MiniProfiler.snapshots_transporter?
|
|
|
|
Rack::MiniProfiler.add_snapshot_custom_field("Site", Discourse.current_hostname)
|
|
|
|
end
|
2020-09-17 23:18:35 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-02-15 16:29:35 +08:00
|
|
|
def clear_notifications
|
2017-09-07 13:29:30 +08:00
|
|
|
if current_user && !@readonly_mode
|
2016-02-15 16:29:35 +08:00
|
|
|
|
2018-01-17 13:32:52 +08:00
|
|
|
cookie_notifications = cookies['cn']
|
|
|
|
notifications = request.headers['Discourse-Clear-Notifications']
|
2016-02-15 16:29:35 +08:00
|
|
|
|
|
|
|
if cookie_notifications
|
|
|
|
if notifications.present?
|
2018-01-17 13:32:52 +08:00
|
|
|
notifications += ",#{cookie_notifications}"
|
2016-02-15 16:29:35 +08:00
|
|
|
else
|
|
|
|
notifications = cookie_notifications
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if notifications.present?
|
|
|
|
notification_ids = notifications.split(",").map(&:to_i)
|
2016-09-16 14:14:00 +08:00
|
|
|
Notification.read(current_user, notification_ids)
|
2018-05-26 09:11:10 +08:00
|
|
|
current_user.reload
|
|
|
|
current_user.publish_notifications_state
|
2018-06-28 23:03:36 +08:00
|
|
|
cookie_args = {}
|
2020-10-09 19:51:24 +08:00
|
|
|
cookie_args[:path] = Discourse.base_path if Discourse.base_path.present?
|
2018-06-28 23:03:36 +08:00
|
|
|
cookies.delete('cn', cookie_args)
|
2016-02-15 16:29:35 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-07-23 00:30:26 +08:00
|
|
|
def with_resolved_locale(check_current_user: true)
|
2020-09-30 03:42:45 +08:00
|
|
|
if check_current_user && (user = current_user rescue nil)
|
|
|
|
locale = user.effective_locale
|
2020-07-23 00:30:26 +08:00
|
|
|
else
|
2016-03-15 07:18:19 +08:00
|
|
|
if SiteSetting.set_locale_from_accept_language_header
|
2018-01-22 21:44:17 +08:00
|
|
|
locale = locale_from_header
|
2016-02-07 03:49:39 +08:00
|
|
|
else
|
2018-01-22 21:44:17 +08:00
|
|
|
locale = SiteSetting.default_locale
|
2016-02-07 03:49:39 +08:00
|
|
|
end
|
|
|
|
end
|
2018-01-22 21:44:17 +08:00
|
|
|
|
2020-07-23 00:30:26 +08:00
|
|
|
if !I18n.locale_available?(locale)
|
|
|
|
locale = SiteSettings::DefaultsProvider::DEFAULT_LOCALE
|
|
|
|
end
|
|
|
|
|
2015-11-14 04:42:01 +08:00
|
|
|
I18n.ensure_all_loaded!
|
2020-07-23 00:30:26 +08:00
|
|
|
I18n.with_locale(locale) { yield }
|
2013-03-01 03:31:39 +08:00
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def store_preloaded(key, json)
|
|
|
|
@preloaded ||= {}
|
2013-02-26 00:42:20 +08: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
|
2021-05-21 09:43:47 +08:00
|
|
|
# to inject this safety deeper in the library or even in AM serializer
|
2013-02-11 14:28:21 +08:00
|
|
|
@preloaded[key] = json.gsub("</", "<\\/")
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# If we are rendering HTML, preload the session data
|
|
|
|
def preload_json
|
2013-06-05 00:56:12 +08:00
|
|
|
# We don't preload JSON on xhr or JSON request
|
2015-01-24 13:03:44 +08:00
|
|
|
return if request.xhr? || request.format.json?
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2015-05-20 15:12:16 +08: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 04:25:44 +08:00
|
|
|
preload_anonymous_data
|
2013-06-05 00:56:12 +08:00
|
|
|
|
2013-08-06 04:25:44 +08:00
|
|
|
if current_user
|
|
|
|
current_user.sync_notification_channel_position
|
2017-03-20 17:16:53 +08:00
|
|
|
preload_current_user_data
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-12-19 03:47:22 +08:00
|
|
|
def set_mobile_view
|
|
|
|
session[:mobile_view] = params[:mobile_view] if params.has_key?(:mobile_view)
|
|
|
|
end
|
|
|
|
|
2018-01-17 13:32:52 +08:00
|
|
|
NO_CUSTOM = "no_custom"
|
|
|
|
NO_PLUGINS = "no_plugins"
|
|
|
|
ONLY_OFFICIAL = "only_official"
|
|
|
|
SAFE_MODE = "safe_mode"
|
2015-01-06 14:39:08 +08:00
|
|
|
|
2017-04-15 01:35:12 +08:00
|
|
|
def resolve_safe_mode
|
2018-04-25 23:46:54 +08:00
|
|
|
return unless guardian.can_enable_safe_mode?
|
2018-04-24 02:50:52 +08:00
|
|
|
|
2017-04-15 01:35:12 +08:00
|
|
|
safe_mode = params[SAFE_MODE]
|
|
|
|
if safe_mode
|
2017-04-15 02:00:46 +08:00
|
|
|
request.env[NO_CUSTOM] = !!safe_mode.include?(NO_CUSTOM)
|
|
|
|
request.env[NO_PLUGINS] = !!safe_mode.include?(NO_PLUGINS)
|
|
|
|
request.env[ONLY_OFFICIAL] = !!safe_mode.include?(ONLY_OFFICIAL)
|
2017-04-15 01:35:12 +08:00
|
|
|
end
|
|
|
|
end
|
2015-01-06 14:39:08 +08:00
|
|
|
|
2017-04-15 01:35:12 +08:00
|
|
|
def handle_theme
|
2020-04-03 22:50:13 +08:00
|
|
|
return if request.format == "js"
|
2017-04-15 01:35:12 +08:00
|
|
|
|
|
|
|
resolve_safe_mode
|
|
|
|
return if request.env[NO_CUSTOM]
|
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
theme_ids = []
|
|
|
|
|
|
|
|
if preview_theme_id = request[:preview_theme_id]&.to_i
|
2018-09-07 08:44:57 +08:00
|
|
|
ids = [preview_theme_id]
|
|
|
|
theme_ids = ids if guardian.allow_themes?(ids, include_preview: true)
|
2018-08-08 12:46:34 +08:00
|
|
|
end
|
2017-05-13 00:41:26 +08:00
|
|
|
|
2017-05-16 00:48:08 +08:00
|
|
|
user_option = current_user&.user_option
|
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
if theme_ids.blank?
|
2018-07-12 12:18:21 +08:00
|
|
|
ids, seq = cookies[:theme_ids]&.split("|")
|
|
|
|
ids = ids&.split(",")&.map(&:to_i)
|
2018-08-08 12:46:34 +08:00
|
|
|
if ids.present? && seq && seq.to_i == user_option&.theme_key_seq.to_i
|
|
|
|
theme_ids = ids if guardian.allow_themes?(ids)
|
2017-05-16 00:48:08 +08:00
|
|
|
end
|
2017-05-13 00:41:26 +08:00
|
|
|
end
|
2017-04-15 01:35:12 +08:00
|
|
|
|
2018-09-07 08:44:57 +08:00
|
|
|
if theme_ids.blank?
|
|
|
|
ids = user_option&.theme_ids || []
|
|
|
|
theme_ids = ids if guardian.allow_themes?(ids)
|
2014-06-21 00:06:36 +08:00
|
|
|
end
|
2015-01-06 14:39:08 +08:00
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
if theme_ids.blank? && SiteSetting.default_theme_id != -1
|
2020-06-03 11:19:42 +08:00
|
|
|
if guardian.allow_themes?([SiteSetting.default_theme_id])
|
|
|
|
theme_ids << SiteSetting.default_theme_id
|
|
|
|
end
|
2018-08-08 12:46:34 +08:00
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2018-08-08 12:46:34 +08:00
|
|
|
@theme_ids = request.env[:resolved_theme_ids] = theme_ids
|
2013-11-13 01:13:17 +08:00
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def guardian
|
2018-10-09 22:21:41 +08:00
|
|
|
@guardian ||= Guardian.new(current_user, request)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2015-06-09 00:07:35 +08:00
|
|
|
def current_homepage
|
2017-11-10 03:45:19 +08:00
|
|
|
current_user&.user_option&.homepage || SiteSetting.anonymous_homepage
|
2015-06-09 00:07:35 +08:00
|
|
|
end
|
|
|
|
|
2015-04-01 00:58:56 +08:00
|
|
|
def serialize_data(obj, serializer, opts = nil)
|
2013-02-06 03:16:51 +08:00
|
|
|
# If it's an array, apply the serializer as an each_serializer to the elements
|
2015-04-01 00:58:56 +08:00
|
|
|
serializer_opts = { scope: guardian }.merge!(opts || {})
|
2014-04-16 23:18:09 +08:00
|
|
|
if obj.respond_to?(:to_ary)
|
2013-02-06 03:16:51 +08:00
|
|
|
serializer_opts[:each_serializer] = serializer
|
2014-04-16 23:18:09 +08:00
|
|
|
ActiveModel::ArraySerializer.new(obj.to_ary, serializer_opts).as_json
|
2013-02-07 23:45:24 +08:00
|
|
|
else
|
2013-05-30 04:49:34 +08:00
|
|
|
serializer.new(obj, serializer_opts).as_json
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
2013-05-30 04:49:34 +08:00
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2013-05-30 04:49:34 +08: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-04-01 00:58:56 +08:00
|
|
|
def render_serialized(obj, serializer, opts = nil)
|
|
|
|
render_json_dump(serialize_data(obj, serializer, opts), opts)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2015-04-01 00:58:56 +08:00
|
|
|
def render_json_dump(obj, opts = nil)
|
|
|
|
opts ||= {}
|
2015-04-28 01:52:37 +08: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-12-01 04:22:58 +08:00
|
|
|
|
|
|
|
obj['extras'] = opts[:extras] if opts[:extras]
|
2017-09-12 04:44:20 +08:00
|
|
|
obj['meta'] = opts[:meta] if opts[:meta]
|
2015-04-28 01:52:37 +08:00
|
|
|
end
|
|
|
|
|
2015-04-01 00:58:56 +08:00
|
|
|
render json: MultiJson.dump(obj), status: opts[:status] || 200
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-02-07 00:55:54 +08:00
|
|
|
def can_cache_content?
|
2018-10-30 06:30:36 +08:00
|
|
|
current_user.blank? && cookies[:authentication_data].blank?
|
2013-02-07 00:55:54 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# Our custom cache method
|
|
|
|
def discourse_expires_in(time_length)
|
|
|
|
return unless can_cache_content?
|
2014-01-04 15:53:27 +08:00
|
|
|
Middleware::AnonymousCache.anon_cache(request.env, time_length)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2016-06-28 10:01:00 +08:00
|
|
|
def fetch_user_from_params(opts = nil, eager_load = [])
|
2014-08-29 00:07:13 +08:00
|
|
|
opts ||= {}
|
2014-06-19 02:40:15 +08:00
|
|
|
user = if params[:username]
|
2016-06-11 10:37:33 +08:00
|
|
|
username_lower = params[:username].downcase.chomp('.json')
|
2016-01-15 19:16:00 +08:00
|
|
|
find_opts = { username_lower: username_lower }
|
2016-01-15 19:34:28 +08:00
|
|
|
find_opts[:active] = true unless opts[:include_inactive] || current_user.try(:staff?)
|
2016-06-28 10:01:00 +08:00
|
|
|
result = User
|
|
|
|
(result = result.includes(*eager_load)) if !eager_load.empty?
|
|
|
|
result.find_by(find_opts)
|
2014-06-19 02:40:15 +08:00
|
|
|
elsif params[:external_id]
|
2016-06-11 10:37:33 +08:00
|
|
|
external_id = params[:external_id].chomp('.json')
|
2020-11-10 18:41:46 +08:00
|
|
|
if provider_name = params[:external_provider]
|
|
|
|
raise Discourse::InvalidAccess unless guardian.is_admin? # external_id might be something sensitive
|
|
|
|
provider = Discourse.enabled_authenticators.find { |a| a.name == provider_name }
|
|
|
|
raise Discourse::NotFound if !provider&.is_managed? # Only managed authenticators use UserAssociatedAccount
|
|
|
|
UserAssociatedAccount.find_by(provider_name: provider_name, provider_uid: external_id)&.user
|
|
|
|
else
|
|
|
|
SingleSignOnRecord.find_by(external_id: external_id).try(:user)
|
|
|
|
end
|
2014-06-19 02:40:15 +08:00
|
|
|
end
|
2015-05-07 09:00:51 +08:00
|
|
|
raise Discourse::NotFound if user.blank?
|
2013-05-22 23:20:16 +08:00
|
|
|
|
|
|
|
guardian.ensure_can_see!(user)
|
|
|
|
user
|
|
|
|
end
|
|
|
|
|
2013-09-04 23:53:00 +08:00
|
|
|
def post_ids_including_replies
|
2018-01-23 00:23:19 +08:00
|
|
|
post_ids = params[:post_ids].map(&:to_i)
|
2020-01-18 00:24:49 +08:00
|
|
|
post_ids |= PostReply.where(post_id: params[:reply_post_ids]).pluck(:reply_post_id) if params[:reply_post_ids]
|
2013-09-04 23:53:00 +08:00
|
|
|
post_ids
|
|
|
|
end
|
|
|
|
|
2015-05-22 14:15:46 +08: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
|
|
|
|
|
2016-12-19 15:00:22 +08:00
|
|
|
def secure_session
|
|
|
|
SecureSession.new(session["secure_session_id"] ||= SecureRandom.hex)
|
|
|
|
end
|
|
|
|
|
2019-03-18 22:24:46 +08:00
|
|
|
def handle_permalink(path)
|
|
|
|
permalink = Permalink.find_by_url(path)
|
|
|
|
if permalink && permalink.target_url
|
|
|
|
redirect_to permalink.target_url, status: :moved_permanently
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-02-04 07:03:30 +08:00
|
|
|
def rate_limit_second_factor!(user)
|
|
|
|
return if params[:second_factor_token].blank?
|
|
|
|
|
|
|
|
RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 6, 1.minute).performed!
|
|
|
|
|
|
|
|
if user
|
|
|
|
RateLimiter.new(nil, "second-factor-min-#{user.username}", 6, 1.minute).performed!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
private
|
|
|
|
|
2013-08-06 04:25:44 +08:00
|
|
|
def locale_from_header
|
2020-07-23 01:00:07 +08:00
|
|
|
HttpLanguageParser.parse(request.env["HTTP_ACCEPT_LANGUAGE"])
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
2013-08-06 04:25:44 +08:00
|
|
|
|
2013-12-18 01:49:22 +08:00
|
|
|
def preload_anonymous_data
|
2014-02-25 03:24:18 +08:00
|
|
|
store_preloaded("site", Site.json_for(guardian))
|
2013-08-06 04:25:44 +08:00
|
|
|
store_preloaded("siteSettings", SiteSetting.client_settings_json)
|
2013-12-18 01:49:22 +08:00
|
|
|
store_preloaded("customHTML", custom_html_json)
|
2014-06-19 02:04:10 +08:00
|
|
|
store_preloaded("banner", banner_json)
|
2015-02-10 00:48:42 +08:00
|
|
|
store_preloaded("customEmoji", custom_emoji)
|
2020-06-12 09:54:05 +08:00
|
|
|
store_preloaded("isReadOnly", @readonly_mode.to_s)
|
2021-04-12 20:02:58 +08:00
|
|
|
store_preloaded("activatedThemes", activated_themes_json)
|
2021-06-08 01:34:01 +08:00
|
|
|
store_preloaded("directoryColumns", directory_columns_json)
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
2017-07-28 09:20:09 +08:00
|
|
|
|
2017-04-15 01:35:12 +08:00
|
|
|
def preload_current_user_data
|
|
|
|
store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false)))
|
|
|
|
report = TopicTrackingState.report(current_user)
|
DEV: Topic tracking state improvements (#13218)
I merged this PR in yesterday, finally thinking this was done https://github.com/discourse/discourse/pull/12958 but then a wild performance regression occurred. These are the problem methods:
https://github.com/discourse/discourse/blob/1aa20bd681e634f7fff22953ed62d90c2573b331/app/serializers/topic_tracking_state_serializer.rb#L13-L21
Turns out date comparison is super expensive on the backend _as well as_ the frontend.
The fix was to just move the `treat_as_new_topic_start_date` into the SQL query rather than using the slower `UserOption#treat_as_new_topic_start_date` method in ruby. After this change, 1% of the total time is spent with the `created_in_new_period` comparison instead of ~20%.
----
History:
Original PR which had to be reverted **https://github.com/discourse/discourse/pull/12555**. See the description there for what this PR is achieving, plus below.
The issue with the original PR is addressed in https://github.com/discourse/discourse/pull/12958/commits/92ef54f4020111ffacb0f2a27da5d5c2855f9d5d
If you went to the `x unread` link for a tag Chrome would freeze up and possibly crash, or eventually unfreeze after nearly 10 mins. Other routes for unread/new were similarly slow. From profiling the issue was the `sync` function of `topic-tracking-state.js`, which calls down to `isNew` which in turn calls `moment`, a change I had made in the PR above. The time it takes locally with ~1400 topics in the tracking state is 2.3 seconds.
To solve this issue, I have moved these calculations for "created in new period" and "unread not too old" into the tracking state serializer.
When I was looking at the profiler I also noticed this issue which was just compounding the problem. Every time we modify topic tracking state we recalculate the sidebar tracking/everything/tag counts. However this calls `forEachTracked` and `countTags` which can be quite expensive as they go through the whole tracking state (and were also calling the removed moment functions).
I added some logs and this was being called 30 times when navigating to a new /unread route because `sync` is being called from `build-topic-route` (one for each topic loaded due to pagination). So I just added a debounce here and it makes things even faster.
Finally, I changed topic tracking state to use a Map so our counts of the state keys is faster (Maps have .size whereas objects you have to do Object.keys(obj) which is O(n).)
<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in JavaScript). If your code does not include test coverage, please include an explanation of why it was omitted. -->
2021-06-02 07:06:29 +08:00
|
|
|
serializer = ActiveModel::ArraySerializer.new(
|
|
|
|
report, each_serializer: TopicTrackingStateSerializer, scope: guardian
|
|
|
|
)
|
2017-04-15 01:35:12 +08:00
|
|
|
store_preloaded("topicTrackingStates", MultiJson.dump(serializer))
|
|
|
|
end
|
2014-06-05 09:39:33 +08:00
|
|
|
|
2021-06-08 01:34:01 +08:00
|
|
|
def directory_columns_json
|
|
|
|
DirectoryColumn
|
|
|
|
.left_joins(:user_field)
|
|
|
|
.where(enabled: true)
|
|
|
|
.order(:position)
|
|
|
|
.pluck('directory_columns.name',
|
|
|
|
'directory_columns.automatic',
|
|
|
|
'directory_columns.icon',
|
|
|
|
'user_fields.id',
|
|
|
|
'user_fields.name')
|
|
|
|
.map { |column| { name: column[0] || column[4], automatic: column[1], icon: column[2], user_field_id: column[3] } }
|
|
|
|
.to_json
|
|
|
|
end
|
|
|
|
|
2014-06-05 09:39:33 +08:00
|
|
|
def custom_html_json
|
|
|
|
target = view_context.mobile_view? ? :mobile : :desktop
|
|
|
|
|
2017-11-03 23:32:32 +08:00
|
|
|
data =
|
2018-08-08 12:46:34 +08:00
|
|
|
if @theme_ids.present?
|
2018-06-07 13:28:18 +08:00
|
|
|
{
|
2018-08-08 12:46:34 +08:00
|
|
|
top: Theme.lookup_field(@theme_ids, target, "after_header"),
|
|
|
|
footer: Theme.lookup_field(@theme_ids, target, "footer")
|
2018-06-07 13:28:18 +08:00
|
|
|
}
|
2017-08-07 23:28:56 +08:00
|
|
|
else
|
2018-06-07 13:28:18 +08:00
|
|
|
{}
|
2017-04-18 03:47:21 +08:00
|
|
|
end
|
|
|
|
|
2014-06-05 09:39:33 +08:00
|
|
|
if DiscoursePluginRegistry.custom_html
|
|
|
|
data.merge! DiscoursePluginRegistry.custom_html
|
2013-12-18 01:25:27 +08:00
|
|
|
end
|
|
|
|
|
2014-11-14 12:39:17 +08:00
|
|
|
DiscoursePluginRegistry.html_builders.each do |name, _|
|
|
|
|
if name.start_with?("client:")
|
|
|
|
data[name.sub(/^client:/, '')] = DiscoursePluginRegistry.build_html(name, self)
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
2014-11-14 12:39:17 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
MultiJson.dump(data)
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
2014-11-14 12:39:17 +08:00
|
|
|
|
2017-02-24 19:56:13 +08:00
|
|
|
def self.banner_json_cache
|
2014-11-14 12:39:17 +08:00
|
|
|
@banner_json_cache ||= DistributedCache.new("banner_json")
|
|
|
|
end
|
|
|
|
|
2014-06-19 02:04:10 +08:00
|
|
|
def banner_json
|
2014-11-14 12:39:17 +08:00
|
|
|
json = ApplicationController.banner_json_cache["json"]
|
2014-06-19 02:04:10 +08:00
|
|
|
|
2014-11-14 12:39:17 +08:00
|
|
|
unless json
|
2014-12-23 08:12:26 +08:00
|
|
|
topic = Topic.where(archetype: Archetype.banner).first
|
|
|
|
banner = topic.present? ? topic.banner : {}
|
|
|
|
ApplicationController.banner_json_cache["json"] = json = MultiJson.dump(banner)
|
|
|
|
end
|
|
|
|
|
2015-02-09 04:25:03 +08:00
|
|
|
json
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def custom_emoji
|
|
|
|
serializer = ActiveModel::ArraySerializer.new(Emoji.custom, each_serializer: EmojiSerializer)
|
|
|
|
MultiJson.dump(serializer)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Render action for a JSON error.
|
2018-06-07 13:28:18 +08:00
|
|
|
#
|
2013-02-06 03:16:51 +08:00
|
|
|
# obj - a translated string, an ActiveRecord model, or an array of translated strings
|
2018-06-07 13:28:18 +08:00
|
|
|
# opts:
|
2013-02-06 03:16:51 +08:00
|
|
|
# type - a machine-readable description of the error
|
2018-03-13 23:12:41 +08:00
|
|
|
# status - HTTP status code to return
|
|
|
|
# headers - extra headers for the response
|
2013-02-06 03:16:51 +08:00
|
|
|
def render_json_error(obj, opts = {})
|
2014-06-19 02:04:10 +08:00
|
|
|
opts = { status: opts } if opts.is_a?(Integer)
|
2018-03-13 23:12:41 +08:00
|
|
|
opts.fetch(:headers, {}).each { |name, value| headers[name.to_s] = value }
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2019-01-04 01:03:01 +08:00
|
|
|
render(
|
|
|
|
json: MultiJson.dump(create_errors_json(obj, opts)),
|
|
|
|
status: opts[:status] || status_code(obj)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
def status_code(obj)
|
|
|
|
return 403 if obj.try(:forbidden)
|
|
|
|
return 404 if obj.try(:not_found)
|
|
|
|
422
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
|
|
|
|
def success_json
|
2013-04-12 08:07:46 +08:00
|
|
|
{ success: 'OK' }
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def failed_json
|
2014-06-19 02:04:10 +08:00
|
|
|
{ failed: 'FAILED' }
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
|
|
|
|
2015-02-23 13:28:50 +08:00
|
|
|
def json_result(obj, opts = {})
|
2013-02-06 03:16:51 +08:00
|
|
|
if yield(obj)
|
|
|
|
json = success_json
|
2018-06-07 13:28:18 +08:00
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
# If we were given a serializer, add the class to the json that comes back
|
|
|
|
if opts[:serializer].present?
|
2013-04-12 08:07:46 +08:00
|
|
|
json[obj.class.name.underscore] = opts[:serializer].new(obj, scope: guardian).serializable_hash
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
render json: MultiJson.dump(json)
|
|
|
|
else
|
2014-09-09 03:17:31 +08:00
|
|
|
error_obj = nil
|
|
|
|
if opts[:additional_errors]
|
|
|
|
error_target = opts[:additional_errors].find do |o|
|
2019-05-07 09:27:05 +08:00
|
|
|
target = obj.public_send(o)
|
2014-09-09 03:17:31 +08:00
|
|
|
target && target.errors.present?
|
|
|
|
end
|
2019-05-07 09:27:05 +08:00
|
|
|
error_obj = obj.public_send(error_target) if error_target
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
2014-09-09 03:17:31 +08:00
|
|
|
render_json_error(error_obj || obj)
|
2013-02-07 23:45:24 +08:00
|
|
|
end
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
|
|
|
|
def mini_profiler_enabled?
|
2017-09-06 11:26:03 +08:00
|
|
|
defined?(Rack::MiniProfiler) && (guardian.is_developer? || Rails.env.development?)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def authorize_mini_profiler
|
|
|
|
return unless mini_profiler_enabled?
|
|
|
|
Rack::MiniProfiler.authorize_request
|
|
|
|
end
|
|
|
|
|
2013-02-07 23:45:24 +08:00
|
|
|
def check_xhr
|
2013-08-06 04:25:44 +08:00
|
|
|
# bypass xhr check on PUT / POST / DELETE provided api key is there, otherwise calling api is annoying
|
2016-12-16 09:05:20 +08:00
|
|
|
return if !request.get? && (is_api? || is_user_api?)
|
2020-03-16 04:56:54 +08:00
|
|
|
raise ApplicationController::RenderEmpty.new unless ((request.format && request.format.json?) || request.xhr?)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2021-01-29 10:14:49 +08:00
|
|
|
def apply_cdn_headers
|
|
|
|
Discourse.apply_cdn_headers(response.headers) if Discourse.is_cdn_request?(request.env, request.method)
|
|
|
|
end
|
|
|
|
|
2018-02-01 12:17:59 +08:00
|
|
|
def self.requires_login(arg = {})
|
|
|
|
@requires_login_arg = arg
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.requires_login_arg
|
|
|
|
@requires_login_arg
|
|
|
|
end
|
|
|
|
|
|
|
|
def block_if_requires_login
|
|
|
|
if arg = self.class.requires_login_arg
|
|
|
|
check =
|
|
|
|
if except = arg[:except]
|
|
|
|
!except.include?(action_name.to_sym)
|
|
|
|
elsif only = arg[:only]
|
|
|
|
only.include?(action_name.to_sym)
|
|
|
|
else
|
|
|
|
true
|
|
|
|
end
|
|
|
|
ensure_logged_in if check
|
|
|
|
end
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
2018-02-01 12:17:59 +08:00
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def ensure_logged_in
|
|
|
|
raise Discourse::NotLoggedIn.new unless current_user.present?
|
|
|
|
end
|
2013-02-07 23:45:24 +08:00
|
|
|
|
2015-04-11 05:00:50 +08:00
|
|
|
def ensure_staff
|
|
|
|
raise Discourse::InvalidAccess.new unless current_user && current_user.staff?
|
|
|
|
end
|
|
|
|
|
2016-09-22 23:12:34 +08:00
|
|
|
def ensure_admin
|
|
|
|
raise Discourse::InvalidAccess.new unless current_user && current_user.admin?
|
|
|
|
end
|
|
|
|
|
2016-09-15 04:36:08 +08:00
|
|
|
def ensure_wizard_enabled
|
|
|
|
raise Discourse::InvalidAccess.new unless SiteSetting.wizard_enabled?
|
|
|
|
end
|
|
|
|
|
2015-08-11 23:27:56 +08:00
|
|
|
def destination_url
|
|
|
|
request.original_url unless request.original_url =~ /uploads/
|
|
|
|
end
|
|
|
|
|
2019-11-20 15:31:25 +08:00
|
|
|
def redirect_to_login
|
|
|
|
dont_cache_page
|
|
|
|
|
2021-02-08 18:04:33 +08:00
|
|
|
if SiteSetting.auth_immediately && SiteSetting.enable_discourse_connect?
|
2019-11-20 15:31:25 +08:00
|
|
|
# save original URL in a session so we can redirect after login
|
|
|
|
session[:destination_url] = destination_url
|
|
|
|
redirect_to path('/session/sso')
|
2021-02-08 18:04:33 +08:00
|
|
|
elsif SiteSetting.auth_immediately && !SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1 && !cookies[:authentication_data]
|
2019-11-20 15:31:25 +08:00
|
|
|
# Only one authentication provider, direct straight to it.
|
|
|
|
# If authentication_data is present, then we are halfway though registration. Don't redirect offsite
|
|
|
|
cookies[:destination_url] = destination_url
|
|
|
|
redirect_to path("/auth/#{Discourse.enabled_authenticators.first.name}")
|
|
|
|
else
|
|
|
|
# save original URL in a cookie (javascript redirects after login in this case)
|
|
|
|
cookies[:destination_url] = destination_url
|
|
|
|
redirect_to path("/login")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-06-05 06:32:36 +08:00
|
|
|
def redirect_to_login_if_required
|
2019-03-15 19:09:37 +08:00
|
|
|
return if request.format.json? && is_api?
|
2018-11-09 08:14:35 +08:00
|
|
|
|
2019-04-02 10:13:53 +08:00
|
|
|
# Used by clients authenticated via user API.
|
|
|
|
# Redirects to provided URL scheme if
|
|
|
|
# - request uses a valid public key and auth_redirect scheme
|
|
|
|
# - one_time_password scope is allowed
|
|
|
|
if !current_user &&
|
|
|
|
params.has_key?(:user_api_public_key) &&
|
|
|
|
params.has_key?(:auth_redirect)
|
|
|
|
begin
|
|
|
|
OpenSSL::PKey::RSA.new(params[:user_api_public_key])
|
|
|
|
rescue OpenSSL::PKey::RSAError
|
|
|
|
return render plain: I18n.t("user_api_key.invalid_public_key")
|
|
|
|
end
|
|
|
|
|
|
|
|
if UserApiKey.invalid_auth_redirect?(params[:auth_redirect])
|
|
|
|
return render plain: I18n.t("user_api_key.invalid_auth_redirect")
|
|
|
|
end
|
|
|
|
|
|
|
|
if UserApiKey.allowed_scopes.superset?(Set.new(["one_time_password"]))
|
|
|
|
redirect_to("#{params[:auth_redirect]}?otp=true")
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-03-15 19:09:37 +08:00
|
|
|
if !current_user && SiteSetting.login_required?
|
2017-05-26 02:10:15 +08:00
|
|
|
flash.keep
|
2019-11-20 15:31:25 +08:00
|
|
|
redirect_to_login
|
|
|
|
return
|
2013-06-05 06:32:36 +08:00
|
|
|
end
|
2019-03-15 19:09:37 +08:00
|
|
|
|
2020-01-15 18:27:12 +08:00
|
|
|
return if !current_user
|
|
|
|
return if !should_enforce_2fa?
|
|
|
|
|
2020-06-06 00:31:58 +08:00
|
|
|
redirect_path = path("/u/#{current_user.encoded_username}/preferences/second-factor")
|
2020-01-15 18:27:12 +08:00
|
|
|
if !request.fullpath.start_with?(redirect_path)
|
|
|
|
redirect_to path(redirect_path)
|
|
|
|
nil
|
2019-03-15 19:09:37 +08:00
|
|
|
end
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
2013-06-05 06:32:36 +08:00
|
|
|
|
2020-01-15 18:27:12 +08:00
|
|
|
def should_enforce_2fa?
|
|
|
|
disqualified_from_2fa_enforcement = request.format.json? || is_api? || current_user.anonymous?
|
|
|
|
enforcing_2fa = ((SiteSetting.enforce_second_factor == 'staff' && current_user.staff?) || SiteSetting.enforce_second_factor == 'all')
|
|
|
|
!disqualified_from_2fa_enforcement && enforcing_2fa && !current_user.has_any_second_factor_methods_enabled?
|
|
|
|
end
|
|
|
|
|
2014-02-13 12:37:28 +08:00
|
|
|
def block_if_readonly_mode
|
2015-03-09 08:45:36 +08:00
|
|
|
return if request.fullpath.start_with?(path "/admin/backups")
|
2017-09-07 13:29:30 +08:00
|
|
|
raise Discourse::ReadOnly.new if !(request.get? || request.head?) && @readonly_mode
|
2017-11-22 05:13:09 +08:00
|
|
|
end
|
|
|
|
|
2019-10-08 19:15:08 +08:00
|
|
|
def build_not_found_page(opts = {})
|
2018-05-24 04:58:47 +08:00
|
|
|
if SiteSetting.bootstrap_error_pages?
|
|
|
|
preload_json
|
2019-10-08 19:15:08 +08:00
|
|
|
opts[:layout] = 'application' if opts[:layout] == 'no_ember'
|
2013-05-31 02:46:02 +08:00
|
|
|
end
|
|
|
|
|
2020-07-30 18:10:16 +08:00
|
|
|
@current_user = current_user rescue nil
|
|
|
|
|
|
|
|
if !SiteSetting.login_required? || @current_user
|
2020-12-07 20:24:18 +08:00
|
|
|
key = "page_not_found_topics:#{I18n.locale}"
|
|
|
|
@topics_partial = Discourse.cache.fetch(key, expires_in: 10.minutes) do
|
2019-02-12 18:20:33 +08:00
|
|
|
category_topic_ids = Category.pluck(:topic_id).compact
|
|
|
|
@top_viewed = TopicQuery.new(nil, except_topic_ids: category_topic_ids).list_top_for("monthly").topics.first(10)
|
|
|
|
@recent = Topic.includes(:category).where.not(id: category_topic_ids).recent(10)
|
2020-12-07 20:24:18 +08:00
|
|
|
render_to_string partial: '/exceptions/not_found_topics', formats: [:html]
|
|
|
|
end.html_safe
|
2019-02-12 18:20:33 +08:00
|
|
|
end
|
|
|
|
|
2015-07-28 16:02:39 +08:00
|
|
|
@container_class = "wrap not-found-container"
|
2019-10-08 19:15:08 +08:00
|
|
|
@title = opts[:title] || I18n.t("page_not_found.title")
|
|
|
|
@group = opts[:group]
|
2018-08-15 09:53:04 +08:00
|
|
|
@hide_search = true if SiteSetting.login_required
|
2019-12-10 16:28:51 +08:00
|
|
|
|
|
|
|
params[:slug] = params[:slug].first if params[:slug].kind_of?(Array)
|
|
|
|
params[:id] = params[:id].first if params[:id].kind_of?(Array)
|
2021-01-27 16:43:33 +08:00
|
|
|
@slug = (params[:slug].presence || params[:id].presence || "").to_s.tr('-', ' ')
|
2019-10-08 19:15:08 +08:00
|
|
|
|
|
|
|
render_to_string status: opts[:status], layout: opts[:layout], formats: [:html], template: '/exceptions/not_found'
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
|
|
|
|
2018-03-06 12:20:39 +08:00
|
|
|
def is_asset_path
|
|
|
|
request.env['DISCOURSE_IS_ASSET_PATH'] = 1
|
|
|
|
end
|
|
|
|
|
2020-01-24 12:00:27 +08:00
|
|
|
def is_feed_request?
|
|
|
|
request.format.atom? || request.format.rss?
|
|
|
|
end
|
|
|
|
|
|
|
|
def add_noindex_header
|
2020-05-11 10:14:21 +08:00
|
|
|
if request.get?
|
|
|
|
if SiteSetting.allow_index_in_robots_txt
|
|
|
|
response.headers['X-Robots-Tag'] = 'noindex'
|
|
|
|
else
|
|
|
|
response.headers['X-Robots-Tag'] = 'noindex, nofollow'
|
|
|
|
end
|
|
|
|
end
|
2020-01-24 12:00:27 +08:00
|
|
|
end
|
|
|
|
|
2013-06-20 10:11:14 +08:00
|
|
|
protected
|
2013-06-17 14:09:59 +08:00
|
|
|
|
2020-10-02 07:01:40 +08:00
|
|
|
def honeypot_value
|
|
|
|
secure_session[HONEYPOT_KEY] ||= SecureRandom.hex
|
|
|
|
end
|
|
|
|
|
|
|
|
def challenge_value
|
|
|
|
secure_session[CHALLENGE_KEY] ||= SecureRandom.hex
|
|
|
|
end
|
|
|
|
|
2019-01-04 01:03:01 +08:00
|
|
|
def render_post_json(post, add_raw: true)
|
2014-09-03 05:37:19 +08:00
|
|
|
post_serializer = PostSerializer.new(post, scope: guardian, root: false)
|
2014-09-25 10:02:41 +08:00
|
|
|
post_serializer.add_raw = add_raw
|
2014-09-03 05:37:19 +08: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)
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
2014-09-03 05:37:19 +08:00
|
|
|
|
2013-06-20 10:11:14 +08: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
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
2013-06-20 10:11:14 +08:00
|
|
|
|
2021-04-12 20:02:58 +08:00
|
|
|
def activated_themes_json
|
|
|
|
ids = @theme_ids&.compact
|
|
|
|
return "{}" if ids.blank?
|
|
|
|
ids = Theme.transform_ids(ids)
|
|
|
|
Theme.where(id: ids).pluck(:id, :name).to_h.to_json
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|