# frozen_string_literal: true # A class we can use to serialize the site data class Site include ActiveModel::Serialization cattr_accessor :preloaded_category_custom_fields def self.reset_preloaded_category_custom_fields self.preloaded_category_custom_fields = Set.new end reset_preloaded_category_custom_fields ## # Sometimes plugins need to have additional data or options available # when rendering custom markdown features/rules that are not available # on the default opts.discourse object. These additional options should # be namespaced to the plugin adding them. # # ``` # Site.markdown_additional_options["chat"] = { limited_pretty_text_markdown_rules: [] } # ``` # # These are passed down to markdown rules on opts.discourse.additionalOptions. cattr_accessor :markdown_additional_options self.markdown_additional_options = {} def self.add_categories_callbacks(&block) categories_callbacks << block end def self.categories_callbacks @categories_callbacks ||= [] end def initialize(guardian) @guardian = guardian end def site_setting SiteSetting end def notification_types Notification.types end def trust_levels TrustLevel.levels end def user_fields UserField.includes(:user_field_options).order(:position).all end def self.categories_cache_key "site_categories_#{Discourse.git_version}" end def self.clear_cache Discourse.cache.delete(categories_cache_key) end def self.all_categories_cache # Categories do not change often so there is no need for us to run the # same query and spend time creating ActiveRecord objects for every requests. # # Do note that any new association added to the eager loading needs a # corresponding ActiveRecord callback to clear the categories cache. Discourse .cache .fetch(categories_cache_key, expires_in: 30.minutes) do categories = Category .includes( :uploaded_logo, :uploaded_logo_dark, :uploaded_background, :tags, :tag_groups, :form_templates, category_required_tag_groups: :tag_group, ) .joins("LEFT JOIN topics t on t.id = categories.topic_id") .select("categories.*, t.slug topic_slug") .order(:position) .to_a if preloaded_category_custom_fields.present? Category.preload_custom_fields(categories, preloaded_category_custom_fields) end ActiveModel::ArraySerializer.new( categories, each_serializer: SiteCategorySerializer, ).as_json end end def categories @categories ||= begin categories = [] self.class.all_categories_cache.each do |category| if @guardian.can_see_serialized_category?( category_id: category[:id], read_restricted: category[:read_restricted], ) categories << category end end with_children = Set.new categories.each { |c| with_children << c[:parent_category_id] if c[:parent_category_id] } allowed_topic_create = nil unless @guardian.is_admin? allowed_topic_create_ids = @guardian.anonymous? ? [] : Category.topic_create_allowed(@guardian).pluck(:id) allowed_topic_create = Set.new(allowed_topic_create_ids) end by_id = {} notification_levels = CategoryUser.notification_levels_for(@guardian.user) default_notification_level = CategoryUser.default_notification_level categories.each do |category| category[:notification_level] = notification_levels[category[:id]] || default_notification_level category[:permission] = CategoryGroup.permission_types[ :full ] if allowed_topic_create&.include?(category[:id]) || @guardian.is_admin? category[:has_children] = with_children.include?(category[:id]) category[:can_edit] = @guardian.can_edit_serialized_category?( category_id: category[:id], read_restricted: category[:read_restricted], ) by_id[category[:id]] = category end categories.reject! { |c| c[:parent_category_id] && !by_id[c[:parent_category_id]] } self.class.categories_callbacks.each { |callback| callback.call(categories, @guardian) } categories end end def groups Group.visible_groups(@guardian.user, "name ASC", include_everyone: true).includes(:flair_upload) end def archetypes Archetype.list.reject { |t| t.id == Archetype.private_message } end def auth_providers Discourse.enabled_auth_providers end def self.json_for(guardian) if guardian.anonymous? && SiteSetting.login_required return( { periods: TopTopic.periods.map(&:to_s), filters: Discourse.filters.map(&:to_s), user_fields: UserField .includes(:user_field_options) .order(:position) .all .map { |userfield| UserFieldSerializer.new(userfield, root: false, scope: guardian) }, auth_providers: Discourse.enabled_auth_providers.map do |provider| AuthProviderSerializer.new(provider, root: false, scope: guardian) end, }.to_json ) end seq = nil if guardian.anonymous? seq = MessageBus.last_id("/site_json") cached_json, cached_seq, cached_version = Discourse.redis.mget("site_json", "site_json_seq", "site_json_version") if cached_json && seq == cached_seq.to_i && Discourse.git_version == cached_version return cached_json end end site = Site.new(guardian) json = MultiJson.dump(SiteSerializer.new(site, root: false, scope: guardian)) if guardian.anonymous? Discourse.redis.multi do |transaction| transaction.setex "site_json", 1800, json transaction.set "site_json_seq", seq transaction.set "site_json_version", Discourse.git_version end end json end SITE_JSON_CHANNEL = "/site_json" def self.clear_anon_cache! # publishing forces the sequence up # the cache is validated based on the sequence MessageBus.publish(SITE_JSON_CHANNEL, "") end end