require 'digest/sha1' require 'fileutils' require_dependency 'plugin/metadata' require_dependency 'plugin/auth_provider' class Plugin::CustomEmoji def self.cache_key @@cache_key ||= "plugin-emoji" end def self.emojis @@emojis ||= {} end def self.register(name, url) @@cache_key = Digest::SHA1.hexdigest(cache_key + name)[0..10] emojis[name] = url end end class Plugin::Instance attr_accessor :path, :metadata attr_reader :admin_route # Memoized array readers [:assets, :auth_providers, :color_schemes, :initializers, :javascripts, :locales, :service_workers, :styles, :themes].each do |att| class_eval %Q{ def #{att} @#{att} ||= [] end } end def seed_data @seed_data ||= HashWithIndifferentAccess.new({}) end def self.find_all(parent_path) [].tap { |plugins| # also follows symlinks - http://stackoverflow.com/q/357754 Dir["#{parent_path}/*/plugin.rb"].sort.each do |path| # tagging is included in core, so don't load it next if path =~ /discourse-tagging/ source = File.read(path) metadata = Plugin::Metadata.parse(source) plugins << self.new(metadata, path) end } end def initialize(metadata = nil, path = nil) @metadata = metadata @path = path @idx = 0 end def add_admin_route(label, location) @admin_route = { label: label, location: location } end def enabled? @enabled_site_setting ? SiteSetting.send(@enabled_site_setting) : true end delegate :name, to: :metadata def add_to_serializer(serializer, attr, define_include_method = true, &block) reloadable_patch do |plugin| klass = "#{serializer.to_s.classify}Serializer".constantize rescue "#{serializer.to_s}Serializer".constantize unless attr.to_s.start_with?("include_") klass.attributes(attr) if define_include_method # Don't include serialized methods if the plugin is disabled klass.send(:define_method, "include_#{attr}?") { plugin.enabled? } end end klass.send(:define_method, attr, &block) end end def replace_flags settings = ::FlagSettings.new yield settings reloadable_patch do |plugin| ::PostActionType.replace_flag_settings(settings) if plugin.enabled? end end def whitelist_staff_user_custom_field(field) reloadable_patch do |plugin| ::User.register_plugin_staff_custom_field(field, plugin) if plugin.enabled? end end def custom_avatar_column(column) reloadable_patch do |plugin| AvatarLookup.lookup_columns << column AvatarLookup.lookup_columns.uniq! end end def add_body_class(class_name) reloadable_patch do |plugin| ::ApplicationHelper.extra_body_classes << class_name if plugin.enabled? end end def rescue_from(exception, &block) reloadable_patch do |plugin| ::ApplicationController.rescue_from(exception, &block) end end # Extend a class but check that the plugin is enabled # for class methods use `add_class_method` def add_to_class(class_name, attr, &block) reloadable_patch do |plugin| klass = class_name.to_s.classify.constantize rescue class_name.to_s.constantize hidden_method_name = :"#{attr}_without_enable_check" klass.send(:define_method, hidden_method_name, &block) klass.send(:define_method, attr) do |*args| send(hidden_method_name, *args) if plugin.enabled? end end end # Adds a class method to a class, respecting if plugin is enabled def add_class_method(klass_name, attr, &block) reloadable_patch do |plugin| klass = klass_name.to_s.classify.constantize rescue klass_name.to_s.constantize hidden_method_name = :"#{attr}_without_enable_check" klass.send(:define_singleton_method, hidden_method_name, &block) klass.send(:define_singleton_method, attr) do |*args| send(hidden_method_name, *args) if plugin.enabled? end end end def add_model_callback(klass_name, callback, options = {}, &block) reloadable_patch do |plugin| klass = klass_name.to_s.classify.constantize rescue klass_name.to_s.constantize # generate a unique method name method_name = "#{plugin.name}_#{klass.name}_#{callback}#{@idx}".underscore @idx += 1 hidden_method_name = :"#{method_name}_without_enable_check" klass.send(:define_method, hidden_method_name, &block) klass.send(callback, options) do |*args| send(hidden_method_name, *args) if plugin.enabled? end hidden_method_name end end def topic_view_post_custom_fields_whitelister(&block) reloadable_patch do |plugin| ::TopicView.add_post_custom_fields_whitelister(&block) if plugin.enabled? end end def add_preloaded_group_custom_field(field) reloadable_patch do |plugin| ::Group.preloaded_custom_field_names << field if plugin.enabled? end end def add_preloaded_topic_list_custom_field(field) reloadable_patch do |plugin| ::TopicList.preloaded_custom_fields << field if plugin.enabled? end end def add_permitted_post_create_param(name) reloadable_patch do |plugin| ::Post.permitted_create_params << name if plugin.enabled? end end # Add validation method but check that the plugin is enabled def validate(klass, name, &block) klass = klass.to_s.classify.constantize klass.send(:define_method, name, &block) plugin = self klass.validate(name, if: -> { plugin.enabled? }) end # will make sure all the assets this plugin needs are registered def generate_automatic_assets! paths = [] assets = [] automatic_assets.each do |path, contents| write_asset(path, contents) paths << path assets << [path] end delete_extra_automatic_assets(paths) assets end def delete_extra_automatic_assets(good_paths) return unless Dir.exists? auto_generated_path 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 def directory File.dirname(path) end def auto_generated_path File.dirname(path) << "/auto_generated" end def after_initialize(&block) initializers << block end # 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 def notify_after_initialize color_schemes.each do |c| unless ColorScheme.where(name: c[:name]).exists? ColorScheme.create_from_base(name: c[:name], colors: c[:colors]) end end initializers.each do |callback| begin callback.call(self) rescue ActiveRecord::StatementInvalid => e # When running `db:migrate` for the first time on a new database, # plugin initializers might try to use models. # Tolerate it. raise e unless e.message.try(:include?, "PG::UndefinedTable") end end end def register_category_custom_field_type(name, type) reloadable_patch do |plugin| Category.register_custom_field_type(name, type) if plugin.enabled? end end def register_topic_custom_field_type(name, type) reloadable_patch do |plugin| ::Topic.register_custom_field_type(name, type) if plugin.enabled? end end def register_post_custom_field_type(name, type) reloadable_patch do |plugin| ::Post.register_custom_field_type(name, type) if plugin.enabled? end end def register_group_custom_field_type(name, type) reloadable_patch do |plugin| ::Group.register_custom_field_type(name, type) if plugin.enabled? end end def register_seedfu_fixtures(paths) paths = [paths] if !paths.kind_of?(Array) SeedFu.fixture_paths.concat(paths) end def listen_for(event_name) return unless self.respond_to?(event_name) DiscourseEvent.on(event_name, &self.method(event_name)) end def register_css(style) styles << style end def register_javascript(js) javascripts << js end # @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 def register_custom_html(hash) DiscoursePluginRegistry.custom_html ||= {} DiscoursePluginRegistry.custom_html.merge!(hash) end def register_html_builder(name, &block) DiscoursePluginRegistry.register_html_builder(name, &block) end def register_asset(file, opts = nil) if opts && opts == :vendored_core_pretty_text full_path = DiscoursePluginRegistry.core_asset_for_name(file) else full_path = File.dirname(path) << "/assets/" << file end assets << [full_path, opts] end def register_service_worker(file, opts = nil) service_workers << [ File.join(File.dirname(path), 'assets', file), opts ] end def register_color_scheme(name, colors) color_schemes << { name: name, colors: colors } end def register_seed_data(key, value) seed_data[key] = value end def register_seed_path_builder(&block) DiscoursePluginRegistry.register_seed_path_builder(&block) end def register_emoji(name, url) Plugin::CustomEmoji.register(name, url) end def automatic_assets css = styles.join("\n") js = javascripts.join("\n") auth_providers.each do |auth| auth_json = auth.to_json hash = Digest::SHA1.hexdigest(auth_json) js << <