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'
require_dependency 'plugin/metadata'
2018-08-07 09:04:29 +08:00
require_dependency '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
2020-04-14 03:05:46 +08:00
# If plugins provide `transpile_js: true` in their metadata we will
# transpile regular JS files in the assets folders. Going forward,
# all plugins should do this.
def transpile_js
metadata . try ( :transpile_js ) == " true "
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 )
2020-07-27 08:23:54 +08:00
Discourse . deprecate ( " whitelist_staff_user_custom_field is deprecated, use the allow_staff_user_custom_field. " , drop_from : " 2.6 " )
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 )
2020-07-27 08:23:54 +08:00
Discourse . deprecate ( " whitelist_public_user_custom_field is deprecated, use the allow_public_user_custom_field. " , drop_from : " 2.6 " )
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
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
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 )
2020-07-27 08:23:54 +08:00
Discourse . deprecate ( " topic_view_post_custom_fields_whitelister is deprecated, use the topic_view_post_custom_fields_allowlister. " , drop_from : " 2.6 " )
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 |
2020-07-27 08:23:54 +08:00
:: TopicView . add_post_custom_fields_allowlister do | user |
2018-07-26 22:52:39 +08:00
plugin . enabled? ? block . call ( user ) : [ ]
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
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
def delete_extra_automatic_assets ( good_paths )
2013-09-21 05:39:14 +08:00
return unless Dir . exists? auto_generated_path
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 )
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 "
2020-12-04 00:25:42 +08:00
DiscoursePluginRegistry . register_glob ( root_path , 'js' ) if transpile_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 "
2020-12-04 00:25:42 +08:00
DiscoursePluginRegistry . register_glob ( admin_path , 'js' , admin : true ) if transpile_js
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
if transpile_js
DiscourseJsProcessor . plugin_transpile_paths << root_path . sub ( Rails . root . to_s , '' ) . sub ( / ^ \/ * / , '' )
2020-05-08 23:10:34 +08:00
DiscourseJsProcessor . plugin_transpile_paths << admin_path . sub ( Rails . root . to_s , '' ) . sub ( / ^ \/ * / , '' )
2020-06-17 02:30:25 +08:00
test_path = " #{ root_dir_name } /test/javascripts "
DiscourseJsProcessor . plugin_transpile_paths << test_path . sub ( Rails . root . to_s , '' ) . sub ( / ^ \/ * / , '' )
2020-04-14 03:05:46 +08:00
end
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 "
if Dir . exists? ( public_data )
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 } ') " }
each_globbed_asset do | f , is_dir |
contents << ( is_dir ? " depend_on(' #{ f } ') " : " require_asset(' #{ f } ') " )
end
if contents . present?
contents . insert ( 0 , " <% " )
contents << " %> "
2020-03-05 01:28:26 +08:00
Discourse :: Utils . atomic_write_file ( js_file_path , contents . join ( " \n " ) )
else
begin
File . delete ( js_file_path )
rescue Errno :: ENOENT
end
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
2018-11-22 23:59:47 +08:00
Discourse . deprecate ( " #{ provider . authenticator . class . name } should define an `enabled?` function. Patching for now. " )
2019-05-07 09:00:09 +08:00
return SiteSetting . get ( provider . enabled_setting ) if provider . enabled_setting
2018-11-22 23:59:47 +08:00
Discourse . deprecate ( " #{ provider . authenticator . class . name } has not defined an enabled_setting. Defaulting to true. " )
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 ]
2021-03-25 01:51:21 +08:00
elsif transpile_js && 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?
File . exists? ( js_file_path )
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,
# parameters: :testparam
# )
#
# 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? ( " * " )
Discourse . deprecate ( " * is no longer a valid api_parameter_route format matcher. Use `nil` instead " , drop_from : " 2.7 " )
# Old API used * as wildcard. New api uses `nil`
format = nil
end
# Backwards compatibility with old parameter names:
if method || route || format
Discourse . deprecate ( " method, route and format parameters for api_parameter_routes are deprecated. Use methods, actions and formats instead. " , drop_from : " 2.7 " )
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
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
2019-08-21 00:39:52 +08:00
@file_path || = " #{ Plugin :: Instance . js_path } / #{ directory_name } .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
def write_asset ( path , contents )
unless File . exists? ( path )
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