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, :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) klass = "#{serializer.to_s.classify}Serializer".constantize rescue "#{serializer.to_s}Serializer".constantize klass.attributes(attr) unless attr.to_s.start_with?("include_") klass.send(:define_method, attr, &block) return unless define_include_method # Don't include serialized methods if the plugin is disabled plugin = self klass.send(:define_method, "include_#{attr}?") { plugin.enabled? } end def whitelist_staff_user_custom_field(field) User.register_plugin_staff_custom_field(field, self) end # Extend a class but check that the plugin is enabled # for class methods use `add_class_method` def add_to_class(klass, attr, &block) klass = klass.to_s.classify.constantize rescue klass.to_s.constantize hidden_method_name = :"#{attr}_without_enable_check" klass.send(:define_method, hidden_method_name, &block) plugin = self klass.send(:define_method, attr) do |*args| send(hidden_method_name, *args) if plugin.enabled? end end # Adds a class method to a class, respecting if plugin is enabled def add_class_method(klass, attr, &block) klass = klass.to_s.classify.constantize rescue klass.to_s.constantize hidden_method_name = :"#{attr}_without_enable_check" klass.send(:define_singleton_method, hidden_method_name, &block) plugin = self klass.send(:define_singleton_method, attr) do |*args| send(hidden_method_name, *args) if plugin.enabled? end end def add_model_callback(klass, callback, options = {}, &block) klass = klass.to_s.classify.constantize rescue klass.to_s.constantize plugin = self # 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 # 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| ColorScheme.create_from_base(name: c[:name], colors: c[:colors]) unless ColorScheme.where(name: c[:name]).exists? 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_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 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) full_path = File.dirname(path) << "/assets/" << file assets << [full_path, 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_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 << <