# frozen_string_literal: true # # A class that handles interaction between a plugin and the Discourse App. # class DiscoursePluginRegistry # Plugins often need to be able to register additional handlers, data, or # classes that will be used by core classes. This should be used if you # need to control which type the registry is, and if it doesn't need to # be removed if the plugin is disabled. # # Shortcut to create new register in the plugin registry # - Register is created in a class variable using the specified name/type # - Defines singleton method to access the register # - Defines instance method as a shortcut to the singleton method # - Automatically deletes the register on registry.reset! def self.define_register(register_name, type) @@register_names ||= Set.new @@register_names << register_name define_singleton_method(register_name) do instance_variable_get(:"@#{register_name}") || instance_variable_set(:"@#{register_name}", type.new) end define_method(register_name) { self.class.public_send(register_name) } end # Plugins often need to add values to a list, and we need to filter those # lists at runtime to ignore values from disabled plugins. Unlike define_register, # the type of the register cannot be defined, and is always Array. # # Create a new register (see `define_register`) with some additions: # - Register is created in a class variable using the specified name/type # - Defines singleton method to access the register # - Defines instance method as a shortcut to the singleton method # - Automatically deletes the register on registry.reset! def self.define_filtered_register(register_name) define_register(register_name, Array) singleton_class.alias_method :"_raw_#{register_name}", :"#{register_name}" define_singleton_method(register_name) do unfiltered = public_send(:"_raw_#{register_name}") unfiltered.filter { |v| v[:plugin].enabled? }.map { |v| v[:value] }.uniq end define_singleton_method("register_#{register_name.to_s.singularize}") do |value, plugin| public_send(:"_raw_#{register_name}") << { plugin: plugin, value: value } end end define_register :javascripts, Set define_register :auth_providers, Set define_register :service_workers, Set define_register :admin_javascripts, Set define_register :stylesheets, Hash define_register :mobile_stylesheets, Hash define_register :desktop_stylesheets, Hash define_register :color_definition_stylesheets, Hash define_register :handlebars, Set define_register :serialized_current_user_fields, Set define_register :seed_data, HashWithIndifferentAccess define_register :locales, HashWithIndifferentAccess define_register :svg_icons, Set define_register :custom_html, Hash define_register :asset_globs, Set define_register :html_builders, Hash define_register :seed_path_builders, Set define_register :vendored_pretty_text, Set define_register :vendored_core_pretty_text, Set define_register :seedfu_filter, Set define_register :demon_processes, Set define_register :groups_callback_for_users_search_controller_action, Hash define_filtered_register :staff_user_custom_fields define_filtered_register :public_user_custom_fields define_filtered_register :self_editable_user_custom_fields define_filtered_register :staff_editable_user_custom_fields define_filtered_register :editable_group_custom_fields define_filtered_register :group_params define_filtered_register :topic_thumbnail_sizes define_filtered_register :topic_preloader_associations define_filtered_register :api_parameter_routes define_filtered_register :api_key_scope_mappings define_filtered_register :user_api_key_scope_mappings define_filtered_register :permitted_bulk_action_parameters define_filtered_register :reviewable_params define_filtered_register :reviewable_score_links define_filtered_register :presence_channel_prefixes define_filtered_register :push_notification_filters define_filtered_register :notification_consolidation_plans define_filtered_register :email_unsubscribers define_filtered_register :user_destroyer_on_content_deletion_callbacks define_filtered_register :hashtag_autocomplete_data_sources define_filtered_register :hashtag_autocomplete_contextual_type_priorities define_filtered_register :search_groups_set_query_callbacks define_filtered_register :about_stat_groups define_filtered_register :bookmarkables define_filtered_register :list_suggested_for_providers def self.register_auth_provider(auth_provider) self.auth_providers << auth_provider end def register_js(filename, options = {}) # If we have a server side option, add that too. self.class.javascripts << filename end def self.register_service_worker(filename, options = {}) self.service_workers << filename end def self.register_svg_icon(icon) self.svg_icons << icon end def register_css(filename, plugin_directory_name) self.class.stylesheets[plugin_directory_name] ||= Set.new self.class.stylesheets[plugin_directory_name] << filename end def self.register_locale(locale, options = {}) self.locales[locale] = options end def register_archetype(name, options = {}) Archetype.register(name, options) end def self.register_glob(root, extension, options = nil) self.asset_globs << [root, extension, options || {}] end def self.each_globbed_asset(each_options = nil) each_options ||= {} self.asset_globs.each do |g| root, ext, options = *g if options[:admin] next unless each_options[:admin] else next if each_options[:admin] end Dir.glob("#{root}/**/*.#{ext}") { |f| yield f } end end JS_REGEX = /\.js$|\.js\.erb$|\.js\.es6\z/ HANDLEBARS_REGEX = /\.(hb[rs]|js\.handlebars)\z/ def self.register_asset(asset, opts = nil, plugin_directory_name = nil) if asset =~ JS_REGEX if opts == :admin self.admin_javascripts << asset elsif opts == :vendored_pretty_text self.vendored_pretty_text << asset elsif opts == :vendored_core_pretty_text self.vendored_core_pretty_text << asset else self.javascripts << asset end elsif asset =~ /\.css$|\.scss\z/ if opts == :mobile self.mobile_stylesheets[plugin_directory_name] ||= Set.new self.mobile_stylesheets[plugin_directory_name] << asset elsif opts == :desktop self.desktop_stylesheets[plugin_directory_name] ||= Set.new self.desktop_stylesheets[plugin_directory_name] << asset elsif opts == :color_definitions self.color_definition_stylesheets[plugin_directory_name] = asset else self.stylesheets[plugin_directory_name] ||= Set.new self.stylesheets[plugin_directory_name] << asset end elsif asset =~ HANDLEBARS_REGEX self.handlebars << asset end end def self.stylesheets_exists?(plugin_directory_name, target = nil) case target when :desktop self.desktop_stylesheets[plugin_directory_name].present? when :mobile self.mobile_stylesheets[plugin_directory_name].present? else self.stylesheets[plugin_directory_name].present? end end def self.register_seed_data(key, value) self.seed_data[key] = value end def self.register_seed_path_builder(&block) seed_path_builders << block end def self.register_html_builder(name, &block) html_builders[name] ||= [] html_builders[name] << block end def self.build_html(name, ctx = nil) builders = html_builders[name] || [] builders.map { |b| b.call(ctx) }.join("\n").html_safe end def self.seed_paths result = SeedFu.fixture_paths.dup unless Rails.env.test? && ENV["LOAD_PLUGINS"] != "1" seed_path_builders.each { |b| result += b.call } end result.uniq end def self.register_seedfu_filter(filter = nil) self.seedfu_filter << filter end VENDORED_CORE_PRETTY_TEXT_MAP = { "moment.js" => "vendor/assets/javascripts/moment.js", "moment-timezone.js" => "vendor/assets/javascripts/moment-timezone-with-data.js", } def self.core_asset_for_name(name) asset = VENDORED_CORE_PRETTY_TEXT_MAP[name] raise KeyError, "Asset #{name} not found in #{VENDORED_CORE_PRETTY_TEXT_MAP}" unless asset asset end def self.clear_modifiers! if Rails.env.test? && GlobalSetting.load_plugins? raise "Clearing modifiers during a plugin spec run will affect all future specs. Use unregister_modifier instead." end @modifiers = nil end def self.register_modifier(plugin_instance, name, &blk) @modifiers ||= {} modifiers = @modifiers[name] ||= [] modifiers << [plugin_instance, blk] end def self.unregister_modifier(plugin_instance, name, &blk) raise "unregister_modifier can only be used in tests" if !Rails.env.test? modifiers_for_name = @modifiers&.[](name) raise "no #{name} modifiers found" if !modifiers_for_name i = modifiers_for_name.find_index { |info| info == [plugin_instance, blk] } raise "no modifier found for that plugin/block combination" if !i modifiers_for_name.delete_at(i) end def self.apply_modifier(name, arg, *more_args) return arg if !@modifiers registered_modifiers = @modifiers[name] return arg if !registered_modifiers # iterate as fast as possible to minimize cost (avoiding each) # also erases one stack frame length = registered_modifiers.length index = 0 while index < length plugin_instance, block = registered_modifiers[index] arg = block.call(arg, *more_args) if plugin_instance.enabled? index += 1 end arg end def self.reset! @@register_names.each { |name| instance_variable_set(:"@#{name}", nil) } clear_modifiers! end def self.reset_register!(register_name) found_register = @@register_names.detect { |name| name == register_name } instance_variable_set(:"@#{found_register}", nil) if found_register end end