discourse/app/helpers/application_helper.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

576 lines
16 KiB
Ruby
Raw Normal View History

# coding: utf-8
# frozen_string_literal: true
2013-02-06 03:16:51 +08:00
require 'current_user'
require 'canonical_url'
2013-02-06 03:16:51 +08:00
module ApplicationHelper
include CurrentUser
include CanonicalURL::Helpers
include ConfigurableUrls
include GlobalPath
def self.extra_body_classes
@extra_body_classes ||= Set.new
end
2013-02-06 03:16:51 +08:00
def google_universal_analytics_json(ua_domain_name = nil)
result = {}
if ua_domain_name
result[:cookieDomain] = ua_domain_name.gsub(/^http(s)?:\/\//, '')
end
2015-04-08 01:09:49 +08:00
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
2015-04-08 01:09:49 +08:00
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
2018-11-16 05:13:18 +08:00
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
2018-11-16 05:13:18 +08:00
path
end
def preload_script(script)
path = script_asset_path(script)
preload_script_url(path)
end
2018-11-16 05:13:18 +08:00
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(' ')
2014-08-27 19:38:03 +08:00
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
2013-09-10 14:01:36 +08:00
javascript.gsub!(/\342\200\250/u, '&#x2028;')
javascript.gsub!(/(<\/)/u, '\u003C/')
javascript
else
''
end
end
def format_topic_title(title)
2016-11-23 01:37:22 +08:00
PrettyText.unescape_emoji strip_tags(title)
2015-10-15 15:59:29 +08:00
end
2013-02-06 03:16:51 +08:00
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
2013-02-06 03:16:51 +08:00
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
(request ? I18n.locale.to_s : 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
FEATURE: Automatically generate optimized site metadata icons (#7372) This change automatically resizes icons for various purposes. Admins can now upload `logo` and `logo_small`, and everything else will be auto-generated. Specific icons can still be uploaded separately if required. ## Core - Adds an SiteIconManager module which manages automatic resizing and fallback - Icons are looked up in the OptimizedImage table at runtime, and then cached in Redis. If the resized version is missing for some reason, then most icons will fall back to the original files. Some icons (e.g. PWA Manifest) will return `nil` (because an incorrectly sized icon is worse than a missing icon). - `SiteSetting.site_large_icon_url` will return the optimized version, including any fallback. `SiteSetting.large_icon` continues to return the upload object. This means that (almost) no changes are required in core/plugins to support this new system. - Icons are resized whenever a relevant site setting is changed, and during post-deploy migrations ## Wizard - Allows `requiresRefresh` wizard steps to reload data via AJAX instead of a full page reload - Add placeholders to the **icons** step of the wizard, which automatically update from the "Square Logo" - Various copy updates to support the changes - Remove the "upload-time" resizing for `large_icon`. This is no longer required. ## Site Settings UX - Move logo/icon settings under a new "Branding" tab - Various copy changes to support the changes - Adds placeholder support to the `image-uploader` component - Automatically reloads site settings after saving. This allows setting placeholders to change based on changes to other settings - Upload site settings will be assigned a placeholder if SiteIconManager `responds_to?` an icon of the same name ## Dashboard Warnings - Remove PWA icon and PWA title warnings. Both are now handled automatically. ## Bonus - Updated the sketch logos to use @awesomerobot's new high-res designs
2019-05-01 21:44:45 +08:00
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 = {
2016-04-14 17:22:26 +08:00
'@context' => 'http://schema.org',
'@type' => 'WebSite',
url: Discourse.base_url,
potentialAction: {
2016-04-14 17:22:26 +08:00
'@type' => 'SearchAction',
target: "#{Discourse.base_url}/search?q={search_term_string}",
2016-04-14 17:22:26 +08:00
'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)
2017-07-22 02:24:28 +08:00
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
2013-11-14 23:41:16 +08:00
def ios_device?
MobileDetection.ios_device?(request.user_agent)
end
2013-11-14 23:41:16 +08:00
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(",")
2013-11-14 23:41:16 +08:00
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,
2018-11-16 05:13:18 +08:00
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
}
Upgrade to FontAwesome 5 (take two) (#6673) * Add missing icons to set * Revert FA5 revert This reverts commit 42572ff * use new SVG syntax in locales * Noscript page changes (remove login button, center "powered by" footer text) * Cast wider net for SVG icons in settings - include any _icon setting for SVG registry (offers better support for plugin settings) - let themes store multiple pipe-delimited icons in a setting - also replaces broken onebox image icon with SVG reference in cooked post processor * interpolate icons in locales * Fix composer whisper icon alignment * Add support for stacked icons * SECURITY: enforce hostname to match discourse hostname This ensures that the hostname rails uses for various helpers always matches the Discourse hostname * load SVG sprite with pre-initializers * FIX: enable caching on SVG sprites * PERF: use JSONP for SVG sprites so they are served from CDN This avoids needing to deal with CORS for loading of the SVG Note, added the svg- prefix to the filename so we can quickly tell in dev tools what the file is * Add missing SVG sprite JSONP script to CSP * Upgrade to FA 5.5.0 * Add support for all FA4.7 icons - adds complete frontend and backend for renamed FA4.7 icons - improves performance of SvgSprite.bundle and SvgSprite.all_icons * Fix group avatar flair preview - adds an endpoint at /svg-sprites/search/:keyword - adds frontend ajax call that pulls icon in avatar flair preview even when it is not in subset * Remove FA 4.7 font files
2018-11-27 05:49:57 +08:00
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
Upgrade to FontAwesome 5 (take two) (#6673) * Add missing icons to set * Revert FA5 revert This reverts commit 42572ff * use new SVG syntax in locales * Noscript page changes (remove login button, center "powered by" footer text) * Cast wider net for SVG icons in settings - include any _icon setting for SVG registry (offers better support for plugin settings) - let themes store multiple pipe-delimited icons in a setting - also replaces broken onebox image icon with SVG reference in cooked post processor * interpolate icons in locales * Fix composer whisper icon alignment * Add support for stacked icons * SECURITY: enforce hostname to match discourse hostname This ensures that the hostname rails uses for various helpers always matches the Discourse hostname * load SVG sprite with pre-initializers * FIX: enable caching on SVG sprites * PERF: use JSONP for SVG sprites so they are served from CDN This avoids needing to deal with CORS for loading of the SVG Note, added the svg- prefix to the filename so we can quickly tell in dev tools what the file is * Add missing SVG sprite JSONP script to CSP * Upgrade to FA 5.5.0 * Add support for all FA4.7 icons - adds complete frontend and backend for renamed FA4.7 icons - improves performance of SvgSprite.bundle and SvgSprite.all_icons * Fix group avatar flair preview - adds an endpoint at /svg-sprites/search/:keyword - adds frontend ajax call that pulls icon in avatar flair preview even when it is not in subset * Remove FA 4.7 font files
2018-11-27 05:49:57 +08:00
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 &&
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
!SiteSetting.enable_discourse_connect
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
2013-02-06 03:16:51 +08:00
end