0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-04 10:39:43 +08:00
discourse/app/helpers/application_helper.rb
Régis Hanol 97e92041af
UX: Hide signup when the site is in read only or staff only mode (#42101)
Previously, a site in read only or staff only mode still offered
anonymous visitors every way in: the header "Sign Up" button, the signup
call to action at the end of a topic, the create account link on the
login page, and the server-rendered header — including the one on the
read only error page itself. Account creation is blocked in both modes,
so all of them were dead ends. The banner made it worse by describing
what members lose (replying, likes) rather than what an anonymous
visitor is actually unable to do.

This change gates the JS `canSignUp` getter and its Ruby twin
`can_sign_up?` on read only state, which covers every signup entry point
at once. `canSignUp` had to drop its `@computed` decorator to do so:
with no dependent keys it cached permanently, so the new term would have
been evaluated once at boot and then frozen — leaving the button visible
if read only mode started later, and hidden long after it ended.

The "Log In" button deliberately stays, because staff can still log in
during staff only mode. Anonymous visitors now get banner copy naming
signup and login instead, a refused login says only staff can log in
rather than claiming login is disabled outright, and the email and code
login forms show that inline instead of a generic "an error occurred"
dialog.

It also fixes a pre-existing blank page. Both `/login` and `/signup`
aborted the transition when read only, which on a direct URL load left
the application template unrendered — and with it the dialog holder, so
the explanation never appeared either. They now redirect home when there
is no route to stay on, and keep aborting when there is.

Meta ref: /t/408703
2026-07-28 20:05:04 +02:00

1111 lines
33 KiB
Ruby
Vendored

# 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
# This generated equivalent of Ember's config/environment.js is used
# in development, production, and theme tests. (i.e. everywhere except
# regular tests)
def discourse_config_environment(testing: false)
# TODO: Can this come from Ember CLI somehow?
config = {
modulePrefix: "discourse",
environment: Rails.env,
rootURL: Discourse.base_path,
locationType: "history",
EmberENV: {
FEATURES: {
},
EXTEND_PROTOTYPES: false,
},
APP: {
name: "discourse",
version: "#{Discourse::VERSION::STRING} #{Discourse.git_version}",
# LOG_RESOLVER: true,
# LOG_ACTIVE_GENERATION: true,
# LOG_TRANSITIONS: true,
# LOG_TRANSITIONS_INTERNAL: true,
# LOG_VIEW_LOOKUPS: true,
},
}
if testing
config[:environment] = "test"
config[:locationType] = "none"
config[:APP][:LOG_ACTIVE_GENERATION] = false
config[:APP][:LOG_VIEW_LOOKUPS] = false
config[:APP][:rootElement] = "#ember-testing"
config[:APP][:autoboot] = false
end
config.to_json
end
def google_universal_analytics_json(ua_domain_name = nil)
result = {}
result[:cookieDomain] = ua_domain_name.gsub(%r{\Ahttp(s)?://}, "") if ua_domain_name
result[:userId] = current_user.id if current_user.present?
result[:allowLinker] = true if SiteSetting.ga_universal_auto_link_domains.present?
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 csp_nonce_placeholder
ContentSecurityPolicy.nonce_placeholder(response.headers, request_env: request.env)
end
def track_view_session_id_placeholder
response.headers[
::Middleware::TrackViewSessionIdInjector::PLACEHOLDER_HEADER
] ||= "[[track_view_session_id_placeholder_#{SecureRandom.hex}]]"
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]
token = request.env[Auth::DefaultCurrentUserProvider::USER_TOKEN_KEY]
return if !token || token.user != current_user
request.env[sk] = key = (session[sk] ||= SecureRandom.hex)
Auth::DefaultCurrentUserProvider.store_shared_session_key(key, token.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 generate_import_map(plugin_assets)
imports =
plugin_assets
.filter { it[:importmap_name] }
.map { [it[:importmap_name], script_asset_path(it[:name])] }
.to_h
available_plugins = plugin_assets.map { |a| a[:plugin].directory_name }
external_plugin_imports =
(
plugin_assets.flat_map { |a| a[:external_plugin_imports] || [] } +
theme_js_assets.flat_map { |a| a[:external_plugin_imports] }
).uniq
external_plugin_imports.each do |plugin_name|
if available_plugins.include?(plugin_name)
imports["discourse/plugins/#{plugin_name}?"] = imports["discourse/plugins/#{plugin_name}"]
else
imports["discourse/plugins/#{plugin_name}?"] = Plugin::JsManager.optional_plugin_stub
imports["discourse/plugins/#{plugin_name}"] = Plugin::JsManager.required_plugin_stub(
plugin_name,
)
end
end
JSON.pretty_generate({ imports: }).html_safe
end
def script_asset_path(script)
path = ActionController::Base.helpers.asset_path("#{script}.js")
if GlobalSetting.use_s3? && GlobalSetting.s3_cdn_url
resolved_s3_asset_cdn_url =
GlobalSetting.s3_asset_cdn_url.presence || 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(resolved_s3_asset_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 = "#{resolved_s3_asset_cdn_url}#{path}"
end
if is_brotli_req?
path = path.sub("/assets/js/", "/assets/br/")
elsif is_gzip_req?
path = path.sub("/assets/js/", "/assets/gz/")
end
end
path
end
def preload_script(script, type_module: false, attrs: {})
resolved_script = EmberAssets.script_chunks[script]&.first || script
path = script_asset_path(resolved_script)
preload_script_url(path, entrypoint: script, type_module:, attrs:).html_safe
end
def module_preloads_for(*scripts)
resolved_preload_scripts =
scripts.compact.flat_map { |script| EmberAssets.script_chunks[script] }.compact.uniq
modulepreload_tags = resolved_preload_scripts.map { |script| <<~HTML }
<link rel="modulepreload" href="#{script_asset_path script}" nonce="#{csp_nonce_placeholder}">
HTML
modulepreload_tags.join("\n").html_safe
end
def preload_script_url(url, entrypoint: nil, type_module: false, attrs: nil)
entrypoint_attribute = entrypoint ? "data-discourse-entrypoint=\"#{entrypoint}\"" : ""
nonce_attribute = "nonce=\"#{csp_nonce_placeholder}\""
extra_attrs =
attrs&.map { |k, v| "#{ERB::Util.html_escape(k)}=\"#{ERB::Util.html_escape(v)}\"" }&.join(" ")
extra_attrs = " #{extra_attrs}" if extra_attrs.present?
add_resource_preload_list(url, "script")
<<~HTML.html_safe
<script #{type_module ? 'type="module"' : "defer"} src="#{url}" #{entrypoint_attribute}#{extra_attrs} #{nonce_attribute}></script>
HTML
end
def add_resource_preload_list(resource_url, type)
links =
controller.instance_variable_get(:@asset_preload_links) ||
controller.instance_variable_set(:@asset_preload_links, [])
links << %Q(<#{resource_url}>; rel="preload"; as="#{type}")
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
csrf_meta_tags if current_user
end
def html_classes
list = []
list << "rtl" if rtl?
list << text_size_class
list << "anon" unless current_user
list << @embed_class if @embed_class.present?
list.join(" ")
end
def body_classes
result = ApplicationHelper.extra_body_classes.to_a
result << "category-#{@category.slug_path.join("-")}" if @category && @category.url.present?
if current_user.present? && current_user.primary_group_id &&
primary_group_name = Group.where(id: current_user.primary_group_id).pick(: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, "&#x2028;")
javascript.gsub!(%r{(</)}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?
Rtl::LOCALES.include? I18n.locale.to_s
end
def html_lang
(request ? I18n.locale.to_s : SiteSetting.default_locale).sub("_", "-")
end
def title_content
DiscoursePluginRegistry.apply_modifier(
:meta_data_content,
content_for(:title) || SiteSetting.title,
:title,
{ url: request.fullpath },
)
end
def description_content
DiscoursePluginRegistry.apply_modifier(
:meta_data_content,
@description_meta || SiteSetting.site_description,
:description,
{ url: request.fullpath },
)
end
def is_crawler_homepage?
request.path == "/" && use_crawler_layout?
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 slug generation method is encoded, non encoded urls can sneak in
# via bots
url = opts[:url]
if url.encoding.name != "UTF-8" || !url.valid_encoding?
opts[:url] = url.dup.force_encoding("UTF-8").scrub!
end
if opts[:image].blank?
x_summary_large_image_url = SiteSetting.site_x_summary_large_image_url
opts[:x_summary_large_image] = x_summary_large_image_url if x_summary_large_image_url.present?
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[:x_summary_large_image] = get_absolute_image_url(opts[:x_summary_large_image]) if opts[
:x_summary_large_image
].present?
result = []
result << tag(:meta, property: "og:site_name", content: opts[:site_name] || SiteSetting.title)
result << tag(:meta, property: "og:type", content: "website")
generate_twitter_card_metadata(result, opts)
if opts[:image].present?
result << tag(:meta, property: "og:image", content: opts[:image])
if opts[:image_width].present? && opts[:image_height].present?
result << tag(:meta, property: "og:image:width", content: opts[:image_width])
result << tag(:meta, property: "og:image:height", content: opts[:image_height])
end
if opts[:image_type].present?
result << tag(:meta, property: "og:image:type", content: opts[:image_type])
end
end
%i[url title description].each do |property|
if opts[property].present?
content = (property == :url ? opts[property] : gsub_emoji_to_unicode(opts[property]))
content =
DiscoursePluginRegistry.apply_modifier(:meta_data_content, content, property, opts)
result << tag(:meta, { property: "og:#{property}", content: content }, nil, true)
result << tag(:meta, { name: "twitter:#{property}", content: content }, nil, true)
end
end
Array
.wrap(opts[:breadcrumbs])
.each do |breadcrumb|
result << tag(:meta, property: "og:article:section", content: breadcrumb[:name])
result << tag(:meta, property: "og:article:section:color", content: breadcrumb[:color])
end
Array
.wrap(opts[:tags])
.each { |tag_name| result << tag(:meta, property: "og:article:tag", content: tag_name) }
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: I18n.t("reading_time_minutes", count: opts[:read_time]),
)
result << tag(:meta, name: "twitter:label2", value: I18n.t("likes"))
result << tag(
:meta,
name: "twitter:data2",
value: I18n.t("likes_count", count: opts[:like_count]),
)
end
if opts[:published_time]
result << tag(:meta, property: "article:published_time", content: opts[:published_time])
end
result << tag(:meta, property: "og:ignore_canonical", content: true) if opts[:ignore_canonical]
result.join("\n")
end
private def generate_twitter_card_metadata(result, opts)
img_url = opts[:x_summary_large_image].presence || opts[:image]
# Twitter does not allow SVGs, see https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup
if img_url.ends_with?(".svg")
img_url = SiteSetting.site_logo_url.ends_with?(".svg") ? nil : SiteSetting.site_logo_url
end
if opts[:x_summary_large_image].present? && img_url.present?
result << tag(:meta, name: "twitter:card", content: "summary_large_image")
result << tag(:meta, name: "twitter:image", content: img_url)
elsif opts[:image].present? && img_url.present?
result << tag(:meta, name: "twitter:card", content: "summary")
result << tag(:meta, name: "twitter:image", content: img_url)
else
result << tag(:meta, name: "twitter:card", content: "summary")
end
end
def render_sitelinks_search_tag
if current_page?("/") || current_page?(Discourse.base_path)
json = {
"@context" => "http://schema.org",
"@type" => "WebSite",
:url => Discourse.base_url,
:name => SiteSetting.title,
: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
end
def discourse_pageview_tracking_meta_tags
if !SiteSetting.trigger_browser_pageview_events && !SiteSetting.persist_browser_pageview_events
return ""
end
tags = +""
tags << tag.meta(
name: "discourse-track-view-session-id",
content: track_view_session_id_placeholder,
)
if UpcomingChanges.enabled?(:dashboard_improvements)
tags << tag.meta(name: "discourse-beacon-pageview-enabled", content: "true")
end
if SiteSetting.persist_browser_pageview_events
tags << tag.meta(name: "discourse-engagement-tracking-enabled", content: "true")
end
tags.html_safe
end
def gsub_emoji_to_unicode(str)
Emoji.gsub_emoji_to_unicode(str)
end
def application_logo_url
@application_logo_url ||=
if mobile_device?
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
def application_logo_dark_url
@application_logo_dark_url ||=
if dark_scheme_id != -1
if mobile_device? && SiteSetting.site_mobile_logo_dark_url != application_logo_url
SiteSetting.site_mobile_logo_dark_url
elsif !mobile_device? && SiteSetting.site_logo_dark_url != application_logo_url
SiteSetting.site_logo_dark_url
end
end
end
def waving_hand_url
UrlHelper.cook_url(Emoji.url_for(":wave:t#{rand(2..6)}:"))
end
def login_path
"#{Discourse.base_path}/login"
end
def mobile_device?
MobileDetection.mobile_device?(request.user_agent)
end
def crawler_layout?
controller&.use_crawler_layout?
end
def include_crawler_content?
if current_user && !crawler_layout?
params.key?(:print)
else
return false if !current_user && SiteSetting.login_required?
crawler_layout? || !mobile_device? || !modern_mobile_device?
end
end
def modern_mobile_device?
MobileDetection.modern_mobile_device?(request.user_agent)
end
def customization_disabled?
request.env[ApplicationController::NO_THEMES]
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
if SiteSetting.ios_app_id == "1173672076"
", app-argument=discourse://new?siteUrl=#{Discourse.base_url}"
else
""
end
end
def custom_splash_screen_enabled?
@custom_splash_screen_enabled ||= SiteSetting.splash_screen_image.is_a?(Upload)
end
def splash_screen_image_animated?
build_splash_screen_image unless defined?(@splash_screen_image_svg)
@splash_screen_image_svg.present? && @splash_screen_image_svg.match?(/@keyframes\s/)
end
def splash_screen_inline_svg
build_splash_screen_image unless defined?(@splash_screen_image_svg)
@splash_screen_image_svg&.html_safe
end
def splash_screen_image_data_uri(dark: false)
build_splash_screen_image unless defined?(@splash_screen_image_svg)
return nil if @splash_screen_image_svg.blank?
# Replace CSS variable references with actual theme colors
svg_with_colors = @splash_screen_image_svg.dup
color_method = dark ? :dark_color_hex_for_name : :light_color_hex_for_name
primary = "##{public_send(color_method, "primary")}"
secondary = "##{public_send(color_method, "secondary")}"
tertiary = "##{public_send(color_method, "tertiary")}"
svg_with_colors.gsub!(/var\(\s*--primary\s*\)/, primary)
svg_with_colors.gsub!(/var\(\s*--secondary\s*\)/, secondary)
svg_with_colors.gsub!(/var\(\s*--tertiary\s*\)/, tertiary)
# Use base64 encoding for better compatibility with complex SVGs
"data:image/svg+xml;base64,#{Base64.strict_encode64(svg_with_colors)}"
end
private
def build_splash_screen_image
@splash_screen_image_svg = nil
upload = SiteSetting.splash_screen_image
return unless upload.is_a?(Upload)
@splash_screen_image_svg =
Discourse
.cache
.fetch("splash_screen_svg_#{upload.id}_#{upload.sha1}", expires_in: 1.day) do
upload.content.presence
rescue StandardError => e
Discourse.warn_exception(e, message: "Failed to fetch splash screen logo SVG")
nil
end
end
public
def allow_plugins?
!request.env[ApplicationController::NO_PLUGINS]
end
def allow_third_party_plugins?
allow_plugins? && !request.env[ApplicationController::NO_UNOFFICIAL_PLUGINS]
end
def normalized_safe_mode
safe_mode = []
safe_mode << ApplicationController::NO_THEMES if customization_disabled?
safe_mode << ApplicationController::NO_PLUGINS if !allow_plugins?
safe_mode << ApplicationController::NO_UNOFFICIAL_PLUGINS if !allow_third_party_plugins?
safe_mode.join(",")
end
def loading_admin?
return false unless defined?(controller)
return false if controller.class.name.blank?
controller.class.name.split("::").first == "Admin"
end
def category_badge(category, opts = nil)
CategoryBadge.html_for(category, opts).html_safe
end
SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES = [Rails.root.join("plugins/").to_s]
private_constant :SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES
if Rails.env.test?
SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES << Rails.root.join("spec/fixtures/plugins/").to_s
end
SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES =
SERVER_PLUGIN_OUTLET_PLUGINS_PREFIXES.each_with_object({}) do |plugins_prefix, connectors|
Dir
.glob("#{plugins_prefix}*/app/views/connectors/**/*.html.erb")
.each do |template_path|
template_path =~ Regexp.new("/connectors/(.*)/.*\.html\.erb$")
outlet_name = Regexp.last_match(1)
connectors[outlet_name] ||= []
connectors[outlet_name] << begin
ActionView::Template.new(
File.read(template_path),
"discourse_plugin_outlet__#{name}",
ActionView::Template.handler_for_extension("erb"),
locals: [],
format: :html,
virtual_path: template_path,
)
end
end
end
private_constant :SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES
def server_plugin_outlet(name, locals: {})
return "" if !GlobalSetting.load_plugins?
return "" if !SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES.key?(name)
SERVER_PLUGIN_OUTLET_CONNECTOR_TEMPLATES[name]
.map { |template| render template:, locals: }
.join
.html_safe
end
def topic_featured_link_domain(link)
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 StandardError
""
end
def theme_id
if customization_disabled?
nil
else
request.env[:resolved_theme_id]
end
end
def stylesheet_manager
return @stylesheet_manager if defined?(@stylesheet_manager)
@stylesheet_manager = Stylesheet::Manager.new(theme_id: theme_id)
end
def user_scheme_id
return @user_scheme_id if defined?(@user_scheme_id)
scheme_id = cookies[:color_scheme_id] || current_user&.user_option&.color_scheme_id
@user_scheme_id = ColorScheme.valid_id(scheme_id) if ColorScheme.exists?(
id: scheme_id,
user_selectable: true,
) || theme&.color_scheme_id == scheme_id.to_i
end
def scheme_id
return @scheme_id if defined?(@scheme_id)
if user_scheme_id
return user_scheme_id unless theme_limits_color_schemes?
return user_scheme_id if ColorScheme.exists?(id: user_scheme_id, theme_id: theme_id)
end
return if theme_id.blank?
@scheme_id = Theme.where(id: theme_id).pick(:color_scheme_id)
end
def theme
@theme = theme_id ? Theme.find_by_id(theme_id) : Theme.find_default
end
def user_dark_scheme_id
return @user_dark_scheme_id if defined?(@user_dark_scheme_id)
scheme_id = cookies[:dark_scheme_id] || current_user&.user_option&.dark_scheme_id
@user_dark_scheme_id = ColorScheme.valid_id(scheme_id) if ColorScheme.exists?(
id: scheme_id,
user_selectable: true,
) || theme&.dark_color_scheme_id == scheme_id.to_i
end
def dark_scheme_id
if user_dark_scheme_id
return user_dark_scheme_id unless theme_limits_color_schemes?
return user_dark_scheme_id if ColorScheme.exists?(id: user_dark_scheme_id, theme_id: theme_id)
end
theme = theme_id ? Theme.find_by_id(theme_id) : Theme.find_default
dark_id = theme&.dark_color_scheme_id
return dark_id if dark_id.present?
return theme&.color_scheme_id if theme_limits_color_schemes?
-1
end
def theme_limits_color_schemes?
return @theme_limits_color_schemes if defined?(@theme_limits_color_schemes)
@theme_limits_color_schemes =
theme_id.present? &&
ThemeModifierSet.exists?(theme_id: theme_id, only_theme_color_schemes: true)
end
def current_homepage
current_user&.user_option&.homepage || HomepageHelper.resolve(request, current_user)
end
def build_plugin_html(name, **kwargs)
return "" unless allow_plugins?
DiscoursePluginRegistry.build_html(name, controller, **kwargs) || ""
end
def crawler_topic_container_schema(topic)
tag.attributes(
DiscoursePluginRegistry.apply_modifier(
:topic_crawler_container_schema,
{ itemscope: true, itemtype: "http://schema.org/DiscussionForumPosting" },
topic,
),
)
end
def crawler_topic_main_entity_schema(topic)
tag.attributes(
DiscoursePluginRegistry.apply_modifier(:topic_crawler_main_entity_schema, {}, topic),
).presence
end
def crawler_post_schema_hash(post, topic)
@crawler_post_schema_hash ||= {}
@crawler_post_schema_hash[post.id] ||= begin
default = {
itemprop: "comment",
itemscope: true,
itemtype: "http://schema.org/Comment",
} unless post.is_first_post?
DiscoursePluginRegistry.apply_modifier(:topic_crawler_post_schema, default || {}, post, topic)
end
end
def crawler_post_schema(post, topic)
tag.attributes(crawler_post_schema_hash(post, topic))
end
def crawler_post_schema_overridden?(post, topic)
hash = crawler_post_schema_hash(post, topic)
itemprop = hash[:itemprop]
(itemprop.present? && itemprop != "comment") || hash[:data].present?
end
def crawler_post_emits_microdata?(post, topic)
post.is_first_post? || crawler_post_schema_hash(post, topic)[:itemscope]
end
def crawler_post_schema_skip?(post, topic)
DiscoursePluginRegistry.apply_modifier(:topic_crawler_skip_post, false, post, topic)
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_id,
mobile_device? ? :mobile : :desktop,
name,
skip_transformation: request.env[:skip_theme_ids_transformation].present?,
csp_nonce: csp_nonce_placeholder,
)
end
def theme_translations_lookup
Theme.lookup_field(
theme_id,
:translations,
I18n.locale,
skip_transformation: request.env[:skip_theme_ids_transformation].present?,
csp_nonce: csp_nonce_placeholder,
)
end
def theme_js_assets
Theme.js_asset_info(
theme_id,
skip_transformation: request.env[:skip_theme_ids_transformation].present?,
)
end
def discourse_stylesheet_preload_tag(name, opts = {})
manager =
if opts.key?(:theme_id)
Stylesheet::Manager.new(theme_id: customization_disabled? ? nil : opts[:theme_id])
else
stylesheet_manager
end
manager.stylesheet_preload_tag(name, "all")
end
def discourse_stylesheet_link_tag(name, opts = {})
manager =
if opts.key?(:theme_id)
Stylesheet::Manager.new(theme_id: customization_disabled? ? nil : opts[:theme_id])
else
stylesheet_manager
end
name = :"#{name}_rtl" if opts[:supports_rtl] && rtl?
manager.stylesheet_link_tag(name, opts[:media] || "all", method(:add_resource_preload_list))
end
def discourse_preload_color_scheme_stylesheets
result = +""
result << stylesheet_manager.color_scheme_stylesheet_preload_tag(
scheme_id,
fallback_to_base: true,
)
if dark_scheme_id != -1
result << stylesheet_manager.color_scheme_stylesheet_preload_tag(
dark_scheme_id,
fallback_to_base: false,
)
end
result.html_safe
end
def discourse_color_scheme_stylesheets
light_href =
stylesheet_manager.color_scheme_stylesheet_link_tag_href(scheme_id, fallback_to_base: true)
add_resource_preload_list(light_href, "style")
dark_href = nil
if dark_scheme_id != -1
dark_href =
stylesheet_manager.color_scheme_stylesheet_link_tag_href(
dark_scheme_id,
fallback_to_base: false,
)
end
result = +""
if dark_href && dark_href != light_href
add_resource_preload_list(dark_href, "style")
result << color_scheme_stylesheet_link_tag(
light_href,
light_elements_media_query,
"light-scheme",
scheme_id,
)
result << color_scheme_stylesheet_link_tag(
dark_href,
dark_elements_media_query,
"dark-scheme",
dark_scheme_id,
)
else
result << color_scheme_stylesheet_link_tag(light_href, "all", "light-scheme", scheme_id)
end
result.html_safe
end
def discourse_theme_color_meta_tags
result = +""
if dark_scheme_id != -1
result << <<~HTML
<meta name="theme-color" media="#{light_elements_media_query}" content="##{light_color_hex_for_name("header_background")}">
<meta name="theme-color" media="#{dark_elements_media_query}" content="##{dark_color_hex_for_name("header_background")}">
HTML
else
result << <<~HTML
<meta name="theme-color" media="all" content="##{light_color_hex_for_name("header_background")}">
HTML
end
result.html_safe
end
def discourse_color_scheme_meta_tag
scheme =
if dark_scheme_id == -1
# no automatic client-side switching
dark_color_scheme? ? "dark" : "light"
else
# auto-switched based on browser setting
"light dark"
end
<<~HTML.html_safe
<meta name="color-scheme" content="#{scheme}">
HTML
end
def dark_color_scheme?
return false if scheme_id.blank?
ColorScheme.find_by_id(scheme_id)&.is_dark?
end
def forced_light_mode?
return false if dark_color_scheme?
cookie = cookies[:forced_color_mode]
return cookie == "light" if cookie.present?
!!current_user&.user_option&.light_mode_forced?
end
def forced_dark_mode?
return false if dark_scheme_id == -1
cookie = cookies[:forced_color_mode]
return cookie == "dark" if cookie.present?
!!current_user&.user_option&.dark_mode_forced?
end
def light_color_hex_for_name(name)
ColorScheme.hex_for_name(name, scheme_id)
end
def dark_color_hex_for_name(name)
ColorScheme.hex_for_name(name, dark_scheme_id)
end
def dark_elements_media_query
if forced_light_mode?
"none"
elsif forced_dark_mode?
"all"
else
"(prefers-color-scheme: dark)"
end
end
def light_elements_media_query
if forced_light_mode?
"all"
elsif forced_dark_mode?
"none"
else
"(prefers-color-scheme: light)"
end
end
def preloaded_json
return "{}" if !@application_layout_preloader
@application_layout_preloader
.preloaded_data
.transform_values { |value| escape_unicode(value) }
.to_json
end
def client_side_setup_data
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,
service_worker_url: "service-worker.js",
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_id),
enable_js_error_reporting: GlobalSetting.enable_js_error_reporting,
color_scheme_is_dark: dark_color_scheme?,
user_color_scheme_id: user_scheme_id || -1,
user_dark_scheme_id: user_dark_scheme_id || -1,
is_staff: staff?,
}
if Rails.env.development?
setup_data[:svg_icon_list] = SvgSprite.all_icons(theme_id)
setup_data[:debug_preloaded_app_data] = true if ENV["DEBUG_PRELOADED_APP_DATA"]
setup_data[:mb_last_file_change_id] = MessageBus.last_id("/file-change")
end
if Rails.env.test?
if ENV["CAPYBARA_PLAYWRIGHT_DEBUG_CLIENT_SETTLED"].present?
setup_data[:capybara_playwright_debug_client_settled] = true
end
# Allow controlling deprecation behavior in tests via environment variable
# Used to enforce or disable deprecation throwing for specific test runs
if ENV["EMBER_RAISE_ON_DEPRECATION"] == "1"
setup_data[:raise_on_deprecation] = true
elsif ENV["EMBER_RAISE_ON_DEPRECATION"] == "0"
setup_data[:raise_on_deprecation] = false
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 escape_noscript(&block)
raw capture(&block).gsub(%r{<(/\s*noscript)}i, '&lt;\1')
end
def manifest_url
# If you want the `manifest_url` to be different for a specific action,
# in the action set @manifest_url = X. Originally added for chat to add a
# separate manifest
@manifest_url || "#{Discourse.base_path}/manifest.webmanifest"
end
def can_sign_up?
!@readonly_mode && SiteSetting.allow_new_registrations && !SiteSetting.invite_only &&
!SiteSetting.enable_discourse_connect
end
def rss_creator(user)
user&.display_name
end
def anonymous_top_menu_items
Discourse.anonymous_top_menu_items.map(&:to_s)
end
def authentication_data
return @authentication_data if defined?(@authentication_data)
@authentication_data =
begin
value = cookies[:authentication_data]
cookies.delete(:authentication_data, path: Discourse.base_path("/")) if value
current_user ? nil : value
end
end
def color_scheme_stylesheet_link_tag(href, media, css_class, scheme_id)
scheme_id = Integer(scheme_id, exception: false)
%[<link href="#{href}" media="#{media}" rel="stylesheet" class="#{css_class}"#{scheme_id && scheme_id != -1 ? %[ data-scheme-id="#{scheme_id}"] : ""}/>]
end
end