refactoring the plugin interfaces to allow for better extensible

This commit is contained in:
Sam 2013-08-23 16:21:52 +10:00
parent 2eb55b74e4
commit 075002a6d5
11 changed files with 308 additions and 263 deletions

View File

@ -2,7 +2,7 @@
<head></head>
<body>
<script type="text/javascript">
window.opener.Discourse.authenticationComplete(<%=@data.to_json.html_safe%>);
window.opener.Discourse.authenticationComplete(<%=@data.to_client_hash.to_json.html_safe%>);
window.close();
</script>
</body>

View File

@ -33,6 +33,8 @@ Rails.application.config.middleware.use OmniAuth::Builder do
:name => p.name,
:require => "omniauth-oauth2"
}.merge(p.options)
else
provider p.type, *p.options
end
end

View File

@ -1,4 +0,0 @@
class AuthProvider
attr_accessor :type, :glyph, :background_color, :name, :title,
:message, :frame_width, :frame_height, :options
end

View File

@ -1,4 +1,5 @@
require 'cache'
require_dependency 'plugin/instance'
module Discourse
@ -24,7 +25,7 @@ module Discourse
class CSRF < Exception; end
def self.activate_plugins!
@plugins = Plugin.find_all("#{Rails.root}/plugins")
@plugins = Plugin::Instance.find_all("#{Rails.root}/plugins")
@plugins.each do |plugin|
plugin.activate!
end

View File

@ -1,192 +0,0 @@
require_dependency 'auth_provider'
require 'digest/sha1'
require 'fileutils'
class Plugin
METADATA = [:name, :about, :version, :authors]
attr_accessor :path
attr_accessor *METADATA
attr_reader :auth_providers
attr_reader :assets
def self.find_all(parent_path)
plugins = []
Dir["#{parent_path}/**/plugin.rb"].each do |path|
plugin = parse(File.read(path))
plugin.path = path
plugins << plugin
end
plugins
end
def self.parse(text)
plugin = self.new
text.each_line do |line|
break unless plugin.parse_line(line)
end
plugin
end
def initialize
@assets = []
end
def parse_line(line)
line = line.strip
unless line.empty?
return false unless line[0] == "#"
attribute, *description = line[1..-1].split(":")
description = description.join(":")
attribute = attribute.strip.to_sym
if METADATA.include?(attribute)
self.send("#{attribute}=", description.strip)
end
end
true
end
# will make sure all the assets this plugin needs are registered
def generate_automatic_assets!
paths = []
automatic_assets.each do |path, contents|
unless File.exists? path
ensure_directory path
File.open(path,"w") do |f|
f.write(contents)
end
end
paths << path
end
delete_extra_automatic_assets(paths)
paths
end
def delete_extra_automatic_assets(good_paths)
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 auto_generated_path
File.dirname(path) << "/auto_generated"
end
def register_css(style)
@styles ||= []
@styles << style
end
def register_javascript(js)
@javascripts ||= []
@javascripts << js
end
def register_asset(file,opts=nil)
full_path = File.dirname(path) << "/assets/" << file
assets << full_path
if opts == :server_side
@server_side_javascripts ||= []
@server_side_javascripts << full_path
end
end
def automatic_assets
css = ""
js = "(function(){"
css = @styles.join("\n") if @styles
js = @javascripts.join("\n") if @javascripts
unless auth_providers.blank?
auth_providers.each do |auth|
overrides = ""
overrides = ", titleOverride: '#{auth.title}'" if auth.title
overrides << ", messageOverride: '#{auth.message}'" if auth.message
overrides << ", frameWidth: '#{auth.frame_width}'" if auth.frame_width
overrides << ", frameHeight: '#{auth.frame_height}'" if auth.frame_height
js << "Discourse.LoginMethod.register(Discourse.LoginMethod.create({name: '#{auth.name}'#{overrides}}));\n"
if auth.glyph
css << ".btn-social.#{auth.name}:before{ content: '#{auth.glyph}'; }\n"
end
if auth.background_color
css << ".btn-social.#{auth.name}{ background: #{auth.background_color}; }\n"
end
end
end
js << "})();"
# TODO don't serve blank assets
[[css,"css"],[js,"js"]].map do |asset, extension|
hash = Digest::SHA1.hexdigest asset
["#{auto_generated_path}/plugin_#{hash}.#{extension}", asset]
end
end
# note, we need to be able to parse seperately to activation.
# this allows us to present information about a plugin in the UI
# prior to activations
def activate!
self.instance_eval File.read(path)
if auto_assets = generate_automatic_assets!
assets.concat auto_assets
end
unless assets.blank?
assets.each do |asset|
if asset =~ /\.js$/
DiscoursePluginRegistry.javascripts << asset
elsif asset =~ /\.css$|\.scss$/
DiscoursePluginRegistry.stylesheets << asset
end
end
# TODO possibly amend this to a rails engine
Rails.configuration.assets.paths << auto_generated_path
Rails.configuration.assets.paths << File.dirname(path) + "/assets"
end
if @server_side_javascripts
@server_side_javascripts.each do |js|
DiscoursePluginRegistry.server_side_javascripts << js
end
end
end
def auth_provider(type, opts)
@auth_providers ||= []
provider = AuthProvider.new
provider.type = type
[:name, :glyph, :background_color, :title, :message, :frame_width, :frame_height].each do |sym|
provider.send "#{sym}=", opts.delete(sym)
end
provider.options = opts
@auth_providers << provider
end
end

