2019-05-03 06:17:27 +08:00
# frozen_string_literal: true
2013-08-23 14:21:52 +08:00
require 'digest/sha1'
require 'fileutils'
2022-03-21 22:28:52 +08:00
require 'plugin/metadata'
require 'auth'
2013-08-23 14:21:52 +08:00
2016-07-23 00:59:43 +08:00
class Plugin :: CustomEmoji
2020-03-31 02:16:10 +08:00
CACHE_KEY || = " plugin-emoji "
2016-07-23 00:59:43 +08:00
def self . cache_key
2020-03-31 02:16:10 +08:00
@@cache_key || = CACHE_KEY
2016-07-23 00:59:43 +08:00
end
def self . emojis
@@emojis || = { }
end
2020-03-31 02:16:10 +08:00
def self . clear_cache
@@cache_key = CACHE_KEY
@@emojis = { }
2020-05-28 02:11:52 +08:00
@@translations = { }
2016-07-23 00:59:43 +08:00
end
2019-01-04 22:14:16 +08:00
2020-03-31 02:16:10 +08: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 15:23:40 +08:00
end
2020-05-27 18:08:24 +08: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-23 00:59:43 +08:00
end
2013-08-23 14:21:52 +08:00
class Plugin :: Instance
2013-08-26 09:04:16 +08:00
attr_accessor :path , :metadata
2015-02-07 06:32:59 +08:00
attr_reader :admin_route
2013-08-23 14:21:52 +08:00
2015-02-05 01:59:18 +08:00
# Memoized array readers
2017-01-13 04:43:09 +08:00
[ :assets ,
:color_schemes ,
2018-12-01 00:58:18 +08:00
:before_auth_initializers ,
2017-01-13 04:43:09 +08:00
:initializers ,
:javascripts ,
2018-01-25 19:09:18 +08:00
:locales ,
2017-11-23 09:02:01 +08:00
:service_workers ,
2017-01-13 04:43:09 +08:00
:styles ,
2018-11-30 22:51:45 +08:00
:themes ,
:csp_extensions ,
2020-03-13 23:30:31 +08:00
:asset_filters
2018-11-30 22:51:45 +08:00
] . each do | att |
2015-02-05 01:59:18 +08:00
class_eval %Q{
def #{att}
@ #{att} ||= []
end
}
end
2016-07-23 00:59:43 +08:00
def seed_data
@seed_data || = HashWithIndifferentAccess . new ( { } )
2015-06-05 03:56:17 +08:00
end
2020-06-16 05:28:07 +08:00
def seed_fu_filter ( filter = nil )
@seed_fu_filter = filter
end
2013-08-23 14:21:52 +08:00
def self . find_all ( parent_path )
[ ] . tap { | plugins |
2013-10-21 17:18:24 +08:00
# also follows symlinks - http://stackoverflow.com/q/357754
2017-05-17 05:28:45 +08:00
Dir [ " #{ parent_path } /*/plugin.rb " ] . sort . each do | path |
2013-08-23 14:21:52 +08:00
source = File . read ( path )
metadata = Plugin :: Metadata . parse ( source )
plugins << self . new ( metadata , path )
end
}
end
2013-08-26 09:04:16 +08:00
def initialize ( metadata = nil , path = nil )
2013-08-23 14:21:52 +08:00
@metadata = metadata
@path = path
2015-08-21 23:28:17 +08:00
@idx = 0
2013-08-23 14:21:52 +08:00
end
2019-12-06 03:57:18 +08:00
def register_anonymous_cache_key ( key , & block )
key_method = " key_ #{ key } "
2019-12-11 22:07:22 +08:00
add_to_class ( Middleware :: AnonymousCache :: Helper , key_method , & block )
2019-12-06 03:57:18 +08:00
Middleware :: AnonymousCache . cache_key_segments [ key ] = key_method
Middleware :: AnonymousCache . compile_key_builder
end
2015-02-07 06:32:59 +08:00
def add_admin_route ( label , location )
@admin_route = { label : label , location : location }
end
2015-02-05 05:23:39 +08:00
def enabled?
2019-05-07 09:00:09 +08:00
@enabled_site_setting ? SiteSetting . get ( @enabled_site_setting ) : true
2013-08-26 09:04:16 +08:00
end
2015-02-05 05:23:39 +08:00
delegate :name , to : :metadata
2015-04-24 01:33:29 +08:00
def add_to_serializer ( serializer , attr , define_include_method = true , & block )
2017-08-10 04:22:18 +08:00
reloadable_patch do | plugin |
2019-08-27 16:21:53 +08:00
base = " #{ serializer . to_s . classify } Serializer " . constantize rescue " #{ serializer . to_s } Serializer " . constantize
2015-04-24 01:33:29 +08:00
2019-08-27 16:21:53 +08: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-24 01:33:29 +08:00
2019-08-27 16:21:53 +08: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-10 04:22:18 +08:00
end
2019-08-27 16:21:53 +08:00
klass . public_send ( :define_method , attr , & block )
end
2017-08-10 00:28:32 +08:00
end
2015-02-05 05:23:39 +08:00
end
2018-07-25 23:44:09 +08:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2018-06-19 21:00:11 +08:00
def add_report ( name , & block )
reloadable_patch do | plugin |
2018-07-25 23:44:09 +08:00
Report . add_report ( name , & block )
2018-06-19 21:00:11 +08:00
end
end
2018-07-25 23:44:09 +08:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2020-07-02 22:47:43 +08:00
def replace_flags ( settings : :: FlagSettings . new , score_type_names : [ ] )
2020-01-17 22:59:38 +08:00
next_flag_id = ReviewableScore . types . values . max + 1
2020-07-04 03:21:06 +08:00
yield ( settings , next_flag_id ) if block_given?
2017-10-18 01:31:45 +08:00
reloadable_patch do | plugin |
2018-07-25 23:44:09 +08:00
:: PostActionType . replace_flag_settings ( settings )
2020-01-17 22:59:38 +08:00
:: ReviewableScore . reload_types
2020-07-02 22:47:43 +08:00
:: ReviewableScore . add_new_types ( score_type_names )
2017-10-18 01:31:45 +08:00
end
end
2016-03-12 04:52:18 +08:00
def whitelist_staff_user_custom_field ( field )
2021-12-03 02:16:55 +08: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 08:23:54 +08:00
allow_staff_user_custom_field ( field )
end
def allow_staff_user_custom_field ( field )
2020-05-15 21:04:38 +08:00
DiscoursePluginRegistry . register_staff_user_custom_field ( field , self )
2016-03-12 04:52:18 +08:00
end
2018-10-17 17:33:27 +08:00
def whitelist_public_user_custom_field ( field )
2021-12-03 02:16:55 +08: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 08:23:54 +08:00
allow_public_user_custom_field ( field )
end
def allow_public_user_custom_field ( field )
2020-05-15 21:04:38 +08:00
DiscoursePluginRegistry . register_public_user_custom_field ( field , self )
2018-10-17 17:33:27 +08:00
end
2019-10-11 16:57:55 +08:00
def register_editable_user_custom_field ( field , staff_only : false )
2020-05-15 21:04:38 +08: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 18:45:36 +08:00
end
end
2019-06-06 10:05:33 +08:00
def register_editable_group_custom_field ( field )
2020-05-15 21:04:38 +08:00
DiscoursePluginRegistry . register_editable_group_custom_field ( field , self )
2019-06-06 10:05:33 +08:00
end
2020-08-07 10:47:00 +08: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 06:57:58 +08: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 18:16:08 +08: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 09:58:28 +08: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 12:56:13 +08: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 15:00:30 +08: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-31 02:24:03 +08:00
def custom_avatar_column ( column )
reloadable_patch do | plugin |
2020-07-17 17:48:08 +08:00
UserLookup . lookup_columns << column
UserLookup . lookup_columns . uniq!
2017-08-31 02:24:03 +08:00
end
end
2018-07-25 23:44:09 +08:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-09-29 01:16:51 +08:00
def add_body_class ( class_name )
reloadable_patch do | plugin |
2018-07-25 23:44:09 +08:00
:: ApplicationHelper . extra_body_classes << class_name
2017-09-29 01:16:51 +08:00
end
end
2017-10-03 00:04:59 +08:00
def rescue_from ( exception , & block )
reloadable_patch do | plugin |
:: ApplicationController . rescue_from ( exception , & block )
end
end
2015-02-05 05:23:39 +08:00
# Extend a class but check that the plugin is enabled
2015-08-21 23:28:17 +08:00
# for class methods use `add_class_method`
2017-08-10 00:28:32 +08:00
def add_to_class ( class_name , attr , & block )
2017-08-10 04:22:18 +08:00
reloadable_patch do | plugin |
2017-08-10 00:28:32 +08:00
klass = class_name . to_s . classify . constantize rescue class_name . to_s . constantize
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-10 00:28:32 +08:00
2019-05-07 09:27:05 +08:00
klass . public_send ( :define_method , attr ) do | * args |
public_send ( hidden_method_name , * args ) if plugin . enabled?
2017-08-10 00:28:32 +08:00
end
2015-02-05 05:23:39 +08:00
end
2015-01-12 23:52:55 +08:00
end
2015-08-21 23:28:17 +08:00
# Adds a class method to a class, respecting if plugin is enabled
2017-08-10 00:28:32 +08:00
def add_class_method ( klass_name , attr , & block )
2017-08-10 04:22:18 +08:00
reloadable_patch do | plugin |
2017-08-10 00:28:32 +08:00
klass = klass_name . to_s . classify . constantize rescue klass_name . to_s . constantize
2015-08-21 23:28:17 +08:00
2017-08-10 00:28:32 +08: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 23:28:17 +08:00
2019-05-07 09:27:05 +08:00
klass . public_send ( :define_singleton_method , attr ) do | * args |
public_send ( hidden_method_name , * args ) if plugin . enabled?
2017-08-10 00:28:32 +08:00
end
2015-08-21 23:28:17 +08:00
end
end
2017-08-10 00:28:32 +08:00
def add_model_callback ( klass_name , callback , options = { } , & block )
2017-08-10 04:22:18 +08:00
reloadable_patch do | plugin |
2017-08-10 00:28:32 +08:00
klass = klass_name . to_s . classify . constantize rescue klass_name . to_s . constantize
2015-08-21 23:28:17 +08:00
2017-08-10 00:28:32 +08: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 23:28:17 +08:00
2020-07-16 15:43:20 +08:00
klass . public_send ( callback , ** options ) do | * args |
2019-05-07 09:27:05 +08:00
public_send ( hidden_method_name , * args ) if plugin . enabled?
2017-08-10 00:28:32 +08:00
end
2015-08-21 23:28:17 +08:00
2017-08-10 00:28:32 +08:00
hidden_method_name
end
2015-08-21 23:28:17 +08:00
end
2017-08-12 10:21:02 +08:00
def topic_view_post_custom_fields_whitelister ( & block )
2021-12-03 02:16:55 +08: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 08:23:54 +08: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 10:21:02 +08: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 23:44:09 +08:00
end
2017-08-12 10:21:02 +08:00
end
end
2020-08-04 17:57:33 +08: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 23:44:09 +08:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-12 10:21:02 +08:00
def add_preloaded_group_custom_field ( field )
reloadable_patch do | plugin |
2018-07-25 23:44:09 +08:00
:: Group . preloaded_custom_field_names << field
2017-08-12 10:21:02 +08:00
end
end
2018-07-25 23:44:09 +08:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-12 10:21:02 +08:00
def add_preloaded_topic_list_custom_field ( field )
reloadable_patch do | plugin |
2018-07-25 23:44:09 +08:00
:: TopicList . preloaded_custom_fields << field
2017-08-12 10:21:02 +08:00
end
end
2018-07-25 23:44:09 +08:00
# Add a permitted_create_param to Post, respecting if the plugin is enabled
2019-12-21 00:37:12 +08:00
def add_permitted_post_create_param ( name , type = :string )
2017-08-12 10:21:02 +08:00
reloadable_patch do | plugin |
2019-12-21 00:37:12 +08:00
:: Post . plugin_permitted_create_params [ name ] = { plugin : plugin , type : type }
2017-08-12 10:21:02 +08:00
end
end
2021-03-24 23:22:16 +08: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 08:18:51 +08: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 07:38:45 +08: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 08:18:51 +08:00
end
2015-04-24 01:33:29 +08:00
# Add validation method but check that the plugin is enabled
2015-04-26 06:12:19 +08:00
def validate ( klass , name , & block )
2015-04-24 01:33:29 +08:00
klass = klass . to_s . classify . constantize
2019-05-07 09:27:05 +08:00
klass . public_send ( :define_method , name , & block )
2015-04-24 01:33:29 +08:00
plugin = self
2015-04-26 06:12:19 +08:00
klass . validate ( name , if : - > { plugin . enabled? } )
2015-04-24 01:33:29 +08:00
end
2013-08-23 14:21:52 +08:00
# will make sure all the assets this plugin needs are registered
def generate_automatic_assets!
paths = [ ]
2015-11-06 22:02:40 +08:00
assets = [ ]
2013-08-23 14:21:52 +08:00
automatic_assets . each do | path , contents |
2015-11-06 22:02:40 +08:00
write_asset ( path , contents )
paths << path
2019-09-17 00:06:34 +08:00
assets << [ path , nil , directory_name ]
2015-11-06 22:02:40 +08:00
end
2013-08-23 14:21:52 +08:00
delete_extra_automatic_assets ( paths )
2015-11-06 22:02:40 +08:00
assets
2013-08-23 14:21:52 +08:00
end
2021-06-23 02:00:04 +08: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 14:21:52 +08:00
def delete_extra_automatic_assets ( good_paths )
2022-01-06 01:45:08 +08:00
return unless Dir . exist? auto_generated_path
2013-09-21 05:39:14 +08:00
2013-08-23 14:21:52 +08:00
filenames = good_paths . map { | f | File . basename ( f ) }
# nuke old files
Dir . foreach ( auto_generated_path ) do | p |
next if [ " . " , " .. " ] . include? ( p )
next if filenames . include? ( p )
File . delete ( auto_generated_path + " / #{ p } " )
end
end
def ensure_directory ( path )
dirname = File . dirname ( path )
unless File . directory? ( dirname )
FileUtils . mkdir_p ( dirname )
end
end
2017-01-13 04:43:09 +08:00
def directory
File . dirname ( path )
end
2013-08-23 14:21:52 +08:00
def auto_generated_path
File . dirname ( path ) << " /auto_generated "
end
2013-09-17 08:23:21 +08:00
def after_initialize ( & block )
2015-02-05 01:59:18 +08:00
initializers << block
2013-09-17 08:23:21 +08:00
end
2018-12-01 00:58:18 +08:00
def before_auth ( & block )
raise " Auth providers must be registered before omniauth middleware. after_initialize is too late! " if @before_auth_complete
before_auth_initializers << block
end
2015-02-05 05:23:39 +08:00
# A proxy to `DiscourseEvent.on` which does nothing if the plugin is disabled
def on ( event_name , & block )
DiscourseEvent . on ( event_name ) do | * args |
block . call ( * args ) if enabled?
end
end
2013-09-17 08:23:21 +08:00
def notify_after_initialize
2014-06-04 00:36:34 +08:00
color_schemes . each do | c |
2017-08-12 10:21:02 +08:00
unless ColorScheme . where ( name : c [ :name ] ) . exists?
ColorScheme . create_from_base ( name : c [ :name ] , colors : c [ :colors ] )
end
2014-06-04 00:36:34 +08:00
end
2015-02-05 01:59:18 +08:00
initializers . each do | callback |
2015-08-26 04:38:25 +08:00
begin
callback . call ( self )
rescue ActiveRecord :: StatementInvalid = > e
2017-08-12 10:21:02 +08:00
# When running `db:migrate` for the first time on a new database,
# plugin initializers might try to use models.
# Tolerate it.
2015-08-26 04:38:25 +08:00
raise e unless e . message . try ( :include? , " PG::UndefinedTable " )
end
2013-09-17 08:23:21 +08:00
end
end
2018-12-01 00:58:18 +08:00
def notify_before_auth
before_auth_initializers . each do | callback |
callback . call ( self )
end
@before_auth_complete = true
end
2018-07-25 23:44:09 +08:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-17 14:59:31 +08:00
def register_category_custom_field_type ( name , type )
reloadable_patch do | plugin |
2018-07-25 23:44:09 +08:00
Category . register_custom_field_type ( name , type )
2017-08-17 14:59:31 +08:00
end
end
2018-07-25 23:44:09 +08:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-12 10:21:02 +08:00
def register_topic_custom_field_type ( name , type )
reloadable_patch do | plugin |
2018-07-25 23:44:09 +08:00
:: Topic . register_custom_field_type ( name , type )
2017-08-12 10:21:02 +08:00
end
end
2018-07-25 23:44:09 +08:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-12 10:21:02 +08:00
def register_post_custom_field_type ( name , type )
reloadable_patch do | plugin |
2018-07-25 23:44:09 +08:00
:: Post . register_custom_field_type ( name , type )
2017-08-12 10:21:02 +08:00
end
end
2018-07-25 23:44:09 +08:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
2017-08-12 10:21:02 +08:00
def register_group_custom_field_type ( name , type )
reloadable_patch do | plugin |
2018-07-25 23:44:09 +08:00
:: Group . register_custom_field_type ( name , type )
2017-08-12 10:21:02 +08:00
end
end
2019-07-25 00:38:44 +08:00
# Applies to all sites in a multisite environment. Ignores plugin.enabled?
def register_user_custom_field_type ( name , type )
reloadable_patch do | plugin |
:: User . register_custom_field_type ( name , type )
end
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-16 05:28:07 +08:00
def register_seedfu_filter ( filter = nil )
DiscoursePluginRegistry . register_seedfu_filter ( filter )
end
2014-12-12 00:08:47 +08: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 14:21:52 +08:00
def register_css ( style )
2015-02-05 01:59:18 +08:00
styles << style
2013-08-23 14:21:52 +08:00
end
def register_javascript ( js )
2015-02-05 01:59:18 +08:00
javascripts << js
2013-08-23 14:21:52 +08:00
end
2018-11-27 05:49:57 +08:00
def register_svg_icon ( icon )
DiscoursePluginRegistry . register_svg_icon ( icon )
end
2018-11-30 22:51:45 +08:00
def extend_content_security_policy ( extension )
csp_extensions << extension
end
2020-03-13 23:30:31 +08: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 19:09:18 +08: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 09:39:33 +08:00
def register_custom_html ( hash )
DiscoursePluginRegistry . custom_html . merge! ( hash )
end
2017-04-18 03:47:21 +08:00
def register_html_builder ( name , & block )
2019-12-05 01:26:23 +08:00
plugin = self
DiscoursePluginRegistry . register_html_builder ( name ) do | * args |
block . call ( * args ) if plugin . enabled?
end
2017-04-18 03:47:21 +08:00
end
2014-04-07 22:33:35 +08:00
def register_asset ( file , opts = nil )
2022-11-25 03:03:06 +08:00
if file . end_with? ( " .hbs " , " .handlebars " )
raise << ~ ERROR
[ #{name}] Handlebars templates can no longer be included via `register_asset`.
Any hbs files under ` assets/javascripts ` will be automatically compiled and included . "
ERROR
end
2018-04-10 14:37:16 +08: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-21 00:39:52 +08:00
assets << [ full_path , opts , directory_name ]
2013-08-23 14:21:52 +08:00
end
2017-11-23 09:02:01 +08:00
def register_service_worker ( file , opts = nil )
service_workers << [
File . join ( File . dirname ( path ) , 'assets' , file ) ,
opts
]
end
2014-06-04 00:36:34 +08:00
def register_color_scheme ( name , colors )
color_schemes << { name : name , colors : colors }
2015-06-05 03:56:17 +08:00
end
def register_seed_data ( key , value )
seed_data [ key ] = value
end
2014-06-04 00:36:34 +08:00
2017-11-17 03:42:38 +08:00
def register_seed_path_builder ( & block )
DiscoursePluginRegistry . register_seed_path_builder ( & block )
end
2020-03-31 02:16:10 +08:00
def register_emoji ( name , url , group = Emoji :: DEFAULT_GROUP )
Plugin :: CustomEmoji . register ( name , url , group )
Emoji . clear_cache
2015-11-06 00:25:26 +08:00
end
2020-05-27 18:08:24 +08:00
def translate_emoji ( from , to )
Plugin :: CustomEmoji . translate ( from , to )
end
2013-08-23 14:21:52 +08:00
def automatic_assets
2015-02-05 01:59:18 +08:00
css = styles . join ( " \n " )
js = javascripts . join ( " \n " )
2013-08-23 14:21:52 +08:00
2015-11-06 22:02:40 +08: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-21 09:43:47 +08:00
# note, we need to be able to parse separately to activation.
2013-08-23 14:21:52 +08:00
# this allows us to present information about a plugin in the UI
# prior to activations
def activate!
2015-04-28 01:06:53 +08:00
if @path
2020-04-14 03:05:46 +08:00
root_dir_name = File . dirname ( @path )
2015-04-28 01:06:53 +08:00
# Automatically include all ES6 JS and hbs files
2020-04-14 03:05:46 +08:00
root_path = " #{ root_dir_name } /assets/javascripts "
2022-06-22 04:07:10 +08:00
DiscoursePluginRegistry . register_glob ( root_path , 'js' )
2015-04-28 01:06:53 +08:00
DiscoursePluginRegistry . register_glob ( root_path , 'js.es6' )
DiscoursePluginRegistry . register_glob ( root_path , 'hbs' )
2020-02-12 03:38:12 +08:00
DiscoursePluginRegistry . register_glob ( root_path , 'hbr' )
2015-08-18 03:03:55 +08:00
2020-04-14 03:05:46 +08:00
admin_path = " #{ root_dir_name } /admin/assets/javascripts "
2022-06-22 04:07:10 +08:00
DiscoursePluginRegistry . register_glob ( admin_path , 'js' , admin : true )
2015-08-18 03:03:55 +08:00
DiscoursePluginRegistry . register_glob ( admin_path , 'js.es6' , admin : true )
DiscoursePluginRegistry . register_glob ( admin_path , 'hbs' , admin : true )
2020-02-12 03:38:12 +08:00
DiscoursePluginRegistry . register_glob ( admin_path , 'hbr' , admin : true )
2020-04-14 03:05:46 +08:00
2022-06-22 04:07:10 +08:00
DiscourseJsProcessor . plugin_transpile_paths << root_path . sub ( Rails . root . to_s , '' ) . sub ( / ^ \/ * / , '' )
DiscourseJsProcessor . plugin_transpile_paths << admin_path . sub ( Rails . root . to_s , '' ) . sub ( / ^ \/ * / , '' )
2020-06-17 02:30:25 +08:00
2022-06-22 04:07:10 +08:00
test_path = " #{ root_dir_name } /test/javascripts "
DiscourseJsProcessor . plugin_transpile_paths << test_path . sub ( Rails . root . to_s , '' ) . sub ( / ^ \/ * / , '' )
2015-04-28 01:06:53 +08:00
end
2013-09-12 09:27:13 +08:00
self . instance_eval File . read ( path ) , path
2013-08-23 14:21:52 +08:00
if auto_assets = generate_automatic_assets!
2015-11-06 22:02:40 +08:00
assets . concat ( auto_assets )
2013-08-23 14:21:52 +08:00
end
2014-12-31 05:29:28 +08:00
register_assets! unless assets . blank?
2018-01-25 19:09:18 +08:00
register_locales!
2017-11-23 09:02:01 +08:00
register_service_workers!
2015-06-05 03:56:17 +08:00
seed_data . each do | key , value |
DiscoursePluginRegistry . register_seed_data ( key , value )
end
2015-05-04 22:01:57 +08:00
# TODO: possibly amend this to a rails engine
# Automatically include assets
2014-12-31 05:29:28 +08:00
Rails . configuration . assets . paths << auto_generated_path
Rails . configuration . assets . paths << File . dirname ( path ) + " /assets "
2015-08-18 03:03:55 +08:00
Rails . configuration . assets . paths << File . dirname ( path ) + " /admin/assets "
2015-08-28 04:59:36 +08:00
Rails . configuration . assets . paths << File . dirname ( path ) + " /test/javascripts "
2013-08-23 14:21:52 +08:00
2015-05-04 22:01:57 +08:00
# Automatically include rake tasks
Rake . add_rakelib ( File . dirname ( path ) + " /lib/tasks " )
# Automatically include migrations
2019-12-17 03:11:55 +08: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 22:01:57 +08:00
2013-11-20 11:38:21 +08:00
public_data = File . dirname ( path ) + " /public "
2022-01-06 01:45:08 +08:00
if Dir . exist? ( public_data )
2013-11-20 11:38:21 +08: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-18 07:35:52 +08:00
target << name . gsub ( / \ s / , " _ " )
2020-03-05 01:28:26 +08:00
Discourse :: Utils . atomic_ln_s ( public_data , target )
2013-11-20 11:38:21 +08:00
end
2019-07-15 22:52:54 +08:00
2020-03-05 01:28:26 +08:00
ensure_directory ( js_file_path )
2019-07-15 22:52:54 +08:00
contents = [ ]
handlebars_includes . each { | hb | contents << " require_asset(' #{ hb } ') " }
javascript_includes . each { | js | contents << " require_asset(' #{ js } ') " }
2022-08-22 16:56:39 +08:00
if ! contents . present?
[ js_file_path , extra_js_file_path ] . each do | f |
File . delete ( f )
2020-03-05 01:28:26 +08:00
rescue Errno :: ENOENT
end
2022-08-22 16:56:39 +08:00
return
end
contents . insert ( 0 , " <% " )
contents << " %> "
2022-09-21 19:38:02 +08:00
Discourse :: Utils . atomic_write_file ( extra_js_file_path , contents . join ( " \n " ) )
2022-08-22 16:56:39 +08:00
begin
2022-09-21 19:38:02 +08:00
File . delete ( js_file_path )
2022-08-22 16:56:39 +08:00
rescue Errno :: ENOENT
2019-07-15 22:52:54 +08:00
end
2013-08-23 14:21:52 +08:00
end
2013-08-26 10:52:36 +08:00
def auth_provider ( opts )
2018-12-01 00:58:18 +08:00
before_auth do
provider = Auth :: AuthProvider . new
2015-09-25 23:29:05 +08:00
2018-12-01 00:58:18 +08: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-12-01 00:58:18 +08:00
end
2018-07-23 23:51:57 +08:00
begin
provider . authenticator . enabled?
rescue NotImplementedError
provider . authenticator . define_singleton_method ( :enabled? ) do
2021-11-12 22:52:59 +08:00
Discourse . deprecate ( " #{ provider . authenticator . class . name } should define an `enabled?` function. Patching for now. " , drop_from : '2.9.0' )
2019-05-07 09:00:09 +08:00
return SiteSetting . get ( provider . enabled_setting ) if provider . enabled_setting
2021-11-12 22:52:59 +08:00
Discourse . deprecate ( " #{ provider . authenticator . class . name } has not defined an enabled_setting. Defaulting to true. " , drop_from : '2.9.0' )
2018-07-23 23:51:57 +08:00
true
end
end
2018-12-01 00:58:18 +08:00
DiscoursePluginRegistry . register_auth_provider ( provider )
end
2013-08-23 14:21:52 +08: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-10 06:10:14 +08:00
PluginGem . load ( path , name , version , opts )
2013-08-23 14:21:52 +08: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 20:05:23 +08:00
STDERR . puts ( " `enabled_site_setting_filter` is deprecated " )
2018-05-08 10:30:33 +08:00
end
2015-07-03 00:45:17 +08:00
def enabled_site_setting ( setting = nil )
if setting
@enabled_site_setting = setting
else
@enabled_site_setting
end
2015-02-05 05:23:39 +08:00
end
2016-11-15 08:42:55 +08: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 14:37:16 +08:00
next if opts == :vendored_core_pretty_text
2016-11-15 08:42:55 +08: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 23:10:34 +08:00
admin_path = " #{ File . dirname ( @path ) } /admin/assets/javascripts "
2016-11-15 08:42:55 +08:00
2021-06-16 23:04:21 +08:00
Dir . glob ( [ " #{ root_path } /**/* " , " #{ admin_path } /**/* " ] ) . sort . each do | f |
2020-04-14 03:05:46 +08:00
f_str = f . to_s
2016-11-15 08:42:55 +08:00
if File . directory? ( f )
yield [ f , true ]
2021-03-25 01:51:21 +08:00
elsif f_str . end_with? ( " .js.es6 " ) || f_str . end_with? ( " .hbs " ) || f_str . end_with? ( " .hbr " )
2020-04-14 03:05:46 +08:00
yield [ f , false ]
2022-06-22 04:07:10 +08:00
elsif f_str . end_with? ( " .js " )
2016-11-15 08:42:55 +08:00
yield [ f , false ]
end
end
end
end
2019-01-04 01:03:01 +08:00
def register_reviewable_type ( reviewable_type_class )
2019-04-09 01:42:36 +08: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-09 01:42:36 +08:00
current_list . concat ( new_attributes )
2019-01-04 01:03:01 +08:00
reloadable_patch do
2019-05-07 09:27:05 +08:00
klass . public_send ( :define_singleton_method , method ) { current_list }
2019-01-04 01:03:01 +08:00
end
end
2019-08-21 00:39:52 +08:00
def directory_name
@directory_name || = File . dirname ( path ) . split ( " / " ) . last
end
2019-08-22 11:09:10 +08:00
def css_asset_exists? ( target = nil )
DiscoursePluginRegistry . stylesheets_exists? ( directory_name , target )
2019-07-15 22:52:54 +08:00
end
def js_asset_exists?
2022-09-21 19:38:02 +08:00
# If assets/javascripts exists, ember-cli will output a .js file
File . exist? ( " #{ File . dirname ( @path ) } /assets/javascripts " )
2022-08-22 16:56:39 +08:00
end
def extra_js_asset_exists?
2022-09-21 19:38:02 +08:00
File . exist? ( extra_js_file_path )
2019-07-15 22:52:54 +08:00
end
2022-08-25 18:36:02 +08:00
def admin_js_asset_exists?
2022-09-21 19:38:02 +08:00
# If this directory exists, ember-cli will output a .js file
File . exist? ( " #{ File . dirname ( @path ) } /admin/assets/javascripts " )
2022-08-25 18:36:02 +08:00
end
2019-11-23 03:33:10 +08: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 do
Reviewable . add_custom_filter ( filter )
end
end
2021-02-18 01:42:44 +08: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-17 02:51:24 +08:00
def add_api_key_scope ( resource , action )
DiscoursePluginRegistry . register_api_key_scope_mapping ( { resource = > action } , self )
end
2020-10-09 21:52:48 +08:00
# Register a new UserApiKey scope, and its allowed routes. Scope will be prefixed
2021-05-21 09:43:47 +08:00
# with the (parameterized) plugin name followed by a colon.
2020-10-09 21:52:48 +08: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-26 00:49:24 +08:00
# params: :testparam
2020-10-09 21:52:48 +08: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 17:24:52 +08: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-07 00:20:15 +08:00
# methods: :get,
# actions: "users#bookmarks",
# formats: :ics
2020-08-24 17:24:52 +08:00
# )
#
# See Auth::DefaultCurrentUserProvider::PARAMETER_API_PATTERNS for more examples
# and Auth::DefaultCurrentUserProvider#api_parameter_allowed? for implementation
2020-10-07 00:20:15 +08: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 23:10:14 +08: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-07 00:20:15 +08: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 23:10:14 +08: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-07 00:20:15 +08: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 17:24:52 +08:00
end
2020-12-16 17:43:39 +08: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-03 00:28:27 +08:00
def add_permitted_reviewable_param ( type , param )
DiscoursePluginRegistry . register_reviewable_param ( {
type : type ,
param : param
} , self )
end
DEV: Introduce PresenceChannel API for core and plugin use
PresenceChannel aims to be a generic system for allow the server, and end-users, to track the number and identity of users performing a specific task on the site. For example, it might be used to track who is currently 'replying' to a specific topic, editing a specific wiki post, etc.
A few key pieces of information about the system:
- PresenceChannels are identified by a name of the format `/prefix/blah`, where `prefix` has been configured by some core/plugin implementation, and `blah` can be any string the implementation wants to use.
- Presence is a boolean thing - each user is either present, or not present. If a user has multiple clients 'present' in a channel, they will be deduplicated so that the user is only counted once
- Developers can configure the existence and configuration of channels 'just in time' using a callback. The result of this is cached for 2 minutes.
- Configuration of a channel can specify permissions in a similar way to MessageBus (public boolean, a list of allowed_user_ids, and a list of allowed_group_ids). A channel can also be placed in 'count_only' mode, where the identity of present users is not revealed to end-users.
- The backend implementation uses redis lua scripts, and is designed to scale well. In the future, hard limits may be introduced on the maximum number of users that can be present in a channel.
- Clients can enter/leave at will. If a client has not marked itself 'present' in the last 60 seconds, they will automatically 'leave' the channel. The JS implementation takes care of this regular check-in.
- On the client-side, PresenceChannel instances can be fetched from the `presence` ember service. Each PresenceChannel can be used entered/left/subscribed/unsubscribed, and the service will automatically deduplicate information before interacting with the server.
- When a client joins a PresenceChannel, the JS implementation will automatically make a GET request for the current channel state. To avoid this, the channel state can be serialized into one of your existing endpoints, and then passed to the `subscribe` method on the channel.
- The PresenceChannel JS object is an ember object. The `users` and `count` property can be used directly in ember templates, and in computed properties.
- It is important to make sure that you `unsubscribe()` and `leave()` any PresenceChannel objects after use
An example implementation may look something like this. On the server:
```ruby
register_presence_channel_prefix("site") do |channel|
next nil unless channel == "/site/online"
PresenceChannel::Config.new(public: true)
end
```
And on the client, a component could be implemented like this:
```javascript
import Component from "@ember/component";
import { inject as service } from "@ember/service";
export default Component.extend({
presence: service(),
init() {
this._super(...arguments);
this.set("presenceChannel", this.presence.getChannel("/site/online"));
},
didInsertElement() {
this.presenceChannel.enter();
this.presenceChannel.subscribe();
},
willDestroyElement() {
this.presenceChannel.leave();
this.presenceChannel.unsubscribe();
},
});
```
With this template:
```handlebars
Online: {{presenceChannel.count}}
<ul>
{{#each presenceChannel.users as |user|}}
<li>{{avatar user imageSize="tiny"}} {{user.username}}</li>
{{/each}}
</ul>
```
2021-08-27 21:43:39 +08: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-04 01:21:33 +08: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 23:41:57 +08: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
REFACTOR: Improve support for consolidating notifications. (#14904)
* REFACTOR: Improve support for consolidating notifications.
Before this commit, we didn't have a single way of consolidating notifications. For notifications like group summaries, we manually removed old ones before creating a new one. On the other hand, we used an after_create callback for likes and group membership requests, which caused unnecessary work, as we need to delete the record we created to replace it with a consolidated one.
We now have all the consolidation rules centralized in a single place: the consolidation planner class. Other parts of the app looking to create a consolidable notification can do so by calling Notification#consolidate_or_save!, instead of the default Notification#create! method.
Finally, we added two more rules: one for re-using existing group summaries and another for deleting duplicated dashboard problems PMs notifications when the user is tracking the moderator's inbox. Setting the threshold to one forces the planner to apply this rule every time.
I plan to add plugin support for adding custom rules in another PR to keep this one relatively small.
* DEV: Introduces a plugin API for consolidating notifications.
This commit removes the `Notification#filter_by_consolidation_data` scope since plugins could have to define their criteria. The Plan class now receives two blocks, one to query for an already consolidated notification, which we'll try to update, and another to query for existing ones to consolidate.
It also receives a consolidation window, which accepts an ActiveSupport::Duration object, and filter notifications created since that value.
2021-12-01 00:36:14 +08: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 21:32:15 +08: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
REFACTOR: Improve support for consolidating notifications. (#14904)
* REFACTOR: Improve support for consolidating notifications.
Before this commit, we didn't have a single way of consolidating notifications. For notifications like group summaries, we manually removed old ones before creating a new one. On the other hand, we used an after_create callback for likes and group membership requests, which caused unnecessary work, as we need to delete the record we created to replace it with a consolidated one.
We now have all the consolidation rules centralized in a single place: the consolidation planner class. Other parts of the app looking to create a consolidable notification can do so by calling Notification#consolidate_or_save!, instead of the default Notification#create! method.
Finally, we added two more rules: one for re-using existing group summaries and another for deleting duplicated dashboard problems PMs notifications when the user is tracking the moderator's inbox. Setting the threshold to one forces the planner to apply this rule every time.
I plan to add plugin support for adding custom rules in another PR to keep this one relatively small.
* DEV: Introduces a plugin API for consolidating notifications.
This commit removes the `Notification#filter_by_consolidation_data` scope since plugins could have to define their criteria. The Plan class now receives two blocks, one to query for an already consolidated notification, which we'll try to update, and another to query for existing ones to consolidate.
It also receives a consolidation window, which accepts an ActiveSupport::Duration object, and filter notifications created since that value.
2021-12-01 00:36:14 +08: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 21:32:15 +08:00
raise ArgumentError . new ( " Not a consolidation plan " ) if ! plan . class . ancestors . include? ( Notifications :: ConsolidationPlan )
REFACTOR: Improve support for consolidating notifications. (#14904)
* REFACTOR: Improve support for consolidating notifications.
Before this commit, we didn't have a single way of consolidating notifications. For notifications like group summaries, we manually removed old ones before creating a new one. On the other hand, we used an after_create callback for likes and group membership requests, which caused unnecessary work, as we need to delete the record we created to replace it with a consolidated one.
We now have all the consolidation rules centralized in a single place: the consolidation planner class. Other parts of the app looking to create a consolidable notification can do so by calling Notification#consolidate_or_save!, instead of the default Notification#create! method.
Finally, we added two more rules: one for re-using existing group summaries and another for deleting duplicated dashboard problems PMs notifications when the user is tracking the moderator's inbox. Setting the threshold to one forces the planner to apply this rule every time.
I plan to add plugin support for adding custom rules in another PR to keep this one relatively small.
* DEV: Introduces a plugin API for consolidating notifications.
This commit removes the `Notification#filter_by_consolidation_data` scope since plugins could have to define their criteria. The Plan class now receives two blocks, one to query for an already consolidated notification, which we'll try to update, and another to query for existing ones to consolidate.
It also receives a consolidation window, which accepts an ActiveSupport::Duration object, and filter notifications created since that value.
2021-12-01 00:36:14 +08:00
DiscoursePluginRegistry . register_notification_consolidation_plan ( plan , self )
end
2021-12-16 11:24:11 +08: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-22 02:49:47 +08: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 )
raise ArgumentError . new ( 'Not an email unsubscriber' ) if ! unsubscriber . ancestors . include? ( EmailControllerHelper :: BaseEmailUnsubscriber )
DiscoursePluginRegistry . register_email_unsubscriber ( { type = > unsubscriber } , self )
end
FEATURE: Add plugin API to register About stat group (#17442)
This commit introduces a new plugin API to register
a group of stats that will be included in about.json
and also conditionally in the site about UI at /about.
The usage is like this:
```ruby
register_about_stat_group("chat_messages", show_in_ui: true) do
{
last_day: 1,
"7_days" => 10,
"30_days" => 100,
count: 1000,
previous_30_days: 120
}
end
```
In reality the stats will be generated any way the implementer
chooses within the plugin. The `last_day`, `7_days`, `30_days,` and `count`
keys must be present but apart from that additional stats may be added.
Only those core 4 stat keys will be shown in the UI, but everything will be shown
in about.json.
The stat group name is used to prefix the stats in about.json like so:
```json
"chat_messages_last_day": 2322,
"chat_messages_7_days": 2322,
"chat_messages_30_days": 2322,
"chat_messages_count": 2322,
```
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. An extension to the Site
serializer, `displayed_about_plugin_stat_groups`, has been added so this
can be inspected on the client-side.
2022-07-15 11:16:00 +08: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 )
About . add_plugin_stat_group ( plugin_stat_group_name , show_in_ui : show_in_ui , & block )
end
FEATURE: Generic hashtag autocomplete lookup and markdown cooking (#18937)
This commit fleshes out and adds functionality for the new `#hashtag` search and
lookup system, still hidden behind the `enable_experimental_hashtag_autocomplete`
feature flag.
**Serverside**
We have two plugin API registration methods that are used to define data sources
(`register_hashtag_data_source`) and hashtag result type priorities depending on
the context (`register_hashtag_type_in_context`). Reading the comments in plugin.rb
should make it clear what these are doing. Reading the `HashtagAutocompleteService`
in full will likely help a lot as well.
Each data source is responsible for providing its own **lookup** and **search**
method that returns hashtag results based on the arguments provided. For example,
the category hashtag data source has to take into account parent categories and
how they relate, and each data source has to define their own icon to use for the
hashtag, and so on.
The `Site` serializer has two new attributes that source data from `HashtagAutocompleteService`.
There is `hashtag_icons` that is just a simple array of all the different icons that
can be used for allowlisting in our markdown pipeline, and there is `hashtag_context_configurations`
that is used to store the type priority orders for each registered context.
When sending emails, we cannot render the SVG icons for hashtags, so
we need to change the HTML hashtags to the normal `#hashtag` text.
**Markdown**
The `hashtag-autocomplete.js` file is where I have added the new `hashtag-autocomplete`
markdown rule, and like all of our rules this is used to cook the raw text on both the clientside
and on the serverside using MiniRacer. Only on the server side do we actually reach out to
the database with the `hashtagLookup` function, on the clientside we just render a plainer
version of the hashtag HTML. Only in the composer preview do we do further lookups based
on this.
This rule is the first one (that I can find) that uses the `currentUser` based on a passed
in `user_id` for guardian checks in markdown rendering code. This is the `last_editor_id`
for both the post and chat message. In some cases we need to cook without a user present,
so the `Discourse.system_user` is used in this case.
**Chat Channels**
This also contains the changes required for chat so that chat channels can be used
as a data source for hashtag searches and lookups. This data source will only be
used when `enable_experimental_hashtag_autocomplete` is `true`, so we don't have
to worry about channel results suddenly turning up.
------
**Known Rough Edges**
- Onebox excerpts will not render the icon svg/use tags, I plan to address that in a follow up PR
- Selecting a hashtag + pressing the Quote button will result in weird behaviour, I plan to address that in a follow up PR
- Mixed hashtag contexts for hashtags without a type suffix will not work correctly, e.g. #ux which is both a category and a channel slug will resolve to a category when used inside a post or within a [chat] transcript in that post. Users can get around this manually by adding the correct suffix, for example ::channel. We may get to this at some point in future
- Icons will not show for the hashtags in emails since SVG support is so terrible in email (this is not likely to be resolved, but still noting for posterity)
- Additional refinements and review fixes wil
2022-11-21 06:37:06 +08:00
##
# Used to register data sources for HashtagAutocompleteService to look
# up results based on a #hashtag string.
2022-10-19 12:03:57 +08:00
#
FEATURE: Generic hashtag autocomplete lookup and markdown cooking (#18937)
This commit fleshes out and adds functionality for the new `#hashtag` search and
lookup system, still hidden behind the `enable_experimental_hashtag_autocomplete`
feature flag.
**Serverside**
We have two plugin API registration methods that are used to define data sources
(`register_hashtag_data_source`) and hashtag result type priorities depending on
the context (`register_hashtag_type_in_context`). Reading the comments in plugin.rb
should make it clear what these are doing. Reading the `HashtagAutocompleteService`
in full will likely help a lot as well.
Each data source is responsible for providing its own **lookup** and **search**
method that returns hashtag results based on the arguments provided. For example,
the category hashtag data source has to take into account parent categories and
how they relate, and each data source has to define their own icon to use for the
hashtag, and so on.
The `Site` serializer has two new attributes that source data from `HashtagAutocompleteService`.
There is `hashtag_icons` that is just a simple array of all the different icons that
can be used for allowlisting in our markdown pipeline, and there is `hashtag_context_configurations`
that is used to store the type priority orders for each registered context.
When sending emails, we cannot render the SVG icons for hashtags, so
we need to change the HTML hashtags to the normal `#hashtag` text.
**Markdown**
The `hashtag-autocomplete.js` file is where I have added the new `hashtag-autocomplete`
markdown rule, and like all of our rules this is used to cook the raw text on both the clientside
and on the serverside using MiniRacer. Only on the server side do we actually reach out to
the database with the `hashtagLookup` function, on the clientside we just render a plainer
version of the hashtag HTML. Only in the composer preview do we do further lookups based
on this.
This rule is the first one (that I can find) that uses the `currentUser` based on a passed
in `user_id` for guardian checks in markdown rendering code. This is the `last_editor_id`
for both the post and chat message. In some cases we need to cook without a user present,
so the `Discourse.system_user` is used in this case.
**Chat Channels**
This also contains the changes required for chat so that chat channels can be used
as a data source for hashtag searches and lookups. This data source will only be
used when `enable_experimental_hashtag_autocomplete` is `true`, so we don't have
to worry about channel results suddenly turning up.
------
**Known Rough Edges**
- Onebox excerpts will not render the icon svg/use tags, I plan to address that in a follow up PR
- Selecting a hashtag + pressing the Quote button will result in weird behaviour, I plan to address that in a follow up PR
- Mixed hashtag contexts for hashtags without a type suffix will not work correctly, e.g. #ux which is both a category and a channel slug will resolve to a category when used inside a post or within a [chat] transcript in that post. Users can get around this manually by adding the correct suffix, for example ::channel. We may get to this at some point in future
- Icons will not show for the hashtags in emails since SVG support is so terrible in email (this is not likely to be resolved, but still noting for posterity)
- Additional refinements and review fixes wil
2022-11-21 06:37:06 +08:00
# @param {String} type - 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.
# @param {Class} klass - Must be a class that implements methods with the following
# signatures:
#
# @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
def register_hashtag_data_source ( type , klass )
HashtagAutocompleteService . register_data_source ( type , klass )
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.
def register_hashtag_type_in_context ( type , context , priority )
HashtagAutocompleteService . register_type_in_context ( type , context , priority )
2022-10-19 12:03:57 +08:00
end
2014-04-10 14:30:22 +08:00
protected
2019-07-15 22:52:54 +08:00
def self . js_path
File . expand_path " #{ Rails . root } /app/assets/javascripts/plugins "
end
def js_file_path
2022-08-22 16:56:39 +08: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 22:52:54 +08:00
end
2014-04-10 14:30:22 +08:00
def register_assets!
2019-08-21 00:39:52 +08:00
assets . each do | asset , opts , plugin_directory_name |
DiscoursePluginRegistry . register_asset ( asset , opts , plugin_directory_name )
2014-04-10 14:30:22 +08:00
end
end
2017-11-23 09: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 19:09:18 +08:00
def register_locales!
root_path = File . dirname ( @path )
locales . each do | locale , opts |
opts = opts . dup
2020-11-11 23:44:01 +08: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 19:09:18 +08: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 21:42:58 +08:00
opts [ :message_format ] = JsLocaleHelper . find_message_format_locale ( locale_chain , fallback_to_english : false ) unless opts [ :message_format ]
2018-01-25 19:09:18 +08: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-26 03:40:02 +08: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 19:09:18 +08:00
if valid_locale? ( opts )
DiscoursePluginRegistry . register_locale ( locale , opts )
Rails . configuration . assets . precompile << " locales/ #{ locale } .js "
2018-04-21 03:29:03 +08:00
else
2018-06-22 22:20:20 +08: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 19:09:18 +08:00
end
end
end
2019-08-29 22:56:46 +08:00
def allow_new_queued_post_payload_attribute ( attribute_name )
reloadable_patch do
NewPostManager . add_plugin_payload_attribute ( attribute_name )
end
end
2015-11-06 22:02:40 +08:00
private
2021-06-23 02:00:04 +08:00
def validate_directory_column_name ( column_name )
match = / ^[_a-z]+$ / . match ( column_name )
raise " Invalid directory column name ' #{ column_name } '. Can only contain a-z and underscores " unless match
end
2015-11-06 22:02:40 +08:00
def write_asset ( path , contents )
2022-01-06 01:45:08 +08:00
unless File . exist? ( path )
2015-11-06 22:02:40 +08:00
ensure_directory ( path )
File . open ( path , " w " ) { | f | f . write ( contents ) }
end
end
2017-08-10 04:22:18 +08: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-10 04:22:18 +08:00
# reload the patch
2017-08-10 00:28:32 +08:00
yield plugin
end
end
# apply the patch
yield plugin
end
2018-01-25 19:09:18 +08: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-11 00:18:45 +08:00
def register_permitted_bulk_action_parameter ( name )
DiscoursePluginRegistry . register_permitted_bulk_action_parameter ( name , self )
end
2013-08-23 14:21:52 +08:00
end