2019-05-03 08:17:27 +10:00
# frozen_string_literal: true
2013-08-23 16:21:52 +10:00
require "digest/sha1"
require "fileutils"
2022-03-21 15:28:52 +01:00
require "plugin/metadata"
require "auth"
2013-08-23 16:21:52 +10:00
2016-07-22 12:59:43 -04:00
class Plugin :: CustomEmoji
2020-03-30 20:16:10 +02:00
CACHE_KEY ||= "plugin-emoji"
2016-07-22 12:59:43 -04:00
def self . cache_key
2020-03-30 20:16:10 +02:00
@@cache_key ||= CACHE_KEY
2016-07-22 12:59:43 -04:00
end
def self . emojis
@@emojis ||= {}
end
2020-03-30 20:16:10 +02:00
def self . clear_cache
@@cache_key = CACHE_KEY
@@emojis = {}
2020-05-27 20:11:52 +02:00
@@translations = {}
2016-07-22 12:59:43 -04:00
end
2019-01-04 15:14:16 +01:00
2020-03-30 20:16:10 +02:00
def self . register ( name , url , group = Emoji :: DEFAULT_GROUP )
@@cache_key = Digest :: SHA1 . hexdigest ( cache_key + name + group ) [ 0 .. 10 ]
new_group = emojis [ group ] || {}
new_group [ name ] = url
emojis [ group ] = new_group
end
def self . unregister ( name , group = Emoji :: DEFAULT_GROUP )
emojis [ group ]. delete ( name )
2019-07-03 09:23:40 +02:00
end
2020-05-27 12:08:24 +02:00
def self . translations
@@translations ||= {}
end
def self . translate ( from , to )
@@cache_key = Digest :: SHA1 . hexdigest ( cache_key + from ) [ 0 .. 10 ]
translations [ from ] = to
end
2016-07-22 12:59:43 -04:00
end
2013-08-23 16:21:52 +10:00
class Plugin :: Instance
2013-08-26 11:04:16 +10:00
attr_accessor :path , :metadata
2015-02-06 17:32:59 -05:00
attr_reader :admin_route
2013-08-23 16:21:52 +10:00
2015-02-04 12:59:18 -05:00
# Memoized array readers
2023-01-09 12:10:19 +00:00
% i [
2017-01-12 15:43:09 -05:00
assets
color_schemes
2018-11-30 16:58:18 +00:00
before_auth_initializers
2017-01-12 15:43:09 -05:00
initializers
javascripts
2018-01-25 12:09:18 +01:00
locales
2017-11-22 17:02:01 -08:00
service_workers
2017-01-12 15:43:09 -05:00
styles
2018-11-30 09:51:45 -05:00
themes
csp_extensions
2020-03-13 15:30:31 +00:00
asset_filters
2018-11-30 09:51:45 -05:00
]. each do | att |
2015-02-04 12:59:18 -05:00
class_eval %Q{
def #{ att }
@ #{ att } ||= []
end
}
end
2016-07-22 12:59:43 -04:00
def seed_data
@seed_data ||= HashWithIndifferentAccess . new ({})
2015-06-04 15:56:17 -04:00
end
2020-06-15 15:28:07 -06:00
def seed_fu_filter ( filter = nil )
@seed_fu_filter = filter
end
2013-08-23 16:21:52 +10:00
def self . find_all ( parent_path )
[]. tap do | plugins |
2013-10-21 11:18:24 +02:00
# also follows symlinks - http://stackoverflow.com/q/357754
2017-05-16 17:28:45 -04:00
Dir [ " #{ parent_path } /*/plugin.rb" ]. sort . each do | path |
2013-08-23 16:21:52 +10:00
source = File . read ( path )
metadata = Plugin :: Metadata . parse ( source )
plugins << self . new ( metadata , path )
end
2023-01-09 12:10:19 +00:00
end
2013-08-23 16:21:52 +10:00
end
2013-08-26 11:04:16 +10:00
def initialize ( metadata = nil , path = nil )
2013-08-23 16:21:52 +10:00
@metadata = metadata
@path = path
2015-08-21 11:28:17 -04:00
@idx = 0
2013-08-23 16:21:52 +10:00
end
2019-12-05 14:57:18 -05:00
def register_anonymous_cache_key ( key , & block )
key_method = "key_ #{ key } "
2019-12-11 09:07:22 -05:00
add_to_class ( Middleware :: AnonymousCache :: Helper , key_method , & block )
2019-12-05 14:57:18 -05:00
Middleware :: AnonymousCache . cache_key_segments [ key ] = key_method
Middleware :: AnonymousCache . compile_key_builder
end
2015-02-06 17:32:59 -05:00
def add_admin_route ( label , location )
@admin_route = { label : label , location : location }
end
2015-02-04 16:23:39 -05:00
def enabled?
2019-05-07 11:00:09 +10:00
@enabled_site_setting ? SiteSetting . get ( @enabled_site_setting ) : true
2013-08-26 11:04:16 +10:00
end
2015-02-04 16:23:39 -05:00
delegate :name , to : :metadata
2015-04-23 19:33:29 +02:00
def add_to_serializer ( serializer , attr , define_include_method = true , & block )
2017-08-09 22:22:18 +02:00
reloadable_patch do | plugin |
2019-08-27 18:21:53 +10:00
base =
2023-01-09 12:10:19 +00:00
begin
2019-08-27 18:21:53 +10:00
" #{ serializer . to_s . classify } Serializer" . constantize
rescue StandardError
" #{ serializer . to_s } Serializer" . constantize
2023-01-09 12:10:19 +00:00
end
2015-04-23 19:33:29 +02:00
2019-08-27 18:21:53 +10:00
# we have to work through descendants cause serializers may already be baked and cached
( [ base ] + base . descendants ) . each do | klass |
unless attr . to_s . start_with? ( "include_" )
klass . attributes ( attr )
2015-04-23 19:33:29 +02:00
2019-08-27 18:21:53 +10:00
if define_include_method
# Don't include serialized methods if the plugin is disabled
klass . public_send ( :define_method , "include_ #{ attr } ?" ) { plugin . enabled? }
end
2017-08-09 22:22:18 +02:00
end
2019-08-27 18:21:53 +10:00
klass . public_send ( :define_method , attr , & block )
end
2017-08-09 12:28:32 -04:00
end
2015-02-04 16:23:39 -05:00
end
2023-03-30 14:39:55 +11:00
def register_modifier ( modifier_name , & blk )
DiscoursePluginRegistry . register_modifier ( self , modifier_name , & blk )
end
2018-07-25 16:44:09 +01:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2018-06-19 15:00:11 +02:00
def add_report ( name , & block )
2018-07-25 16:44:09 +01:00
reloadable_patch { | plugin | Report . add_report ( name , & block ) }
2018-06-19 15:00:11 +02:00
end
2018-07-25 16:44:09 +01:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2020-07-02 11:47:43 -03:00
def replace_flags ( settings : :: FlagSettings . new , score_type_names : [] )
2020-01-17 11:59:38 -03:00
next_flag_id = ReviewableScore . types . values . max + 1
2020-07-03 16:21:06 -03:00
yield ( settings , next_flag_id ) if block_given?
2017-10-17 13:31:45 -04:00
reloadable_patch do | plugin |
2018-07-25 16:44:09 +01:00
:: PostActionType . replace_flag_settings ( settings )
2020-01-17 11:59:38 -03:00
:: ReviewableScore . reload_types
2020-07-02 11:47:43 -03:00
:: ReviewableScore . add_new_types ( score_type_names )
2017-10-17 13:31:45 -04:00
end
end
2016-03-11 15:52:18 -05:00
def whitelist_staff_user_custom_field ( field )
2021-12-02 12:16:55 -06:00
Discourse . deprecate (
"whitelist_staff_user_custom_field is deprecated, use the allow_staff_user_custom_field." ,
drop_from : "2.6" ,
raise_error : true ,
)
2020-07-27 10:23:54 +10:00
allow_staff_user_custom_field ( field )
end
def allow_staff_user_custom_field ( field )
2020-05-15 14:04:38 +01:00
DiscoursePluginRegistry . register_staff_user_custom_field ( field , self )
2016-03-11 15:52:18 -05:00
end
2018-10-17 10:33:27 +01:00
def whitelist_public_user_custom_field ( field )
2021-12-02 12:16:55 -06:00
Discourse . deprecate (
"whitelist_public_user_custom_field is deprecated, use the allow_public_user_custom_field." ,
drop_from : "2.6" ,
raise_error : true ,
)
2020-07-27 10:23:54 +10:00
allow_public_user_custom_field ( field )
end
def allow_public_user_custom_field ( field )
2020-05-15 14:04:38 +01:00
DiscoursePluginRegistry . register_public_user_custom_field ( field , self )
2018-10-17 10:33:27 +01:00
end
2019-10-11 09:57:55 +01:00
def register_editable_user_custom_field ( field , staff_only : false )
2020-05-15 14:04:38 +01:00
if staff_only
DiscoursePluginRegistry . register_staff_editable_user_custom_field ( field , self )
else
DiscoursePluginRegistry . register_self_editable_user_custom_field ( field , self )
2018-09-04 11:45:36 +01:00
end
end
2019-06-05 23:05:33 -03:00
def register_editable_group_custom_field ( field )
2020-05-15 14:04:38 +01:00
DiscoursePluginRegistry . register_editable_group_custom_field ( field , self )
2019-06-05 23:05:33 -03:00
end
2020-08-07 12:47:00 +10:00
# Allows to define custom search order. Example usage:
# Search.advanced_order(:chars) do |posts|
# posts.reorder("(SELECT LENGTH(raw) FROM posts WHERE posts.topic_id = subquery.topic_id) DESC")
# end
def register_search_advanced_order ( trigger , & block )
Search . advanced_order ( trigger , & block )
end
# Allows to define custom search filters. Example usage:
# Search.advanced_filter(/^min_chars:(\d+)$/) do |posts, match|
# posts.where("(SELECT LENGTH(p2.raw) FROM posts p2 WHERE p2.id = posts.id) >= ?", match.to_i)
# end
def register_search_advanced_filter ( trigger , & block )
Search . advanced_filter ( trigger , & block )
end
2021-05-10 08:57:58 +10:00
# Allows to define TopicView posts filters. Example usage:
# TopicView.advanced_filter do |posts, opts|
# posts.where(wiki: true)
# end
def register_topic_view_posts_filter ( trigger , & block )
TopicView . add_custom_filter ( trigger , & block )
end
2021-08-25 13:16:08 +03:00
# Allows to add more user IDs to the list of preloaded users. This can be
# useful to efficiently change the list of posters or participants.
# Example usage:
# register_topic_list_preload_user_ids do |topics, user_ids, topic_list|
# user_ids << Discourse::SYSTEM_USER_ID
# end
def register_topic_list_preload_user_ids ( & block )
TopicList . on_preload_user_ids ( & block )
end
2020-09-14 11:58:28 +10:00
# Allow to eager load additional tables in Search. Useful to avoid N+1 performance problems.
# Example usage:
# register_search_topic_eager_load do |opts|
# %i(example_table)
# end
# OR
# register_search_topic_eager_load(%i(example_table))
def register_search_topic_eager_load ( tables = nil , & block )
Search . custom_topic_eager_load ( tables , & block )
end
2020-05-23 00:56:13 -04:00
# Request a new size for topic thumbnails
# Will respect plugin enabled setting is enabled
# Size should be an array with two elements [max_width, max_height]
def register_topic_thumbnail_size ( size )
if ! ( size . kind_of? ( Array ) && size . length == 2 )
raise ArgumentError . new ( "Topic thumbnail dimension is not valid" )
end
DiscoursePluginRegistry . register_topic_thumbnail_size ( size , self )
end
2021-07-19 13:54:19 +08:00
# Register a callback to add custom payload to Site#categories
# Example usage:
# register_site_categories_callback do |categories|
# categories.each do |category|
# category[:some_field] = 'test'
# end
# end
def register_site_categories_callback ( & block )
Site . add_categories_callbacks ( & block )
end
2022-02-16 09:00:30 +02:00
def register_upload_unused ( & block )
Upload . add_unused_callback ( & block )
end
def register_upload_in_use ( & block )
Upload . add_in_use_callback ( & block )
end
2017-08-30 14:24:03 -04:00
def custom_avatar_column ( column )
reloadable_patch do | plugin |
2020-07-17 10:48:08 +01:00
UserLookup . lookup_columns << column
UserLookup . lookup_columns . uniq!
2017-08-30 14:24:03 -04:00
end
end
2018-07-25 16:44:09 +01:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-09-28 13:16:51 -04:00
def add_body_class ( class_name )
2018-07-25 16:44:09 +01:00
reloadable_patch { | plugin | :: ApplicationHelper . extra_body_classes << class_name }
2017-09-28 13:16:51 -04:00
end
2017-10-02 12:04:59 -04:00
def rescue_from ( exception , & block )
reloadable_patch { | plugin | :: ApplicationController . rescue_from ( exception , & block ) }
end
2015-02-04 16:23:39 -05:00
# Extend a class but check that the plugin is enabled
2015-08-21 11:28:17 -04:00
# for class methods use `add_class_method`
2017-08-09 12:28:32 -04:00
def add_to_class ( class_name , attr , & block )
2017-08-09 22:22:18 +02:00
reloadable_patch do | plugin |
2017-08-09 12:28:32 -04:00
klass =
2023-01-09 12:10:19 +00:00
begin
2017-08-09 12:28:32 -04:00
class_name . to_s . classify . constantize
rescue StandardError
class_name . to_s . constantize
2023-01-09 12:10:19 +00:00
end
2017-08-09 12:28:32 -04:00
hidden_method_name = :" #{ attr } _without_enable_check"
2019-05-07 09:27:05 +08:00
klass . public_send ( :define_method , hidden_method_name , & block )
2017-08-09 12:28:32 -04:00
2022-12-21 19:02:40 +02:00
klass . public_send ( :define_method , attr ) do |* args , ** kwargs |
public_send ( hidden_method_name , * args , ** kwargs ) if plugin . enabled?
2017-08-09 12:28:32 -04:00
end
2015-02-04 16:23:39 -05:00
end
2015-01-12 10:52:55 -05:00
end
2015-08-21 11:28:17 -04:00
# Adds a class method to a class, respecting if plugin is enabled
2017-08-09 12:28:32 -04:00
def add_class_method ( klass_name , attr , & block )
2017-08-09 22:22:18 +02:00
reloadable_patch do | plugin |
2017-08-09 12:28:32 -04:00
klass =
2023-01-09 12:10:19 +00:00
begin
2017-08-09 12:28:32 -04:00
klass_name . to_s . classify . constantize
rescue StandardError
klass_name . to_s . constantize
2023-01-09 12:10:19 +00:00
end
2015-08-21 11:28:17 -04:00
2017-08-09 12:28:32 -04:00
hidden_method_name = :" #{ attr } _without_enable_check"
2019-05-07 09:27:05 +08:00
klass . public_send ( :define_singleton_method , hidden_method_name , & block )
2015-08-21 11:28:17 -04:00
2022-12-21 19:02:40 +02:00
klass . public_send ( :define_singleton_method , attr ) do |* args , ** kwargs |
public_send ( hidden_method_name , * args , ** kwargs ) if plugin . enabled?
2017-08-09 12:28:32 -04:00
end
2015-08-21 11:28:17 -04:00
end
end
2017-08-09 12:28:32 -04:00
def add_model_callback ( klass_name , callback , options = {}, & block )
2017-08-09 22:22:18 +02:00
reloadable_patch do | plugin |
2017-08-09 12:28:32 -04:00
klass =
2023-01-09 12:10:19 +00:00
begin
2017-08-09 12:28:32 -04:00
klass_name . to_s . classify . constantize
rescue StandardError
klass_name . to_s . constantize
2023-01-09 12:10:19 +00:00
end
2015-08-21 11:28:17 -04:00
2017-08-09 12:28:32 -04:00
# generate a unique method name
method_name = " #{ plugin . name } _ #{ klass . name } _ #{ callback }#{ @idx } " . underscore
@idx += 1
hidden_method_name = :" #{ method_name } _without_enable_check"
2019-05-07 09:27:05 +08:00
klass . public_send ( :define_method , hidden_method_name , & block )
2015-08-21 11:28:17 -04:00
2022-12-21 19:02:40 +02:00
klass . public_send ( callback , ** options ) do |* args , ** kwargs |
public_send ( hidden_method_name , * args , ** kwargs ) if plugin . enabled?
2017-08-09 12:28:32 -04:00
end
2015-08-21 11:28:17 -04:00
2017-08-09 12:28:32 -04:00
hidden_method_name
end
2015-08-21 11:28:17 -04:00
end
2017-08-12 04:21:02 +02:00
def topic_view_post_custom_fields_whitelister ( & block )
2021-12-02 12:16:55 -06:00
Discourse . deprecate (
"topic_view_post_custom_fields_whitelister is deprecated, use the topic_view_post_custom_fields_allowlister." ,
drop_from : "2.6" ,
raise_error : true ,
)
2020-07-27 10:23:54 +10:00
topic_view_post_custom_fields_allowlister ( & block )
end
# Add a post_custom_fields_allowlister block to the TopicView, respecting if the plugin is enabled
def topic_view_post_custom_fields_allowlister ( & block )
2017-08-12 04:21:02 +02:00
reloadable_patch do | plugin |
2021-10-22 10:22:09 +08:00
:: TopicView . add_post_custom_fields_allowlister do | user , topic |
plugin . enabled? ? block . call ( user , topic ) : []
2018-07-25 16:44:09 +01:00
end
2017-08-12 04:21:02 +02:00
end
end
2020-08-04 11:57:33 +02:00
# Allows to add additional user_ids to the list of people notified when doing a post revision
def add_post_revision_notifier_recipients ( & block )
reloadable_patch do | plugin |
:: PostActionNotifier . add_post_revision_notifier_recipients do | post_revision |
plugin . enabled? ? block . call ( post_revision ) : []
end
end
end
2018-07-25 16:44:09 +01:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-12 04:21:02 +02:00
def add_preloaded_group_custom_field ( field )
2018-07-25 16:44:09 +01:00
reloadable_patch { | plugin | :: Group . preloaded_custom_field_names << field }
2017-08-12 04:21:02 +02:00
end
2018-07-25 16:44:09 +01:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-12 04:21:02 +02:00
def add_preloaded_topic_list_custom_field ( field )
2018-07-25 16:44:09 +01:00
reloadable_patch { | plugin | :: TopicList . preloaded_custom_fields << field }
2017-08-12 04:21:02 +02:00
end
2018-07-25 16:44:09 +01:00
# Add a permitted_create_param to Post, respecting if the plugin is enabled
2019-12-20 10:37:12 -06:00
def add_permitted_post_create_param ( name , type = :string )
2017-08-12 04:21:02 +02:00
reloadable_patch do | plugin |
2019-12-20 10:37:12 -06:00
:: Post . plugin_permitted_create_params [ name ] = { plugin : plugin , type : type }
2017-08-12 04:21:02 +02:00
end
end
2021-03-24 10:22:16 -05:00
# Add a permitted_update_param to Post, respecting if the plugin is enabled
def add_permitted_post_update_param ( attribute , & block )
reloadable_patch do | plugin |
:: Post . plugin_permitted_update_params [ attribute ] = { plugin : plugin , handler : block }
end
end
2021-09-06 10:18:51 +10:00
# Add a permitted_param to Group, respecting if the plugin is enabled
# Used in GroupsController#update and Admin::GroupsController#create
def register_group_param ( param )
DiscoursePluginRegistry . register_group_param ( param , self )
end
2021-09-08 09:38:45 +10:00
# Add a custom callback for search to Group
# Callback is called in UsersController#search_users
# Block takes groups and optional current_user
# For example:
# plugin.register_groups_callback_for_users_search_controller_action(:admins_filter) do |groups, user|
# groups.where(name: "admins")
# end
def register_groups_callback_for_users_search_controller_action ( callback , & block )
if DiscoursePluginRegistry . groups_callback_for_users_search_controller_action . key? ( callback )
raise "groups_callback_for_users_search_controller_action callback already registered"
end
DiscoursePluginRegistry . groups_callback_for_users_search_controller_action [ callback ] = block
2021-09-06 10:18:51 +10:00
end
2015-04-23 19:33:29 +02:00
# Add validation method but check that the plugin is enabled
2015-04-26 00:12:19 +02:00
def validate ( klass , name , & block )
2015-04-23 19:33:29 +02:00
klass = klass . to_s . classify . constantize
2019-05-07 09:27:05 +08:00
klass . public_send ( :define_method , name , & block )
2015-04-23 19:33:29 +02:00
plugin = self
2015-04-26 00:12:19 +02:00
klass . validate ( name , if : -> { plugin . enabled? })
2015-04-23 19:33:29 +02:00
end
2013-08-23 16:21:52 +10:00
# will make sure all the assets this plugin needs are registered
def generate_automatic_assets!
paths = []
2015-11-06 15:02:40 +01:00
assets = []
2013-08-23 16:21:52 +10:00
automatic_assets . each do | path , contents |
2015-11-06 15:02:40 +01:00
write_asset ( path , contents )
paths << path
2019-09-16 17:06:34 +01:00
assets << [ path , nil , directory_name ]
2015-11-06 15:02:40 +01:00
end
2013-08-23 16:21:52 +10:00
delete_extra_automatic_assets ( paths )
2015-11-06 15:02:40 +01:00
assets
2013-08-23 16:21:52 +10:00
end
2021-06-22 13:00:04 -05:00
def add_directory_column ( column_name , query :, icon : nil )
validate_directory_column_name ( column_name )
DiscourseEvent . on ( "before_directory_refresh" ) do
DirectoryColumn . find_or_create_plugin_directory_column (
column_name : column_name ,
icon : icon ,
query : query ,
)
end
end
2013-08-23 16:21:52 +10:00
def delete_extra_automatic_assets ( good_paths )
2022-01-05 12:45:08 -05:00
return unless Dir . exist? auto_generated_path
2013-09-20 14:39:14 -07:00
2013-08-23 16:21:52 +10:00
filenames = good_paths . map { | f | File . basename ( f ) }
# nuke old files
Dir . foreach ( auto_generated_path ) do | p |
next if %w[. ..] . include? ( p )
next if filenames . include? ( p )
File . delete ( auto_generated_path + "/ #{ p } " )
end
end
def ensure_directory ( path )
dirname = File . dirname ( path )
FileUtils . mkdir_p ( dirname ) unless File . directory? ( dirname )
end
2017-01-12 15:43:09 -05:00
def directory
File . dirname ( path )
end
2013-08-23 16:21:52 +10:00
def auto_generated_path
File . dirname ( path ) << "/auto_generated"
end
2013-09-17 10:23:21 +10:00
def after_initialize ( & block )
2015-02-04 12:59:18 -05:00
initializers << block
2013-09-17 10:23:21 +10:00
end
2018-11-30 16:58:18 +00:00
def before_auth ( & block )
if @before_auth_complete
raise "Auth providers must be registered before omniauth middleware. after_initialize is too late!"
2023-01-09 12:10:19 +00:00
end
2018-11-30 16:58:18 +00:00
before_auth_initializers << block
end
2015-02-04 16:23:39 -05:00
# A proxy to `DiscourseEvent.on` which does nothing if the plugin is disabled
def on ( event_name , & block )
2022-12-21 19:02:40 +02:00
DiscourseEvent . on ( event_name ) { |* args , ** kwargs | block . call ( * args , ** kwargs ) if enabled? }
2015-02-04 16:23:39 -05:00
end
2013-09-17 10:23:21 +10:00
def notify_after_initialize
2014-06-03 12:36:34 -04:00
color_schemes . each do | c |
2017-08-12 04:21:02 +02:00
unless ColorScheme . where ( name : c [ :name ] ) . exists?
ColorScheme . create_from_base ( name : c [ :name ] , colors : c [ :colors ] )
end
2014-06-03 12:36:34 -04:00
end
2015-02-04 12:59:18 -05:00
initializers . each do | callback |
2015-08-25 16:38:25 -04:00
begin
callback . call ( self )
rescue ActiveRecord :: StatementInvalid => e
2017-08-12 04:21:02 +02:00
# When running `db:migrate` for the first time on a new database,
# plugin initializers might try to use models.
# Tolerate it.
2015-08-25 16:38:25 -04:00
raise e unless e . message . try ( :include? , "PG::UndefinedTable" )
end
2013-09-17 10:23:21 +10:00
end
end
2018-11-30 16:58:18 +00:00
def notify_before_auth
before_auth_initializers . each { | callback | callback . call ( self ) }
@before_auth_complete = true
end
2018-07-25 16:44:09 +01:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-17 15:59:31 +09:00
def register_category_custom_field_type ( name , type )
2018-07-25 16:44:09 +01:00
reloadable_patch { | plugin | Category . register_custom_field_type ( name , type ) }
2017-08-17 15:59:31 +09:00
end
2018-07-25 16:44:09 +01:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-12 04:21:02 +02:00
def register_topic_custom_field_type ( name , type )
2018-07-25 16:44:09 +01:00
reloadable_patch { | plugin | :: Topic . register_custom_field_type ( name , type ) }
2017-08-12 04:21:02 +02:00
end
2018-07-25 16:44:09 +01:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-12 04:21:02 +02:00
def register_post_custom_field_type ( name , type )
2018-07-25 16:44:09 +01:00
reloadable_patch { | plugin | :: Post . register_custom_field_type ( name , type ) }
2017-08-12 04:21:02 +02:00
end
2018-07-25 16:44:09 +01:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-12 04:21:02 +02:00
def register_group_custom_field_type ( name , type )
2018-07-25 16:44:09 +01:00
reloadable_patch { | plugin | :: Group . register_custom_field_type ( name , type ) }
2017-08-12 04:21:02 +02:00
end
2019-07-24 18:38:44 +02:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
def register_user_custom_field_type ( name , type )
reloadable_patch { | plugin | :: User . register_custom_field_type ( name , type ) }
end
2016-10-25 14:55:53 +08:00
def register_seedfu_fixtures ( paths )
paths = [ paths ] if ! paths . kind_of? ( Array )
SeedFu . fixture_paths . concat ( paths )
end
2020-06-15 15:28:07 -06:00
def register_seedfu_filter ( filter = nil )
DiscoursePluginRegistry . register_seedfu_filter ( filter )
end
2014-12-11 17:08:47 +01:00
def listen_for ( event_name )
return unless self . respond_to? ( event_name )
DiscourseEvent . on ( event_name , & self . method ( event_name ))
end
2013-08-23 16:21:52 +10:00
def register_css ( style )
2015-02-04 12:59:18 -05:00
styles << style
2013-08-23 16:21:52 +10:00
end
def register_javascript ( js )
2015-02-04 12:59:18 -05:00
javascripts << js
2013-08-23 16:21:52 +10:00
end
2018-11-26 16:49:57 -05:00
def register_svg_icon ( icon )
DiscoursePluginRegistry . register_svg_icon ( icon )
end
2018-11-30 09:51:45 -05:00
def extend_content_security_policy ( extension )
csp_extensions << extension
end
2020-03-13 15:30:31 +00:00
# Register a block to run when adding css and js assets
# Two arguments will be passed: (type, request)
# Type is :css or :js. `request` is an instance of Rack::Request
# When using this, make sure to consider the effect on AnonymousCache
def register_asset_filter ( & blk )
asset_filters << blk
end
2018-01-25 12:09:18 +01:00
# @option opts [String] :name
# @option opts [String] :nativeName
# @option opts [String] :fallbackLocale
# @option opts [Hash] :plural
def register_locale ( locale , opts = {})
locales << [ locale , opts ]
end
2014-06-05 11:39:33 +10:00
def register_custom_html ( hash )
DiscoursePluginRegistry . custom_html . merge! ( hash )
end
2017-04-17 15:47:21 -04:00
def register_html_builder ( name , & block )
2019-12-04 12:26:23 -05:00
plugin = self
2022-12-21 19:02:40 +02:00
DiscoursePluginRegistry . register_html_builder ( name ) do |* args , ** kwargs |
block . call ( * args , ** kwargs ) if plugin . enabled?
2019-12-04 12:26:23 -05:00
end
2017-04-17 15:47:21 -04:00
end
2014-04-07 16:33:35 +02:00
def register_asset ( file , opts = nil )
2022-11-24 19:03:06 +00:00
raise <<~ ERROR if file . end_with? ( ".hbs" , ".handlebars" )
[ #{name}] Handlebars templates can no longer be included via `register_asset`.
Any hbs files under `assets/javascripts` will be automatically compiled and included . "
ERROR
2018-04-10 08:37:16 +02:00
if opts && opts == :vendored_core_pretty_text
full_path = DiscoursePluginRegistry.core_asset_for_name(file)
else
full_path = File.dirname(path) << " / assets / " << file
end
2019-08-20 22:09:52 +05:30
assets << [full_path, opts, directory_name]
2013-08-23 16:21:52 +10:00
end
2017-11-22 17:02:01 -08:00
def register_service_worker(file, opts = nil)
service_workers << [File.join(File.dirname(path), " assets ", file), opts]
end
2014-06-03 12:36:34 -04:00
def register_color_scheme(name, colors)
color_schemes << { name: name, colors: colors }
2015-06-04 15:56:17 -04:00
end
def register_seed_data(key, value)
seed_data[key] = value
end
2014-06-03 12:36:34 -04:00
2017-11-16 14:42:38 -05:00
def register_seed_path_builder(&block)
DiscoursePluginRegistry.register_seed_path_builder(&block)
end
2020-03-30 20:16:10 +02:00
def register_emoji(name, url, group = Emoji::DEFAULT_GROUP)
Plugin::CustomEmoji.register(name, url, group)
Emoji.clear_cache
2015-11-05 17:25:26 +01:00
end
2020-05-27 12:08:24 +02:00
def translate_emoji(from, to)
Plugin::CustomEmoji.translate(from, to)
end
2013-08-23 16:21:52 +10:00
def automatic_assets
2015-02-04 12:59:18 -05:00
css = styles.join(" \ n ")
js = javascripts.join(" \ n ")
2013-08-23 16:21:52 +10:00
2015-11-06 15:02:40 +01:00
# Generate an IIFE for the JS
js = " ( function (){ #{js}})();" if js.present?
result = []
result << [ css , "css" ] if css . present?
result << [ js , "js" ] if js . present?
result . map do | asset , extension |
hash = Digest :: SHA1 . hexdigest asset
[ " #{ auto_generated_path } /plugin_ #{ hash } . #{ extension } " , asset ]
end
end
2021-05-20 21:43:47 -04:00
# note, we need to be able to parse separately to activation.
2013-08-23 16:21:52 +10:00
# this allows us to present information about a plugin in the UI
# prior to activations
def activate!
2015-04-27 13:06:53 -04:00
if @path
2020-04-13 15:05:46 -04:00
root_dir_name = File . dirname ( @path )
2015-04-27 13:06:53 -04:00
# Automatically include all ES6 JS and hbs files
2020-04-13 15:05:46 -04:00
root_path = " #{ root_dir_name } /assets/javascripts"
2022-06-21 22:07:10 +02:00
DiscoursePluginRegistry . register_glob ( root_path , "js" )
2015-04-27 13:06:53 -04:00
DiscoursePluginRegistry . register_glob ( root_path , "js.es6" )
DiscoursePluginRegistry . register_glob ( root_path , "hbs" )
2020-02-11 13:38:12 -06:00
DiscoursePluginRegistry . register_glob ( root_path , "hbr" )
2015-08-17 15:03:55 -04:00
2020-04-13 15:05:46 -04:00
admin_path = " #{ root_dir_name } /admin/assets/javascripts"
2022-06-21 22:07:10 +02:00
DiscoursePluginRegistry . register_glob ( admin_path , "js" , admin : true )
2015-08-17 15:03:55 -04:00
DiscoursePluginRegistry . register_glob ( admin_path , "js.es6" , admin : true )
DiscoursePluginRegistry . register_glob ( admin_path , "hbs" , admin : true )
2020-02-11 13:38:12 -06:00
DiscoursePluginRegistry . register_glob ( admin_path , "hbr" , admin : true )
2023-01-09 12:10:19 +00:00
2022-06-21 22:07:10 +02:00
DiscourseJsProcessor . plugin_transpile_paths << root_path . sub ( Rails . root . to_s , "" ) . sub (
2023-01-20 12:52:49 -06:00
%r{\A/*} ,
2022-06-21 22:07:10 +02:00
"" ,
)
DiscourseJsProcessor . plugin_transpile_paths << admin_path . sub ( Rails . root . to_s , "" ) . sub (
2023-01-20 12:52:49 -06:00
%r{\A/*} ,
2023-01-09 12:10:19 +00:00
"" ,
)
2020-06-16 14:30:25 -04:00
2022-06-21 22:07:10 +02:00
test_path = " #{ root_dir_name } /test/javascripts"
DiscourseJsProcessor . plugin_transpile_paths << test_path . sub ( Rails . root . to_s , "" ) . sub (
2023-01-20 12:52:49 -06:00
%r{\A/*} ,
2023-01-09 12:10:19 +00:00
"" ,
2022-06-21 22:07:10 +02:00
)
2015-04-27 13:06:53 -04:00
end
2013-09-12 11:27:13 +10:00
self . instance_eval File . read ( path ), path
2013-08-23 16:21:52 +10:00
if auto_assets = generate_automatic_assets!
2015-11-06 15:02:40 +01:00
assets . concat ( auto_assets )
2013-08-23 16:21:52 +10:00
end
2014-12-30 16:29:28 -05:00
register_assets! unless assets . blank?
2018-01-25 12:09:18 +01:00
register_locales!
2017-11-22 17:02:01 -08:00
register_service_workers!
2015-06-04 15:56:17 -04:00
seed_data . each { | key , value | DiscoursePluginRegistry . register_seed_data ( key , value ) }
2015-05-04 16:01:57 +02:00
# TODO: possibly amend this to a rails engine
# Automatically include assets
2014-12-30 16:29:28 -05:00
Rails . configuration . assets . paths << auto_generated_path
Rails . configuration . assets . paths << File . dirname ( path ) + "/assets"
2015-08-17 15:03:55 -04:00
Rails . configuration . assets . paths << File . dirname ( path ) + "/admin/assets"
2015-08-27 16:59:36 -04:00
Rails . configuration . assets . paths << File . dirname ( path ) + "/test/javascripts"
2013-08-23 16:21:52 +10:00
2015-05-04 16:01:57 +02:00
# Automatically include rake tasks
Rake . add_rakelib ( File . dirname ( path ) + "/lib/tasks" )
# Automatically include migrations
2019-12-16 14:11:55 -05:00
migration_paths = ActiveRecord :: Tasks :: DatabaseTasks . migrations_paths
2018-10-09 13:11:45 +08:00
migration_paths << File . dirname ( path ) + "/db/migrate"
unless Discourse . skip_post_deployment_migrations?
migration_paths << " #{ File . dirname ( path ) } / #{ Discourse :: DB_POST_MIGRATE_PATH } "
end
2015-05-04 16:01:57 +02:00
2013-11-20 14:38:21 +11:00
public_data = File . dirname ( path ) + "/public"
2022-01-05 12:45:08 -05:00
if Dir . exist? ( public_data )
2013-11-20 14:38:21 +11:00
target = Rails . root . to_s + "/public/plugins/"
2017-03-17 14:21:30 +08:00
Discourse :: Utils . execute_command ( "mkdir" , "-p" , target )
2014-01-17 15:35:52 -08:00
target << name . gsub ( /\s/ , "_" )
2020-03-04 17:28:26 +00:00
Discourse :: Utils . atomic_ln_s ( public_data , target )
2013-11-20 14:38:21 +11:00
end
2019-07-15 20:22:54 +05:30
2020-03-04 17:28:26 +00:00
ensure_directory ( js_file_path )
2019-07-15 20:22:54 +05:30
contents = []
handlebars_includes . each { | hb | contents << "require_asset(' #{ hb } ')" }
javascript_includes . each { | js | contents << "require_asset(' #{ js } ')" }
2022-08-22 09:56:39 +01:00
if ! contents . present?
[ js_file_path , extra_js_file_path ]. each do | f |
File . delete ( f )
2020-03-04 17:28:26 +00:00
rescue Errno :: ENOENT
end
2022-08-22 09:56:39 +01:00
return
end
contents . insert ( 0 , "<%" )
contents << "%>"
2022-09-21 12:38:02 +01:00
Discourse :: Utils . atomic_write_file ( extra_js_file_path , contents . join ( " \n " ))
2022-08-22 09:56:39 +01:00
begin
2022-09-21 12:38:02 +01:00
File . delete ( js_file_path )
2022-08-22 09:56:39 +01:00
rescue Errno :: ENOENT
2019-07-15 20:22:54 +05:30
end
2013-08-23 16:21:52 +10:00
end
2013-08-26 12:52:36 +10:00
def auth_provider ( opts )
2018-11-30 16:58:18 +00:00
before_auth do
provider = Auth :: AuthProvider . new
2015-09-25 11:29:05 -04:00
2018-11-30 16:58:18 +00:00
Auth :: AuthProvider . auth_attributes . each do | sym |
2019-05-07 10:05:58 +08:00
provider . public_send ( " #{ sym } =" , opts . delete ( sym )) if opts . has_key? ( sym )
2018-11-30 16:58:18 +00:00
end
2018-07-23 16:51:57 +01:00
begin
provider . authenticator . enabled?
rescue NotImplementedError
provider
. authenticator
. define_singleton_method ( :enabled? ) do
2021-11-12 08:52:59 -06:00
Discourse . deprecate (
" #{ provider . authenticator . class . name } should define an `enabled?` function. Patching for now." ,
drop_from : "2.9.0" ,
)
2019-05-07 11:00:09 +10:00
return SiteSetting . get ( provider . enabled_setting ) if provider . enabled_setting
2021-11-12 08:52:59 -06:00
Discourse . deprecate (
" #{ provider . authenticator . class . name } has not defined an enabled_setting. Defaulting to true." ,
drop_from : "2.9.0" ,
)
2018-07-23 16:51:57 +01:00
true
end
end
2018-11-30 16:58:18 +00:00
DiscoursePluginRegistry . register_auth_provider ( provider )
end
2013-08-23 16:21:52 +10:00
end
# shotgun approach to gem loading, in future we need to hack bundler
# to at least determine dependencies do not clash before loading
#
# Additionally we want to support multiple ruby versions correctly and so on
#
# This is a very rough initial implementation
def gem ( name , version , opts = {})
2017-01-09 17:10:14 -05:00
PluginGem . load ( path , name , version , opts )
2013-08-23 16:21:52 +10:00
end
2018-05-08 13:24:58 +08:00
def hide_plugin
Discourse . hidden_plugins << self
end
2018-05-08 10:30:33 +08:00
def enabled_site_setting_filter ( filter = nil )
2020-05-10 15:05:23 +03:00
STDERR . puts ( "`enabled_site_setting_filter` is deprecated" )
2018-05-08 10:30:33 +08:00
end
2015-07-02 09:45:17 -07:00
def enabled_site_setting ( setting = nil )
if setting
@enabled_site_setting = setting
else
@enabled_site_setting
end
2015-02-04 16:23:39 -05:00
end
2016-11-15 11:42:55 +11:00
def handlebars_includes
assets
. map do | asset , opts |
next if opts == :admin
next unless asset =~ DiscoursePluginRegistry :: HANDLEBARS_REGEX
asset
end
. compact
end
def javascript_includes
assets
. map do | asset , opts |
2018-04-10 08:37:16 +02:00
next if opts == :vendored_core_pretty_text
2016-11-15 11:42:55 +11:00
next if opts == :admin
next unless asset =~ DiscoursePluginRegistry :: JS_REGEX
asset
end
. compact
end
def each_globbed_asset
if @path
# Automatically include all ES6 JS and hbs files
root_path = " #{ File . dirname ( @path ) } /assets/javascripts"
2020-05-08 11:10:34 -04:00
admin_path = " #{ File . dirname ( @path ) } /admin/assets/javascripts"
2016-11-15 11:42:55 +11:00
2021-06-16 08:04:21 -07:00
Dir
. glob ( [ " #{ root_path } /**/*" , " #{ admin_path } /**/*" ] )
. sort
. each do | f |
2020-04-13 15:05:46 -04:00
f_str = f . to_s
2016-11-15 11:42:55 +11:00
if File . directory? ( f )
yield [ f , true ]
2021-03-24 19:51:21 +02:00
elsif f_str . end_with? ( ".js.es6" ) || f_str . end_with? ( ".hbs" ) || f_str . end_with? ( ".hbr" )
2020-04-13 15:05:46 -04:00
yield [ f , false ]
2022-06-21 22:07:10 +02:00
elsif f_str . end_with? ( ".js" )
2016-11-15 11:42:55 +11:00
yield [ f , false ]
2023-01-09 12:10:19 +00:00
end
2016-11-15 11:42:55 +11:00
end
end
end
2019-01-03 12:03:01 -05:00
def register_reviewable_type ( reviewable_type_class )
2019-04-08 14:42:36 -03:00
extend_list_method Reviewable , :types , [ reviewable_type_class . name ]
end
def extend_list_method ( klass , method , new_attributes )
2019-05-07 09:27:05 +08:00
current_list = klass . public_send ( method )
2019-04-08 14:42:36 -03:00
current_list . concat ( new_attributes )
2019-01-03 12:03:01 -05:00
2019-05-07 09:27:05 +08:00
reloadable_patch { klass . public_send ( :define_singleton_method , method ) { current_list } }
2019-01-03 12:03:01 -05:00
end
2019-08-20 22:09:52 +05:30
def directory_name
@directory_name ||= File . dirname ( path ) . split ( "/" ) . last
end
2019-08-22 08:39:10 +05:30
def css_asset_exists? ( target = nil )
DiscoursePluginRegistry . stylesheets_exists? ( directory_name , target )
2019-07-15 20:22:54 +05:30
end
def js_asset_exists?
2022-09-21 12:38:02 +01:00
# If assets/javascripts exists, ember-cli will output a .js file
File . exist? ( " #{ File . dirname ( @path ) } /assets/javascripts" )
2022-08-22 09:56:39 +01:00
end
def extra_js_asset_exists?
2022-09-21 12:38:02 +01:00
File . exist? ( extra_js_file_path )
2019-07-15 20:22:54 +05:30
end
2022-08-25 11:36:02 +01:00
def admin_js_asset_exists?
2022-09-21 12:38:02 +01:00
# If this directory exists, ember-cli will output a .js file
File . exist? ( " #{ File . dirname ( @path ) } /admin/assets/javascripts" )
2022-08-25 11:36:02 +01:00
end
2019-11-22 16:33:10 -03:00
# Receives an array with two elements:
# 1. A symbol that represents the name of the value to filter.
# 2. A Proc that takes the existing ActiveRecord::Relation and the value received from the front-end.
def add_custom_reviewable_filter ( filter )
reloadable_patch { Reviewable . add_custom_filter ( filter ) }
end
2021-02-17 14:42:44 -03:00
# Register a new API key scope.
#
# Example:
# add_api_key_scope(:groups, { delete: { actions: %w[groups#add_members], params: %i[id] } })
#
# This scope lets you add members to a group. Additionally, you can specify which group ids are allowed.
# The delete action is added to the groups resource.
2020-07-16 15:51:24 -03:00
def add_api_key_scope ( resource , action )
DiscoursePluginRegistry . register_api_key_scope_mapping ({ resource => action }, self )
end
2020-10-09 14:52:48 +01:00
# Register a new UserApiKey scope, and its allowed routes. Scope will be prefixed
2021-05-20 21:43:47 -04:00
# with the (parameterized) plugin name followed by a colon.
2020-10-09 14:52:48 +01:00
#
# For example, if discourse-awesome-plugin registered this:
#
# add_user_api_key_scope(:read_my_route,
# methods: :get,
# actions: "mycontroller#myaction",
# formats: :ics,
2021-07-25 18:49:24 +02:00
# params: :testparam
2020-10-09 14:52:48 +01:00
# )
#
# The scope registered would be `discourse-awesome-plugin:read_my_route`
#
# Multiple matchers can be attached by supplying an array of parameter hashes
#
# See UserApiKeyScope::SCOPES for more examples
# And lib/route_matcher.rb for the route matching logic
def add_user_api_key_scope ( scope_name , matcher_parameters )
raise ArgumentError . new ( "scope_name must be a symbol" ) if ! scope_name . is_a? ( Symbol )
matcher_parameters = [ matcher_parameters ] if ! matcher_parameters . is_a? ( Array )
prefixed_scope_name = :" #{ ( name || directory_name ) . parameterize } : #{ scope_name } "
DiscoursePluginRegistry . register_user_api_key_scope_mapping (
{ prefixed_scope_name => matcher_parameters &. map { | m | RouteMatcher . new ( ** m ) } },
self ,
)
end
2020-08-24 10:24:52 +01:00
# Register a route which can be authenticated using an api key or user api key
# in a query parameter rather than a header. For example:
#
# add_api_parameter_route(
2020-10-06 17:20:15 +01:00
# methods: :get,
# actions: "users#bookmarks",
# formats: :ics
2020-08-24 10:24:52 +01:00
# )
#
# See Auth::DefaultCurrentUserProvider::PARAMETER_API_PATTERNS for more examples
# and Auth::DefaultCurrentUserProvider#api_parameter_allowed? for implementation
2020-10-06 17:20:15 +01:00
def add_api_parameter_route (
method : nil ,
methods : nil ,
route : nil ,
actions : nil ,
format : nil ,
formats : nil
)
if Array ( format ) . include? ( "*" )
2021-12-06 09:10:14 -06:00
Discourse . deprecate (
"* is no longer a valid api_parameter_route format matcher. Use `nil` instead" ,
drop_from : "2.7" ,
raise_error : true ,
)
2020-10-06 17:20:15 +01:00
# Old API used * as wildcard. New api uses `nil`
format = nil
end
# Backwards compatibility with old parameter names:
if method || route || format
2021-12-06 09:10:14 -06:00
Discourse . deprecate (
"method, route and format parameters for api_parameter_routes are deprecated. Use methods, actions and formats instead." ,
drop_from : "2.7" ,
raise_error : true ,
)
2020-10-06 17:20:15 +01:00
methods ||= method
actions ||= route
formats ||= format
end
DiscoursePluginRegistry . register_api_parameter_route (
RouteMatcher . new ( methods : methods , actions : actions , formats : formats ),
self ,
)
2020-08-24 10:24:52 +01:00
end
2020-12-16 09:43:39 +00:00
# Register a new demon process to be forked by the Unicorn master.
# The demon_class should inherit from Demon::Base.
# With great power comes great responsibility - this method should
# be used with extreme caution. See `config/unicorn.conf.rb`.
def register_demon_process ( demon_class )
raise "Not a demon class" if ! demon_class . ancestors . include? ( Demon :: Base )
DiscoursePluginRegistry . demon_processes << demon_class
end
2021-03-02 10:28:27 -06:00
def add_permitted_reviewable_param ( type , param )
DiscoursePluginRegistry . register_reviewable_param ({ type : type , param : param }, self )
end
2021-08-27 14:43:39 +01:00
# Register a new PresenceChannel prefix. See {PresenceChannel.register_prefix}
# for usage instructions
def register_presence_channel_prefix ( prefix , & block )
DiscoursePluginRegistry . register_presence_channel_prefix ( [ prefix , block ] , self )
end
2021-11-03 12:21:33 -05:00
# Registers a new push notification filter. User and notification payload are passed into block, and if all
# filters return `true`, the push notification will be sent.
def register_push_notification_filter ( & block )
DiscoursePluginRegistry . register_push_notification_filter ( block , self )
end
2021-10-07 12:41:57 -03:00
# Register a ReviewableScore setting_name associated with a reason.
# We'll use this to build a site setting link and add it to the reason's translation.
#
# If your plugin has a reason translation looking like this:
#
# my_plugin_reason: "This is the reason this post was flagged. See %{link}."
#
# And you associate the reason with a setting:
#
# add_reviewable_score_link(:my_plugin_reason, 'a_plugin_setting')
#
# We'll generate the following link and attach it to the translation:
#
# <a href="/admin/site_settings/category/all_results?filter=a_plugin_setting">
# a plugin setting
# </a>
def add_reviewable_score_link ( reason , setting_name )
DiscoursePluginRegistry . register_reviewable_score_link (
{ reason : reason . to_sym , setting : setting_name },
self ,
)
end
2021-11-30 13:36:14 -03:00
# If your plugin creates notifications, and you'd like to consolidate/collapse similar ones,
# you're in the right place.
# This method receives a plan object, which must be an instance of `Notifications::ConsolidateNotifications`.
#
# Instead of using `Notification#create!`, you should use `Notification#consolidate_or_save!`,
# which will automatically pick your plan and apply it, updating an already consolidated notification,
# consolidating multiple ones, or creating a regular one.
#
# The rule object is quite complex. We strongly recommend you write tests to ensure your plugin consolidates notifications correctly.
#
2021-12-10 10:32:15 -03:00
# - Threshold and time window consolidation plan: https://github.com/discourse/discourse/blob/main/app/services/notifications/consolidate_notifications.rb
# - Create a new notification and delete previous versions plan: https://github.com/discourse/discourse/blob/main/app/services/notifications/delete_previous_notifications.rb
2021-11-30 13:36:14 -03:00
# - Base plans: https://github.com/discourse/discourse/blob/main/app/services/notifications/consolidation_planner.rb
def register_notification_consolidation_plan ( plan )
2021-12-10 10:32:15 -03:00
if ! plan . class . ancestors . include? ( Notifications :: ConsolidationPlan )
raise ArgumentError . new ( "Not a consolidation plan" )
2023-01-09 12:10:19 +00:00
end
2021-11-30 13:36:14 -03:00
DiscoursePluginRegistry . register_notification_consolidation_plan ( plan , self )
end
2021-12-16 04:24:11 +01:00
# Allows customizing existing topic-backed static pages, like:
# faq, tos, privacy (see: StaticController) The block passed to this
# method has to return a SiteSetting name that contains a topic id.
#
# add_topic_static_page("faq") do |controller|
# current_user&.locale == "pl" ? "polish_faq_topic_id" : "faq_topic_id"
# end
#
# You can also add new pages in a plugin, but remember to add a route,
# for example:
#
# get "contact" => "static#show", id: "contact"
def add_topic_static_page ( page , options = {}, & blk )
StaticController :: CUSTOM_PAGES [ page ] = blk ? { topic_id : blk } : options
end
2022-06-21 15:49:47 -03:00
# Let plugin define custom unsubscribe keys,
# set custom instance variables on the `EmailController#unsubscribe` action,
# and describe what unsubscribing for that key does.
#
# The method receives a class that inherits from `Email::BaseEmailUnsubscriber`.
# Take a look at it to know how to implement your child class.
#
# In conjunction with this, you'll have to:
#
# - Register a new connector under app/views/connectors/unsubscribe_options.
# We'll include the HTML inside the unsubscribe form, so you can add your fields using the
# instance variables you set in the controller previously. When the form is submitted,
# it sends the updated preferences to `EmailController#perform_unsubscribe`.
#
# - Your code is responsible for creating the custom key by calling `UnsubscribeKey#create_key_for`.
def register_email_unsubscriber ( type , unsubscriber )
core_types = [ UnsubscribeKey :: ALL_TYPE , UnsubscribeKey :: DIGEST_TYPE , UnsubscribeKey :: TOPIC_TYPE ]
raise ArgumentError . new ( "Type already exists" ) if core_types . include? ( type )
if ! unsubscriber . ancestors . include? ( EmailControllerHelper :: BaseEmailUnsubscriber )
raise ArgumentError . new ( "Not an email unsubscriber" )
2023-01-09 12:10:19 +00:00
end
2022-06-21 15:49:47 -03:00
DiscoursePluginRegistry . register_email_unsubscriber ({ type => unsubscriber }, self )
end
2022-07-15 13:16:00 +10:00
# Allows the plugin to export additional site stats via the About class
# which will be shown on the /about route. The stats returned by the block
# should be in the following format (these four keys are _required_):
#
# {
# last_day: 1,
# 7_days: 10,
# 30_days: 100,
# count: 1000
# }
#
# Only keys above will be shown on the /about page in the UI,
# but all stats will be shown on the /about.json route. For example take
# this usage:
#
# register_about_stat_group("chat_messages") do
# { last_day: 1, "7_days" => 10, "30_days" => 100, count: 1000, previous_30_days: 150 }
# end
#
# In the UI we will show a table like this:
#
# | 24h | 7 days | 30 days | all time|
# Chat Messages | 1 | 10 | 100 | 1000 |
#
# But the JSON will be like this:
#
# {
# "chat_messages_last_day": 1,
# "chat_messages_7_days": 10,
# "chat_messages_30_days": 100,
# "chat_messages_count": 1000,
# }
#
# The show_in_ui option (default false) is used to determine whether the
# group of stats is shown on the site About page in the Site Statistics
# table. Some stats may be needed purely for reporting purposes and thus
# do not need to be shown in the UI to admins/users.
def register_about_stat_group ( plugin_stat_group_name , show_in_ui : false , & block )
2023-03-02 08:10:16 +10:00
# We do not want to register and display the same group multiple times.
if DiscoursePluginRegistry . about_stat_groups . any? { | stat_group |
stat_group [ :name ] == plugin_stat_group_name
}
return
end
DiscoursePluginRegistry . register_about_stat_group (
{ name : plugin_stat_group_name , show_in_ui : show_in_ui , block : block },
self ,
)
2022-07-15 13:16:00 +10:00
end
2022-11-21 08:37:06 +10:00
##
# Used to register data sources for HashtagAutocompleteService to look
# up results based on a #hashtag string.
2022-10-19 14:03:57 +10:00
#
2022-11-21 08:37:06 +10:00
# @param {Class} klass - Must be a class that implements methods with the following
# signatures:
#
2022-12-19 13:46:17 +10:00
# Roughly corresponding to a model, this is used as a unique
# key for the datasource and is also used when allowing different
# contexts to search for and lookup these types. The `category`
# and `tag` types are registered by default.
# def self.type
# end
#
# The FontAwesome icon to use for the data source in the search results
# and cooked markdown.
# def self.icon
# end
#
2022-11-21 08:37:06 +10:00
# @param {Guardian} guardian - Current user's guardian, used for permission-based filtering
# @param {Array} slugs - An array of strings that represent slugs to search this type for,
# e.g. category slugs.
# @returns {Hash} A hash with the slug as the key and the URL of the record as the value.
# def self.lookup(guardian, slugs)
# end
#
# @param {Guardian} guardian - Current user's guardian, used for permission-based filtering
# @param {String} term - The search term used to filter results
# @param {Integer} limit - The number of search results that should be returned by the query
# @returns {Array} An Array of HashtagAutocompleteService::HashtagItem
# def self.search(guardian, term, limit)
# end
2022-12-19 13:46:17 +10:00
#
# @param {Array} search_results - An array of HashtagAutocompleteService::HashtagItem to sort
# @param {String} term - The search term which was used, which may help with sorting.
# @returns {Array} An Array of HashtagAutocompleteService::HashtagItem
# def self.search_sort(search_results, term)
# end
#
# @param {Guardian} guardian - Current user's guardian, used for permission-based filtering
# @param {Integer} limit - The number of search results that should be returned by the query
# @returns {Array} An Array of HashtagAutocompleteService::HashtagItem
# def self.search_without_term(guardian, limit)
# end
def register_hashtag_data_source ( klass )
DiscoursePluginRegistry . register_hashtag_autocomplete_data_source ( klass , self )
2022-11-21 08:37:06 +10:00
end
##
# Used to set up the priority ordering of hashtag autocomplete results by
# type using HashtagAutocompleteService.
#
# @param {String} type - Roughly corresponding to a model, can only be registered once
# per context. The `category` and `tag` types are registered
# for the `topic-composer` context by default in that priority order.
# @param {String} context - The context in which the hashtag lookup or search is happening
# in. For example, the Discourse composer context is `topic-composer`.
# Different contexts may want to have different priority orderings
# for certain types of hashtag result.
# @param {Integer} priority - A number value for ordering type results when hashtag searches
# or lookups occur. Priority is ordered by DESCENDING order.
2022-12-19 13:46:17 +10:00
def register_hashtag_type_priority_for_context ( type , context , priority )
DiscoursePluginRegistry . register_hashtag_autocomplete_contextual_type_priority (
{ type : type , context : context , priority : priority },
self ,
)
2022-10-19 14:03:57 +10:00
end
2022-11-28 13:32:57 -03:00
##
# Register a block that will be called when the UserDestroyer runs
# with the :delete_posts opt set to true. It's important to note that the block will
# execute before any other :delete_posts actions, it allows us to manipulate flags
# before agreeing with them. For example, discourse-akismet makes use of this
#
# @param {Block} callback to be called with the user, guardian, and the destroyer opts as arguments
def register_user_destroyer_on_content_deletion_callback ( callback )
DiscoursePluginRegistry . register_user_destroyer_on_content_deletion_callback ( callback , self )
end
2023-03-08 10:39:12 +10:00
##
# Register a class that implements [BaseBookmarkable], which represents another
# [ActiveRecord::Model] that may be bookmarked via the [Bookmark] model's
# polymorphic association. The class handles create and destroy hooks, querying,
# and reminders among other things.
def register_bookmarkable ( klass )
return if Bookmark . registered_bookmarkable_from_type ( klass . model . name ) . present?
DiscoursePluginRegistry . register_bookmarkable ( RegisteredBookmarkable . new ( klass ), self )
end
2014-04-10 16:30:22 +10:00
protected
2019-07-15 20:22:54 +05:30
def self . js_path
File . expand_path " #{ Rails . root } /app/assets/javascripts/plugins"
end
def js_file_path
2022-08-22 09:56:39 +01:00
" #{ Plugin :: Instance . js_path } / #{ directory_name } .js.erb"
end
def extra_js_file_path
@extra_js_file_path ||= " #{ Plugin :: Instance . js_path } / #{ directory_name } _extra.js.erb"
2019-07-15 20:22:54 +05:30
end
2014-04-10 16:30:22 +10:00
def register_assets!
2019-08-20 22:09:52 +05:30
assets . each do | asset , opts , plugin_directory_name |
DiscoursePluginRegistry . register_asset ( asset , opts , plugin_directory_name )
2014-04-10 16:30:22 +10:00
end
end
2017-11-22 17:02:01 -08:00
def register_service_workers!
service_workers . each do | asset , opts |
DiscoursePluginRegistry . register_service_worker ( asset , opts )
end
end
2018-01-25 12:09:18 +01:00
def register_locales!
root_path = File . dirname ( @path )
locales . each do | locale , opts |
opts = opts . dup
2020-11-11 09:44:01 -06:00
opts [ :client_locale_file ] = Dir [ " #{ root_path } /config/locales/client*. #{ locale } .yml" ]. first ||
""
opts [ :server_locale_file ] = Dir [ " #{ root_path } /config/locales/server*. #{ locale } .yml" ]. first ||
""
2018-01-25 12:09:18 +01:00
opts [ :js_locale_file ] = File . join ( root_path , "assets/locales/ #{ locale } .js.erb" )
locale_chain = opts [ :fallbackLocale ] ? [ locale , opts [ :fallbackLocale ]] : [ locale ]
lib_locale_path = File . join ( root_path , "lib/javascripts/locale" )
path = File . join ( lib_locale_path , "message_format" )
opts [ :message_format ] = find_locale_file ( locale_chain , path )
2019-02-19 14:42:58 +01:00
opts [ :message_format ] = JsLocaleHelper . find_message_format_locale (
locale_chain ,
fallback_to_english : false ,
) unless opts [ :message_format ]
2018-01-25 12:09:18 +01:00
path = File . join ( lib_locale_path , "moment_js" )
opts [ :moment_js ] = find_locale_file ( locale_chain , path )
opts [ :moment_js ] = JsLocaleHelper . find_moment_locale ( locale_chain ) unless opts [ :moment_js ]
2019-02-25 14:40:02 -05:00
path = File . join ( lib_locale_path , "moment_js_timezones" )
opts [ :moment_js_timezones ] = find_locale_file ( locale_chain , path )
opts [ :moment_js_timezones ] = JsLocaleHelper . find_moment_locale (
locale_chain ,
timezone_names : true ,
) unless opts [ :moment_js_timezones ]
2018-01-25 12:09:18 +01:00
if valid_locale? ( opts )
DiscoursePluginRegistry . register_locale ( locale , opts )
Rails . configuration . assets . precompile << "locales/ #{ locale } .js"
2018-04-20 15:29:03 -04:00
else
2018-06-22 10:20:20 -04:00
msg = "Invalid locale! #{ opts . inspect } "
# The logger isn't always present during boot / parsing locales from plugins
if Rails . logger . present?
Rails . logger . error ( msg )
else
puts msg
end
2018-01-25 12:09:18 +01:00
end
end
end
2019-08-29 11:56:46 -03:00
def allow_new_queued_post_payload_attribute ( attribute_name )
reloadable_patch { NewPostManager . add_plugin_payload_attribute ( attribute_name ) }
end
2022-12-12 09:08:13 -05:00
def register_topic_preloader_associations ( fields )
DiscoursePluginRegistry . register_topic_preloader_association ( fields , self )
end
2023-01-16 15:48:00 -03:00
def register_search_group_query_callback ( callback )
DiscoursePluginRegistry . register_search_groups_set_query_callback ( callback , self )
end
2015-11-06 15:02:40 +01:00
private
2021-06-22 13:00:04 -05:00
def validate_directory_column_name ( column_name )
2023-01-20 12:52:49 -06:00
match = /\A[_a-z]+\z/ . match ( column_name )
2021-06-22 13:00:04 -05:00
unless match
raise "Invalid directory column name ' #{ column_name } '. Can only contain a-z and underscores"
2023-01-09 12:10:19 +00:00
end
2021-06-22 13:00:04 -05:00
end
2015-11-06 15:02:40 +01:00
def write_asset ( path , contents )
2022-01-05 12:45:08 -05:00
unless File . exist? ( path )
2015-11-06 15:02:40 +01:00
ensure_directory ( path )
File . open ( path , "w" ) { | f | f . write ( contents ) }
end
end
2017-08-09 22:22:18 +02:00
def reloadable_patch ( plugin = self )
2017-08-31 12:06:56 +08:00
if Rails . env . development? && defined? ( ActiveSupport :: Reloader )
ActiveSupport :: Reloader . to_prepare do
2017-08-09 22:22:18 +02:00
# reload the patch
2017-08-09 12:28:32 -04:00
yield plugin
end
end
# apply the patch
yield plugin
end
2018-01-25 12:09:18 +01:00
def valid_locale? ( custom_locale )
File . exist? ( custom_locale [ :client_locale_file ] ) &&
File . exist? ( custom_locale [ :server_locale_file ] ) &&
File . exist? ( custom_locale [ :js_locale_file ] ) && custom_locale [ :message_format ] &&
custom_locale [ :moment_js ]
end
def find_locale_file ( locale_chain , path )
locale_chain . each do | locale |
filename = File . join ( path , " #{ locale } .js" )
return locale , filename if File . exist? ( filename )
end
nil
end
2020-09-10 21:48:45 +05:30
def register_permitted_bulk_action_parameter ( name )
DiscoursePluginRegistry . register_permitted_bulk_action_parameter ( name , self )
end
2013-08-23 16:21:52 +10:00
end