View File

@ -0,0 +1,4 @@
class Plugin::AuthProvider
attr_accessor :type, :glyph, :background_color, :name, :title,
:message, :frame_width, :frame_height, :options, :callback
end

193
lib/plugin/instance.rb Normal file
View File

@ -0,0 +1,193 @@
require 'digest/sha1'
require 'fileutils'
require_dependency 'plugin/metadata'
require_dependency 'plugin/auth_provider'
class Plugin::Instance
attr_reader :auth_providers, :assets, :path
def self.find_all(parent_path)
[].tap { |plugins|
Dir["#{parent_path}/**/plugin.rb"].each do |path|
source = File.read(path)
metadata = Plugin::Metadata.parse(source)
plugins << self.new(metadata, path)
end
}
end
def initialize(metadata, path)
@metadata = metadata
@path = path
@assets = []
end
# will make sure all the assets this plugin needs are registered
def generate_automatic_assets!
paths = []
automatic_assets.each do |path, contents|
unless File.exists? path
ensure_directory path
File.open(path,"w") do |f|
f.write(contents)
end
end
paths << path
end
delete_extra_automatic_assets(paths)
paths
end
def delete_extra_automatic_assets(good_paths)
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 auto_generated_path
File.dirname(path) << "/auto_generated"
end
def register_css(style)
@styles ||= []
@styles << style
end
def register_javascript(js)
@javascripts ||= []
@javascripts << js
end
def register_asset(file,opts=nil)
full_path = File.dirname(path) << "/assets/" << file
assets << full_path
if opts == :server_side
@server_side_javascripts ||= []
@server_side_javascripts << full_path
end
end
def automatic_assets
css = ""
js = "(function(){"
css = @styles.join("\n") if @styles
js = @javascripts.join("\n") if @javascripts
unless auth_providers.blank?
auth_providers.each do |auth|
overrides = ""
overrides = ", titleOverride: '#{auth.title}'" if auth.title
overrides << ", messageOverride: '#{auth.message}'" if auth.message
overrides << ", frameWidth: '#{auth.frame_width}'" if auth.frame_width
overrides << ", frameHeight: '#{auth.frame_height}'" if auth.frame_height
js << "Discourse.LoginMethod.register(Discourse.LoginMethod.create({name: '#{auth.name}'#{overrides}}));\n"
if auth.glyph
css << ".btn-social.#{auth.name}:before{ content: '#{auth.glyph}'; }\n"
end
if auth.background_color
css << ".btn-social.#{auth.name}{ background: #{auth.background_color}; }\n"
end
end
end
js << "})();"
# TODO don't serve blank assets
[[css,"css"],[js,"js"]].map do |asset, extension|
hash = Digest::SHA1.hexdigest asset
["#{auto_generated_path}/plugin_#{hash}.#{extension}", asset]
end
end
# note, we need to be able to parse seperately to activation.
# this allows us to present information about a plugin in the UI
# prior to activations
def activate!
self.instance_eval File.read(path)
if auto_assets = generate_automatic_assets!
assets.concat auto_assets
end
unless assets.blank?
assets.each do |asset|
if asset =~ /\.js$/
DiscoursePluginRegistry.javascripts << asset
elsif asset =~ /\.css$|\.scss$/
DiscoursePluginRegistry.stylesheets << asset
end
end
# TODO possibly amend this to a rails engine
Rails.configuration.assets.paths << auto_generated_path
Rails.configuration.assets.paths << File.dirname(path) + "/assets"
end
if @server_side_javascripts
@server_side_javascripts.each do |js|
DiscoursePluginRegistry.server_side_javascripts << js
end
end
end
def auth_provider(type, opts)
@auth_providers ||= []
provider = Plugin::AuthProvider.new
provider.type = type
[:name, :glyph, :background_color, :title, :message, :frame_width, :frame_height, :callback].each do |sym|
provider.send "#{sym}=", opts.delete(sym)
end
provider.name ||= type.to_s
provider.options = opts[:middleware_options] || opts
# prepare for splatting
provider.options = [provider.options] if provider.options.is_a? Hash
@auth_providers << provider
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 = {})
gems_path = File.dirname(path) + "/gems/#{RUBY_VERSION}"
spec_path = gems_path + "/specifications"
spec_file = spec_path + "/#{name}-#{version}.gemspec"
unless File.exists? spec_file
command = "gem install #{name} -v #{version} -i #{gems_path} --no-rdoc --no-ri"
puts command
puts `command`
end
if File.exists? spec_file
spec = Gem::Specification.load spec_file
spec.activate
unless opts[:require] == false
require name
end
else
puts "You are specifying the gem #{name} in #{path}, however it does not exist!"
exit -1
end
end
end

