discourse/lib/discourse_plugin_registry.rb
Ted Johansson 0c875cb4d5
DEV: Make problem check registration more explicit (#26413)
Previously the problem check registry simply looked at the subclasses of ProblemCheck. This was causing some confusion in environments where eager loading is not enabled, as the registry would appear empty as a result of the classes never being referenced (and thus never loaded.)

This PR changes the approach to a more explicit one. I followed other implementations (bookmarkable and hashtag autocomplete.) As a bonus, this now has a neat plugin entry point as well.
2024-03-28 14:00:47 +08:00

294 lines
9.6 KiB
Ruby

# 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 :stylesheets, Hash
define_register :mobile_stylesheets, Hash
define_register :desktop_stylesheets, Hash
define_register :color_definition_stylesheets, Hash
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 :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_register :mail_pollers, Set
define_filtered_register :staff_user_custom_fields
define_filtered_register :public_user_custom_fields
define_filtered_register :staff_editable_topic_custom_fields
define_filtered_register :public_editable_topic_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 :email_notification_filters
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 :stats
define_filtered_register :bookmarkables
define_filtered_register :list_suggested_for_providers
define_filtered_register :summarization_strategies
define_filtered_register :post_action_notify_user_handlers
define_filtered_register :post_strippers
define_filtered_register :problem_checks
def self.register_auth_provider(auth_provider)
self.auth_providers << auth_provider
end
def self.register_mail_poller(mail_poller)
self.mail_pollers << mail_poller
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
JS_REGEX = /\.js$|\.js\.erb$|\.js\.es6\z/
def self.register_asset(asset, opts = nil, plugin_directory_name = nil)
if asset =~ JS_REGEX
if 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
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