diff --git a/app/assets/javascripts/discourse/controllers/login_controller.js b/app/assets/javascripts/discourse/controllers/login_controller.js
index 5ea6a4c39dd..55c3c309886 100644
--- a/app/assets/javascripts/discourse/controllers/login_controller.js
+++ b/app/assets/javascripts/discourse/controllers/login_controller.js
@@ -16,19 +16,12 @@ Discourse.LoginController = Discourse.Controller.extend(Discourse.ModalFunctiona
return Discourse.Site.instance();
}.property(),
-
/**
Determines whether at least one login button is enabled
**/
hasAtLeastOneLoginButton: function() {
- return Discourse.SiteSettings.enable_google_logins ||
- Discourse.SiteSettings.enable_facebook_logins ||
- Discourse.SiteSettings.enable_cas_logins ||
- Discourse.SiteSettings.enable_twitter_logins ||
- Discourse.SiteSettings.enable_yahoo_logins ||
- Discourse.SiteSettings.enable_github_logins ||
- Discourse.SiteSettings.enable_persona_logins;
- }.property(),
+ return Em.get("Discourse.LoginMethod.all").length > 0;
+ }.property("Discourse.LoginMethod.all.@each"),
loginButtonText: function() {
return this.get('loggingIn') ? I18n.t('login.logging_in') : I18n.t('login.title');
@@ -78,54 +71,30 @@ Discourse.LoginController = Discourse.Controller.extend(Discourse.ModalFunctiona
authMessage: (function() {
if (this.blank('authenticate')) return "";
- return I18n.t("login." + (this.get('authenticate')) + ".message");
+ var method = Discourse.get('LoginMethod.all').findProperty("name", this.get("authenticate"));
+ if(method){
+ return method.get('message');
+ }
}).property('authenticate'),
- twitterLogin: function() {
- this.set('authenticate', 'twitter');
- var left = this.get('lastX') - 400;
- var top = this.get('lastY') - 200;
- return window.open(Discourse.getURL("/auth/twitter"), "_blank", "menubar=no,status=no,height=400,width=800,left=" + left + ",top=" + top);
- },
+ externalLogin: function(loginMethod){
+ var name = loginMethod.get("name");
+ var customLogin = loginMethod.get("customLogin");
- facebookLogin: function() {
- this.set('authenticate', 'facebook');
- var left = this.get('lastX') - 400;
- var top = this.get('lastY') - 200;
- return window.open(Discourse.getURL("/auth/facebook"), "_blank", "menubar=no,status=no,height=400,width=800,left=" + left + ",top=" + top);
- },
-
- casLogin: function() {
- var left, top;
- this.set('authenticate', 'cas');
- left = this.get('lastX') - 400;
- top = this.get('lastY') - 200;
- return window.open("/auth/cas", "_blank", "menubar=no,status=no,height=400,width=800,left=" + left + ",top=" + top);
- },
-
- openidLogin: function(provider) {
- var left = this.get('lastX') - 400;
- var top = this.get('lastY') - 200;
- if (provider === "yahoo") {
- this.set("authenticate", 'yahoo');
- return window.open(Discourse.getURL("/auth/yahoo"), "_blank", "menubar=no,status=no,height=400,width=800,left=" + left + ",top=" + top);
+ if(customLogin){
+ customLogin();
} else {
- window.open(Discourse.getURL("/auth/google"), "_blank", "menubar=no,status=no,height=500,width=850,left=" + left + ",top=" + top);
- return this.set("authenticate", 'google');
+ this.set('authenticate', name);
+ var left = this.get('lastX') - 400;
+ var top = this.get('lastY') - 200;
+
+ var height = loginMethod.get("frameHeight") || 400;
+ var width = loginMethod.get("frameWidth") || 800;
+ window.open(Discourse.getURL("/auth/" + name), "_blank",
+ "menubar=no,status=no,height=" + height + ",width=" + width + ",left=" + left + ",top=" + top);
}
},
- githubLogin: function() {
- this.set('authenticate', 'github');
- var left = this.get('lastX') - 400;
- var top = this.get('lastY') - 200;
- return window.open(Discourse.getURL("/auth/github"), "_blank", "menubar=no,status=no,height=400,width=800,left=" + left + ",top=" + top);
- },
-
- personaLogin: function() {
- navigator.id.request();
- },
-
authenticationComplete: function(options) {
if (options.awaiting_approval) {
this.flash(I18n.t('login.awaiting_approval'), 'success');
diff --git a/app/assets/javascripts/discourse/models/login_method.js b/app/assets/javascripts/discourse/models/login_method.js
new file mode 100644
index 00000000000..5d11e3c2101
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/login_method.js
@@ -0,0 +1,69 @@
+Discourse.LoginMethod = Ember.Object.extend({
+ title: function(){
+ return this.get("titleOverride") || I18n.t("login." + this.get("name") + ".title");
+ }.property(),
+
+ message: function(){
+ return this.get("messageOverride") || I18n.t("login." + this.get("name") + ".message");
+ }.property()
+});
+
+// Note, you can add login methods by adding to the list
+// just Em.get("Discourse.LoginMethod.all") and then
+// pushObject for any new methods
+Discourse.LoginMethod.reopenClass({
+ register: function(method){
+ if(this.methods){
+ this.methods.pushObject(method);
+ } else {
+ this.preRegister = this.preRegister || [];
+ this.preRegister.push(method);
+ }
+ },
+
+ all: function(){
+ if (this.methods) { return this.methods; }
+
+ var methods = this.methods = Em.A();
+
+ /*
+ * enable_google_logins etc.
+ * */
+
+ [ "google",
+ "facebook",
+ "cas",
+ "twitter",
+ "yahoo",
+ "github",
+ "persona"
+ ].forEach(function(name){
+ if(Discourse.SiteSettings["enable_" + name + "_logins"]){
+
+ var params = {name: name};
+
+ if(name === "persona") {
+ params.customLogin = function(){
+ navigator.id.request();
+ };
+ }
+
+ if(name === "google") {
+ params.frameWidth = 850;
+ params.frameHeight = 500;
+ }
+
+ methods.pushObject(Discourse.LoginMethod.create(params));
+ }
+ });
+
+ if (this.preRegister){
+ this.preRegister.forEach(function(method){
+ methods.pushObject(method);
+ });
+ delete this.preRegister;
+ }
+ return methods;
+ }.property()
+});
+
diff --git a/app/assets/javascripts/discourse/templates/modal/login.js.handlebars b/app/assets/javascripts/discourse/templates/modal/login.js.handlebars
index 53904f4c3f9..b0bf035c4cf 100644
--- a/app/assets/javascripts/discourse/templates/modal/login.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/modal/login.js.handlebars
@@ -1,27 +1,9 @@
{{#if hasAtLeastOneLoginButton}}
- {{#if Discourse.SiteSettings.enable_google_logins}}
-
- {{/if}}
- {{#if Discourse.SiteSettings.enable_facebook_logins}}
-
- {{/if}}
- {{#if Discourse.SiteSettings.enable_cas_logins}}
-
- {{/if}}
- {{#if Discourse.SiteSettings.enable_twitter_logins}}
-
- {{/if}}
- {{#if Discourse.SiteSettings.enable_yahoo_logins}}
-
- {{/if}}
- {{#if Discourse.SiteSettings.enable_github_logins}}
-
- {{/if}}
- {{#if Discourse.SiteSettings.enable_persona_logins}}
-
- {{/if}}
+ {{#each Discourse.LoginMethod.all}}
+
+ {{/each}}
{{/if}}
{{#if Discourse.SiteSettings.enable_local_logins}}
diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb
index 168e1bc95ef..a76cbd9ece4 100644
--- a/app/controllers/users/omniauth_callbacks_controller.rb
+++ b/app/controllers/users/omniauth_callbacks_controller.rb
@@ -20,15 +20,28 @@ class Users::OmniauthCallbacksController < ApplicationController
skip_before_filter :verify_authenticity_token, only: :complete
def complete
- # Make sure we support that provider
provider = params[:provider]
- raise Discourse::InvalidAccess.new unless self.class.types.keys.map(&:to_s).include?(provider)
- # Check if the provider is enabled
- raise Discourse::InvalidAccess.new("provider is not enabled") unless SiteSetting.send("enable_#{provider}_logins?")
+ # If we are a plugin, then try to login with it
+ found = false
+ Discourse.auth_providers.each do |p|
+ if p.name == provider && p.type == :open_id
+ create_or_sign_on_user_using_openid request.env["omniauth.auth"]
+ found = true
+ break
+ end
+ end
- # Call the appropriate logic
- send("create_or_sign_on_user_using_#{provider}", request.env["omniauth.auth"])
+ unless found
+ # Make sure we support that provider
+ raise Discourse::InvalidAccess.new unless self.class.types.keys.map(&:to_s).include?(provider)
+
+ # Check if the provider is enabled
+ raise Discourse::InvalidAccess.new("provider is not enabled") unless SiteSetting.send("enable_#{provider}_logins?")
+
+ # Call the appropriate logic
+ send("create_or_sign_on_user_using_#{provider}", request.env["omniauth.auth"])
+ end
@data[:awaiting_approval] = true if invite_only?
diff --git a/config/application.rb b/config/application.rb
index 4aa0a2f1b21..311174a58cc 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -119,6 +119,11 @@ module Discourse
# attr_accessible.
config.active_record.whitelist_attributes = false
+ unless Rails.env.test?
+ require 'plugin'
+ Discourse.activate_plugins!
+ end
+
# So open id logs somewhere sane
config.after_initialize do
OpenID::Util.logger = Rails.logger
@@ -131,9 +136,6 @@ module Discourse
Clockwork.run
end
end
-
end
-
-
end
end
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 18c4a8d1891..1300c64f915 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -18,8 +18,18 @@ Rails.application.config.middleware.use OmniAuth::Builder do
:identifier => 'https://me.yahoo.com',
:require => 'omniauth-openid'
- # lambda is required for proper multisite support,
- # without it subdomains will not function correctly
+ Discourse.auth_providers.each do |p|
+ if p.type == :open_id
+ provider :open_id, {
+ :name => p.name,
+ :store => OpenID::Store::Redis.new($redis),
+ :require => 'omniauth-openid'
+ }.merge(p.options)
+ end
+ end
+
+ # lambda is required for proper multisite support,
+ # without it subdomains will not function correctly
provider :facebook,
:setup => lambda { |env|
strategy = env['omniauth.strategy']
diff --git a/lib/auth_provider.rb b/lib/auth_provider.rb
new file mode 100644
index 00000000000..dc8c9524006
--- /dev/null
+++ b/lib/auth_provider.rb
@@ -0,0 +1,4 @@
+class AuthProvider
+ attr_accessor :type, :glyph, :background_color, :name, :title,
+ :message, :frame_width, :frame_height, :options
+end
diff --git a/lib/discourse.rb b/lib/discourse.rb
index f8fe64be856..b9829e5111b 100644
--- a/lib/discourse.rb
+++ b/lib/discourse.rb
@@ -23,6 +23,31 @@ module Discourse
# Cross site request forgery
class CSRF < Exception; end
+ def self.activate_plugins!
+ @plugins = Plugin.find_all("#{Rails.root}/plugins")
+ @plugins.each do |plugin|
+ plugin.activate!
+ end
+ end
+
+ def self.plugins
+ @plugins
+ end
+
+ def self.auth_providers
+ providers = nil
+ if plugins
+ plugins.each do |p|
+ next unless p.auth_providers
+ p.auth_providers.each do |prov|
+ providers ||= []
+ providers << prov
+ end
+ end
+ end
+ providers
+ end
+
def self.cache
@cache ||= Cache.new
end
diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb
index bf5fa95fa88..11d12d18b28 100644
--- a/lib/discourse_plugin_registry.rb
+++ b/lib/discourse_plugin_registry.rb
@@ -7,21 +7,22 @@ class DiscoursePluginRegistry
attr_accessor :javascripts
attr_accessor :server_side_javascripts
attr_accessor :stylesheets
+
+ # Default accessor values
+ #
+ def stylesheets
+ @stylesheets ||= Set.new
+ end
+
+ def javascripts
+ @javascripts ||= Set.new
+ end
+
+ def server_side_javascripts
+ @server_side_javascripts ||= Set.new
+ end
end
- # Default accessor values
- #
- def self.stylesheets
- @stylesheets ||= Set.new
- end
-
- def self.javascripts
- @javascripts ||= Set.new
- end
-
- def self.server_side_javascripts
- @server_side_javascripts ||= Set.new
- end
def register_js(filename, options={})
# If we have a server side option, add that too.
diff --git a/lib/plugin.rb b/lib/plugin.rb
new file mode 100644
index 00000000000..faef47cafe8
--- /dev/null
+++ b/lib/plugin.rb
@@ -0,0 +1,177 @@
+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 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?
+ paths = []
+ assets.each do |asset|
+ if asset =~ /\.js$/
+ DiscoursePluginRegistry.javascripts << asset
+ elsif asset =~ /\.css$|\.scss$/
+ DiscoursePluginRegistry.stylesheets << asset
+ end
+ paths << File.dirname(asset)
+ 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
+ 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
diff --git a/spec/components/plugin_spec.rb b/spec/components/plugin_spec.rb
new file mode 100644
index 00000000000..4d683636712
--- /dev/null
+++ b/spec/components/plugin_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+require_dependency 'plugin'
+
+describe Plugin do
+ context "parse" do
+ it "correctly parses plugin info" do
+ plugin = Plugin.parse <
'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
diff --git a/spec/fixtures/plugins/my_plugin/plugin.rb b/spec/fixtures/plugins/my_plugin/plugin.rb
new file mode 100644
index 00000000000..718f236499c
--- /dev/null
+++ b/spec/fixtures/plugins/my_plugin/plugin.rb
@@ -0,0 +1,10 @@
+# name: plugin-name
+# about: about: my plugin
+# version: 0.1
+# authors: Frank Zappa
+
+auth_provider :open_id,
+ :name => 'zappa',
+ :identifier => 'https://zappa.com',
+ :background_color => '#dd4814',
+ :glyph => 'B'