33
lib/plugin/metadata.rb Normal file
View File

@ -0,0 +1,33 @@
# loaded really early
module Plugin; end
class Plugin::Metadata
FIELDS = [:name, :about, :version, :authors]
attr_accessor *FIELDS
def self.parse(text)
metadata = self.new
text.each_line do |line|
break unless metadata.parse_line(line)
end
metadata
end
def parse_line(line)
line = line.strip
unless line.empty?
return false unless line[0] == "#"
attribute, *description = line[1..-1].split(":")
description = description.join(":")
attribute = attribute.strip.to_sym
if FIELDS.include?(attribute)
self.send("#{attribute}=", description.strip)
end
end
true
end
end

View File

@ -0,0 +1,32 @@
require 'spec_helper'
require_dependency 'plugin/instance'
describe Plugin::Instance do
context "activate!" do
it "can activate plugins correctly" do
plugin = Plugin.new
plugin.path = "#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb"
junk_file = "#{plugin.auto_generated_path}/junk"
plugin.ensure_directory(junk_file)
File.open("#{plugin.auto_generated_path}/junk", "w") {|f| f.write("junk")}
plugin.activate!
plugin.auth_providers.count.should == 1
auth_provider = plugin.auth_providers[0]
auth_provider.options.should == {:identifier => 'https://zappa.com'}
auth_provider.type.should == :open_id
# calls ensure_assets! make sure they are there
plugin.assets.count.should == 2
plugin.assets.each do |a|
File.exists?(a).should be_true
end
# ensure it cleans up all crap in autogenerated directory
File.exists?(junk_file).should be_false
end
end
end

View File

@ -0,0 +1,41 @@
require 'spec_helper'
require_dependency 'plugin/metadata'
describe Plugin::Metadata do
context "parse" do
it "correctly parses plugin info" do
metadata = Plugin::Metadata.parse <<TEXT
# name: plugin-name
# about: about: my plugin
# version: 0.1
# authors: Frank Zappa
# gem: some_gem
# gem: some_gem, "1"
some_ruby
TEXT
metadata.name.should == "plugin-name"
metadata.about.should == "about: my plugin"
metadata.version.should == "0.1"
metadata.authors.should == "Frank Zappa"
metadata.gems.should == ["some_gem", 'some_gem, "1"']
end
end
context "find_all" do
it "can find plugins correctly" do
metadatas = Plugin::Metadata.find_all("#{Rails.root}/spec/fixtures/plugins")
metadatas.count.should == 1
metadata = metadata[0]
metadata.name.should == "plugin-name"
metadata.path.should == "#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb"
end
it "does not blow up on missing directory" do
metadatas = Plugin.find_all("#{Rails.root}/frank_zappa")
metadatas.count.should == 0
end
end
end

View File

@ -1,65 +0,0 @@
require 'spec_helper'
require_dependency 'plugin'
describe Plugin do
context "parse" do
it "correctly parses plugin info" do
plugin = Plugin.parse <<TEXT
# name: plugin-name
# about: about: my plugin
# version: 0.1
# authors: Frank Zappa
some_ruby
TEXT
plugin.name.should == "plugin-name"
plugin.about.should == "about: my plugin"
plugin.version.should == "0.1"
plugin.authors.should == "Frank Zappa"
end
end
context "find_all" do
it "can find plugins correctly" do
plugins = Plugin.find_all("#{Rails.root}/spec/fixtures/plugins")
plugins.count.should == 1
plugin = plugins[0]
plugin.name.should == "plugin-name"
plugin.path.should == "#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb"
end
it "does not blow up on missing directory" do
plugins = Plugin.find_all("#{Rails.root}/frank_zappa")
plugins.count.should == 0
end
end
context "activate!" do
it "can activate plugins correctly" do
plugin = Plugin.new
plugin.path = "#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb"
junk_file = "#{plugin.auto_generated_path}/junk"
plugin.ensure_directory(junk_file)
File.open("#{plugin.auto_generated_path}/junk", "w") {|f| f.write("junk")}
plugin.activate!
plugin.auth_providers.count.should == 1
auth_provider = plugin.auth_providers[0]
auth_provider.options.should == {:identifier => 'https://zappa.com'}
auth_provider.type.should == :open_id
# calls ensure_assets! make sure they are there
plugin.assets.count.should == 2
plugin.assets.each do |a|
File.exists?(a).should be_true
end
# ensure it cleans up all crap in autogenerated directory
File.exists?(junk_file).should be_false
end
end
end