mirror of
https://github.com/discourse/discourse.git
synced 2024-11-28 22:16:17 +08:00
2092152b03
This cookie is only used during login. Having it persist after that can cause some unusual behavior, especially for sites with short session lengths. We were already deleting the cookie following a new signup, but not for existing users. This commit moves the cookie deletion logic out of the erb template, and adds logic and tests to ensure it is always deleted consistently. Co-authored-by: Jarek Radosz <jradosz@gmail.com>
576 lines
16 KiB
Ruby
576 lines
16 KiB
Ruby
# coding: utf-8
|
|
# frozen_string_literal: true
|
|
require 'current_user'
|
|
require 'canonical_url'
|
|
|
|
module ApplicationHelper
|
|
include CurrentUser
|
|
include CanonicalURL::Helpers
|
|
include ConfigurableUrls
|
|
include GlobalPath
|
|
|
|
def self.extra_body_classes
|
|
@extra_body_classes ||= Set.new
|
|
end
|
|
|
|
def google_universal_analytics_json(ua_domain_name = nil)
|
|
result = {}
|
|
if ua_domain_name
|
|
result[:cookieDomain] = ua_domain_name.gsub(/^http(s)?:\/\//, '')
|
|
end
|
|
if current_user.present?
|
|
result[:userId] = current_user.id
|
|
end
|
|
if SiteSetting.ga_universal_auto_link_domains.present?
|
|
result[:allowLinker] = true
|
|
end
|
|
result.to_json
|
|
end
|
|
|
|
def ga_universal_json
|
|
google_universal_analytics_json(SiteSetting.ga_universal_domain_name)
|
|
end
|
|
|
|
def google_tag_manager_json
|
|
google_universal_analytics_json
|
|
end
|
|
|
|
def shared_session_key
|
|
if SiteSetting.long_polling_base_url != '/' && current_user
|
|
sk = "shared_session_key"
|
|
return request.env[sk] if request.env[sk]
|
|
|
|
request.env[sk] = key = (session[sk] ||= SecureRandom.hex)
|
|
Discourse.redis.setex "#{sk}_#{key}", 7.days, current_user.id.to_s
|
|
key
|
|
end
|
|
end
|
|
|
|
def is_brotli_req?
|
|
request.env["HTTP_ACCEPT_ENCODING"] =~ /br/
|
|
end
|
|
|
|
def is_gzip_req?
|
|
request.env["HTTP_ACCEPT_ENCODING"] =~ /gzip/
|
|
end
|
|
|
|
def script_asset_path(script)
|
|
path = ActionController::Base.helpers.asset_path("#{script}.js")
|
|
|
|
if GlobalSetting.use_s3? && GlobalSetting.s3_cdn_url
|
|
if GlobalSetting.cdn_url
|
|
folder = ActionController::Base.config.relative_url_root || "/"
|
|
path = path.gsub(File.join(GlobalSetting.cdn_url, folder, "/"), File.join(GlobalSetting.s3_cdn_url, "/"))
|
|
else
|
|
# we must remove the subfolder path here, assets are uploaded to s3
|
|
# without it getting involved
|
|
if ActionController::Base.config.relative_url_root
|
|
path = path.sub(ActionController::Base.config.relative_url_root, "")
|
|
end
|
|
|
|
path = "#{GlobalSetting.s3_cdn_url}#{path}"
|
|
end
|
|
|
|
if is_brotli_req?
|
|
path = path.gsub(/\.([^.]+)$/, '.br.\1')
|
|
elsif is_gzip_req?
|
|
path = path.gsub(/\.([^.]+)$/, '.gz.\1')
|
|
end
|
|
|
|
elsif GlobalSetting.cdn_url&.start_with?("https") && is_brotli_req? && Rails.env != "development"
|
|
path = path.gsub("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/brotli_asset/")
|
|
end
|
|
|
|
if Rails.env == "development"
|
|
if !path.include?("?")
|
|
# cache breaker for mobile iOS
|
|
path = path + "?#{Time.now.to_f}"
|
|
end
|
|
end
|
|
|
|
path
|
|
end
|
|
|
|
def preload_script(script)
|
|
path = script_asset_path(script)
|
|
preload_script_url(path)
|
|
end
|
|
|
|
def preload_script_url(url)
|
|
<<~HTML.html_safe
|
|
<link rel="preload" href="#{url}" as="script">
|
|
<script src="#{url}"></script>
|
|
HTML
|
|
end
|
|
|
|
def discourse_csrf_tags
|
|
# anon can not have a CSRF token cause these are all pages
|
|
# that may be cached, causing a mismatch between session CSRF
|
|
# and CSRF on page and horrible impossible to debug login issues
|
|
if current_user
|
|
csrf_meta_tags
|
|
end
|
|
end
|
|
|
|
def html_classes
|
|
list = []
|
|
list << (mobile_view? ? 'mobile-view' : 'desktop-view')
|
|
list << (mobile_device? ? 'mobile-device' : 'not-mobile-device')
|
|
list << 'ios-device' if ios_device?
|
|
list << 'rtl' if rtl?
|
|
list << text_size_class
|
|
list << 'anon' unless current_user
|
|
list.join(' ')
|
|
end
|
|
|
|
def body_classes
|
|
result = ApplicationHelper.extra_body_classes.to_a
|
|
|
|
if @category && @category.url.present?
|
|
result << "category-#{@category.url.sub(/^\/c\//, '').gsub(/\//, '-')}"
|
|
end
|
|
|
|
if current_user.present? &&
|
|
current_user.primary_group_id &&
|
|
primary_group_name = Group.where(id: current_user.primary_group_id).pluck_first(:name)
|
|
result << "primary-group-#{primary_group_name.downcase}"
|
|
end
|
|
|
|
result.join(' ')
|
|
end
|
|
|
|
def text_size_class
|
|
requested_cookie_size, cookie_seq = cookies[:text_size]&.split("|")
|
|
server_seq = current_user&.user_option&.text_size_seq
|
|
if cookie_seq && server_seq && cookie_seq.to_i >= server_seq &&
|
|
UserOption.text_sizes.keys.include?(requested_cookie_size&.to_sym)
|
|
cookie_size = requested_cookie_size
|
|
end
|
|
|
|
size = cookie_size || current_user&.user_option&.text_size || SiteSetting.default_text_size
|
|
"text-size-#{size}"
|
|
end
|
|
|
|
def escape_unicode(javascript)
|
|
if javascript
|
|
javascript = javascript.scrub
|
|
javascript.gsub!(/\342\200\250/u, '
')
|
|
javascript.gsub!(/(<\/)/u, '\u003C/')
|
|
javascript
|
|
else
|
|
''
|
|
end
|
|
end
|
|
|
|
def format_topic_title(title)
|
|
PrettyText.unescape_emoji strip_tags(title)
|
|
end
|
|
|
|
def with_format(format, &block)
|
|
old_formats = formats
|
|
self.formats = [format]
|
|
block.call
|
|
self.formats = old_formats
|
|
nil
|
|
end
|
|
|
|
def age_words(secs)
|
|
AgeWords.age_words(secs)
|
|
end
|
|
|
|
def short_date(dt)
|
|
if dt.year == Time.now.year
|
|
I18n.l(dt, format: :short_no_year)
|
|
else
|
|
I18n.l(dt, format: :date_only)
|
|
end
|
|
end
|
|
|
|
def guardian
|
|
@guardian ||= Guardian.new(current_user)
|
|
end
|
|
|
|
def admin?
|
|
current_user.try(:admin?)
|
|
end
|
|
|
|
def moderator?
|
|
current_user.try(:moderator?)
|
|
end
|
|
|
|
def staff?
|
|
current_user.try(:staff?)
|
|
end
|
|
|
|
def rtl?
|
|
["ar", "ur", "fa_IR", "he"].include? I18n.locale.to_s
|
|
end
|
|
|
|
def html_lang
|
|
SiteSetting.default_locale.sub("_", "-")
|
|
end
|
|
|
|
# Creates open graph and twitter card meta data
|
|
def crawlable_meta_data(opts = nil)
|
|
opts ||= {}
|
|
opts[:url] ||= "#{Discourse.base_url_no_prefix}#{request.fullpath}"
|
|
|
|
if opts[:image].blank?
|
|
twitter_summary_large_image_url = SiteSetting.site_twitter_summary_large_image_url
|
|
|
|
if twitter_summary_large_image_url.present?
|
|
opts[:twitter_summary_large_image] = twitter_summary_large_image_url
|
|
end
|
|
|
|
opts[:image] = SiteSetting.site_opengraph_image_url
|
|
end
|
|
|
|
# Use the correct scheme for opengraph/twitter image
|
|
opts[:image] = get_absolute_image_url(opts[:image]) if opts[:image].present?
|
|
opts[:twitter_summary_large_image] =
|
|
get_absolute_image_url(opts[:twitter_summary_large_image]) if opts[:twitter_summary_large_image].present?
|
|
|
|
# Add opengraph & twitter tags
|
|
result = []
|
|
result << tag(:meta, property: 'og:site_name', content: SiteSetting.title)
|
|
|
|
if opts[:twitter_summary_large_image].present?
|
|
result << tag(:meta, name: 'twitter:card', content: "summary_large_image")
|
|
result << tag(:meta, name: "twitter:image", content: opts[:twitter_summary_large_image])
|
|
elsif opts[:image].present?
|
|
result << tag(:meta, name: 'twitter:card', content: "summary")
|
|
result << tag(:meta, name: "twitter:image", content: opts[:image])
|
|
else
|
|
result << tag(:meta, name: 'twitter:card', content: "summary")
|
|
end
|
|
result << tag(:meta, property: "og:image", content: opts[:image]) if opts[:image].present?
|
|
|
|
[:url, :title, :description].each do |property|
|
|
if opts[property].present?
|
|
content = (property == :url ? opts[property] : gsub_emoji_to_unicode(opts[property]))
|
|
result << tag(:meta, { property: "og:#{property}", content: content }, nil, true)
|
|
result << tag(:meta, { name: "twitter:#{property}", content: content }, nil, true)
|
|
end
|
|
end
|
|
|
|
if opts[:read_time] && opts[:read_time] > 0 && opts[:like_count] && opts[:like_count] > 0
|
|
result << tag(:meta, name: 'twitter:label1', value: I18n.t("reading_time"))
|
|
result << tag(:meta, name: 'twitter:data1', value: "#{opts[:read_time]} mins 🕑")
|
|
result << tag(:meta, name: 'twitter:label2', value: I18n.t("likes"))
|
|
result << tag(:meta, name: 'twitter:data2', value: "#{opts[:like_count]} ❤")
|
|
end
|
|
|
|
if opts[:published_time]
|
|
result << tag(:meta, property: 'article:published_time', content: opts[:published_time])
|
|
end
|
|
|
|
if opts[:ignore_canonical]
|
|
result << tag(:meta, property: 'og:ignore_canonical', content: true)
|
|
end
|
|
|
|
result.join("\n")
|
|
end
|
|
|
|
def render_sitelinks_search_tag
|
|
json = {
|
|
'@context' => 'http://schema.org',
|
|
'@type' => 'WebSite',
|
|
url: Discourse.base_url,
|
|
potentialAction: {
|
|
'@type' => 'SearchAction',
|
|
target: "#{Discourse.base_url}/search?q={search_term_string}",
|
|
'query-input' => 'required name=search_term_string',
|
|
}
|
|
}
|
|
content_tag(:script, MultiJson.dump(json).html_safe, type: 'application/ld+json')
|
|
end
|
|
|
|
def gsub_emoji_to_unicode(str)
|
|
Emoji.gsub_emoji_to_unicode(str)
|
|
end
|
|
|
|
def application_logo_url
|
|
@application_logo_url ||= begin
|
|
if mobile_view?
|
|
if dark_color_scheme? && SiteSetting.site_mobile_logo_dark_url.present?
|
|
SiteSetting.site_mobile_logo_dark_url
|
|
elsif SiteSetting.site_mobile_logo_url.present?
|
|
SiteSetting.site_mobile_logo_url
|
|
end
|
|
else
|
|
if dark_color_scheme? && SiteSetting.site_logo_dark_url.present?
|
|
SiteSetting.site_logo_dark_url
|
|
else
|
|
SiteSetting.site_logo_url
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def login_path
|
|
"#{Discourse.base_path}/login"
|
|
end
|
|
|
|
def mobile_view?
|
|
MobileDetection.resolve_mobile_view!(request.user_agent, params, session)
|
|
end
|
|
|
|
def crawler_layout?
|
|
controller&.use_crawler_layout?
|
|
end
|
|
|
|
def include_crawler_content?
|
|
crawler_layout? || !mobile_view?
|
|
end
|
|
|
|
def mobile_device?
|
|
MobileDetection.mobile_device?(request.user_agent)
|
|
end
|
|
|
|
def ios_device?
|
|
MobileDetection.ios_device?(request.user_agent)
|
|
end
|
|
|
|
def customization_disabled?
|
|
request.env[ApplicationController::NO_CUSTOM]
|
|
end
|
|
|
|
def include_ios_native_app_banner?
|
|
current_user && current_user.trust_level >= 1 && SiteSetting.native_app_install_banner_ios
|
|
end
|
|
|
|
def ios_app_argument
|
|
# argument only makes sense for DiscourseHub app
|
|
SiteSetting.ios_app_id == "1173672076" ?
|
|
", app-argument=discourse://new?siteUrl=#{Discourse.base_url}" : ""
|
|
end
|
|
|
|
def allow_plugins?
|
|
!request.env[ApplicationController::NO_PLUGINS]
|
|
end
|
|
|
|
def allow_third_party_plugins?
|
|
allow_plugins? && !request.env[ApplicationController::ONLY_OFFICIAL]
|
|
end
|
|
|
|
def normalized_safe_mode
|
|
safe_mode = []
|
|
|
|
safe_mode << ApplicationController::NO_CUSTOM if customization_disabled?
|
|
safe_mode << ApplicationController::NO_PLUGINS if !allow_plugins?
|
|
safe_mode << ApplicationController::ONLY_OFFICIAL if !allow_third_party_plugins?
|
|
|
|
safe_mode.join(",")
|
|
end
|
|
|
|
def loading_admin?
|
|
return false unless defined?(controller)
|
|
controller.class.name.split("::").first == "Admin"
|
|
end
|
|
|
|
def category_badge(category, opts = nil)
|
|
CategoryBadge.html_for(category, opts).html_safe
|
|
end
|
|
|
|
def self.all_connectors
|
|
@all_connectors = Dir.glob("plugins/*/app/views/connectors/**/*.html.erb")
|
|
end
|
|
|
|
def server_plugin_outlet(name)
|
|
|
|
# Don't evaluate plugins in test
|
|
return "" if Rails.env.test?
|
|
|
|
matcher = Regexp.new("/connectors/#{name}/.*\.html\.erb$")
|
|
erbs = ApplicationHelper.all_connectors.select { |c| c =~ matcher }
|
|
return "" if erbs.blank?
|
|
|
|
result = +""
|
|
erbs.each { |erb| result << render(inline: File.read(erb)) }
|
|
result.html_safe
|
|
end
|
|
|
|
def topic_featured_link_domain(link)
|
|
begin
|
|
uri = UrlHelper.encode_and_parse(link)
|
|
uri = URI.parse("http://#{uri}") if uri.scheme.nil?
|
|
host = uri.host.downcase
|
|
host.start_with?('www.') ? host[4..-1] : host
|
|
rescue
|
|
''
|
|
end
|
|
end
|
|
|
|
def theme_ids
|
|
if customization_disabled?
|
|
[nil]
|
|
else
|
|
request.env[:resolved_theme_ids]
|
|
end
|
|
end
|
|
|
|
def scheme_id
|
|
return @scheme_id if defined?(@scheme_id)
|
|
|
|
custom_user_scheme_id = cookies[:color_scheme_id] || current_user&.user_option&.color_scheme_id
|
|
if custom_user_scheme_id && ColorScheme.find_by_id(custom_user_scheme_id)
|
|
return custom_user_scheme_id
|
|
end
|
|
|
|
return if theme_ids.blank?
|
|
|
|
@scheme_id = Theme
|
|
.where(id: theme_ids.first)
|
|
.pluck(:color_scheme_id)
|
|
.first
|
|
end
|
|
|
|
def dark_scheme_id
|
|
cookies[:dark_scheme_id] || current_user&.user_option&.dark_scheme_id || SiteSetting.default_dark_mode_color_scheme_id
|
|
end
|
|
|
|
def current_homepage
|
|
current_user&.user_option&.homepage || SiteSetting.anonymous_homepage
|
|
end
|
|
|
|
def build_plugin_html(name)
|
|
return "" unless allow_plugins?
|
|
DiscoursePluginRegistry.build_html(name, controller) || ""
|
|
end
|
|
|
|
# If there is plugin HTML return that, otherwise yield to the template
|
|
def replace_plugin_html(name)
|
|
if (html = build_plugin_html(name)).present?
|
|
html
|
|
else
|
|
yield
|
|
nil
|
|
end
|
|
end
|
|
|
|
def theme_lookup(name)
|
|
Theme.lookup_field(theme_ids, mobile_view? ? :mobile : :desktop, name)
|
|
end
|
|
|
|
def theme_translations_lookup
|
|
Theme.lookup_field(theme_ids, :translations, I18n.locale)
|
|
end
|
|
|
|
def theme_js_lookup
|
|
Theme.lookup_field(theme_ids, :extra_js, nil)
|
|
end
|
|
|
|
def discourse_stylesheet_link_tag(name, opts = {})
|
|
if opts.key?(:theme_ids)
|
|
ids = opts[:theme_ids] unless customization_disabled?
|
|
else
|
|
ids = theme_ids
|
|
end
|
|
|
|
Stylesheet::Manager.stylesheet_link_tag(name, 'all', ids)
|
|
end
|
|
|
|
def discourse_color_scheme_stylesheets
|
|
result = +""
|
|
result << Stylesheet::Manager.color_scheme_stylesheet_link_tag(scheme_id, 'all', theme_ids)
|
|
|
|
if dark_scheme_id != -1
|
|
result << Stylesheet::Manager.color_scheme_stylesheet_link_tag(dark_scheme_id, '(prefers-color-scheme: dark)', theme_ids)
|
|
end
|
|
result.html_safe
|
|
end
|
|
|
|
def dark_color_scheme?
|
|
return false if scheme_id.blank?
|
|
ColorScheme.find_by_id(scheme_id)&.is_dark?
|
|
end
|
|
|
|
def preloaded_json
|
|
return '{}' if @preloaded.blank?
|
|
@preloaded.transform_values { |value| escape_unicode(value) }.to_json
|
|
end
|
|
|
|
def client_side_setup_data
|
|
service_worker_url = Rails.env.development? ? 'service-worker.js' : Rails.application.assets_manifest.assets['service-worker.js']
|
|
|
|
setup_data = {
|
|
cdn: Rails.configuration.action_controller.asset_host,
|
|
base_url: Discourse.base_url,
|
|
base_uri: Discourse.base_path,
|
|
environment: Rails.env,
|
|
letter_avatar_version: LetterAvatar.version,
|
|
markdown_it_url: script_asset_path('markdown-it-bundle'),
|
|
service_worker_url: service_worker_url,
|
|
default_locale: SiteSetting.default_locale,
|
|
asset_version: Discourse.assets_digest,
|
|
disable_custom_css: loading_admin?,
|
|
highlight_js_path: HighlightJs.path,
|
|
svg_sprite_path: SvgSprite.path(theme_ids),
|
|
enable_js_error_reporting: GlobalSetting.enable_js_error_reporting,
|
|
color_scheme_is_dark: dark_color_scheme?,
|
|
user_color_scheme_id: scheme_id,
|
|
user_dark_scheme_id: dark_scheme_id
|
|
}
|
|
|
|
if Rails.env.development?
|
|
setup_data[:svg_icon_list] = SvgSprite.all_icons(theme_ids)
|
|
|
|
if ENV['DEBUG_PRELOADED_APP_DATA']
|
|
setup_data[:debug_preloaded_app_data] = true
|
|
end
|
|
end
|
|
|
|
if guardian.can_enable_safe_mode? && params["safe_mode"]
|
|
setup_data[:safe_mode] = normalized_safe_mode
|
|
end
|
|
|
|
if SiteSetting.Upload.enable_s3_uploads
|
|
setup_data[:s3_cdn] = SiteSetting.Upload.s3_cdn_url.presence
|
|
setup_data[:s3_base_url] = SiteSetting.Upload.s3_base_url
|
|
end
|
|
|
|
setup_data
|
|
end
|
|
|
|
def get_absolute_image_url(link)
|
|
absolute_url = link
|
|
if link.start_with?('//')
|
|
uri = URI(Discourse.base_url)
|
|
absolute_url = "#{uri.scheme}:#{link}"
|
|
elsif link.start_with?('/uploads/', '/images/', '/user_avatar/')
|
|
absolute_url = "#{Discourse.base_url}#{link}"
|
|
elsif GlobalSetting.relative_url_root && link.start_with?(GlobalSetting.relative_url_root)
|
|
absolute_url = "#{Discourse.base_url_no_prefix}#{link}"
|
|
end
|
|
absolute_url
|
|
end
|
|
|
|
def can_sign_up?
|
|
SiteSetting.allow_new_registrations &&
|
|
!SiteSetting.invite_only &&
|
|
!SiteSetting.enable_sso
|
|
end
|
|
|
|
def rss_creator(user)
|
|
if user
|
|
if SiteSetting.prioritize_username_in_ux
|
|
"#{user.username}"
|
|
else
|
|
"#{user.name.presence || user.username }"
|
|
end
|
|
end
|
|
end
|
|
|
|
def authentication_data
|
|
return @authentication_data if defined?(@authentication_data)
|
|
|
|
@authentication_data = begin
|
|
value = cookies[:authentication_data]
|
|
if value
|
|
cookies.delete(:authentication_data, path: Discourse.base_path("/"))
|
|
end
|
|
current_user ? nil : value
|
|
end
|
|
end
|
|
end
|