diff --git a/Gemfile b/Gemfile
index fc3738e4bdd..976d6f87e79 100644
--- a/Gemfile
+++ b/Gemfile
@@ -97,6 +97,7 @@ gem 'openid-redis-store'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
gem 'omniauth-github'
+gem 'omniauth-oauth2', require: false
gem 'omniauth-browserid', git: 'https://github.com/callahad/omniauth-browserid.git', branch: 'observer_api'
gem 'omniauth-cas'
gem 'oj'
diff --git a/Gemfile.lock b/Gemfile.lock
index 86dc6284cc9..e188624082e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -497,6 +497,7 @@ DEPENDENCIES
omniauth-cas
omniauth-facebook
omniauth-github
+ omniauth-oauth2
omniauth-openid
omniauth-twitter
openid-redis-store
diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js
index b947839b381..2881f7224d1 100644
--- a/app/assets/javascripts/discourse.js
+++ b/app/assets/javascripts/discourse.js
@@ -50,8 +50,8 @@ Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
faviconChanged: function() {
if(Discourse.User.currentProp('dynamic_favicon')) {
- $.faviconNotify(
- Discourse.SiteSettings.favicon_url, this.get('notifyCount')
+ new Favcount(Discourse.SiteSettings.favicon_url).set(
+ this.get('notifyCount')
);
}
}.observes('notifyCount'),
diff --git a/app/assets/javascripts/external/favcount.js b/app/assets/javascripts/external/favcount.js
new file mode 100644
index 00000000000..5ec64d9092d
--- /dev/null
+++ b/app/assets/javascripts/external/favcount.js
@@ -0,0 +1,84 @@
+/*
+ * favcount.js v1.0.1
+ * http://chrishunt.co/favcount
+ * Dynamically updates the favicon with a number.
+ *
+ * Copyright 2013, Chris Hunt
+ * Released under the MIT license
+ */
+
+(function(){
+ function Favcount(icon) {
+ this.icon = icon;
+ this.canvas = document.createElement('canvas');
+ }
+
+ Favcount.prototype.set = function(count) {
+ var self = this,
+ img = document.createElement('img');
+
+ if (self.canvas.getContext) {
+ img.onload = function() {
+ drawCanvas(self.canvas, img, normalize(count));
+ };
+
+ img.src = this.icon;
+ }
+ }
+
+ function normalize(count) {
+ count = Math.round(count);
+
+ if (isNaN(count) || count < 1) {
+ return '';
+ } else if (count < 10) {
+ return ' ' + count;
+ } else if (count > 99) {
+ return '99';
+ } else {
+ return count;
+ }
+ }
+
+ function drawCanvas(canvas, img, count) {
+ var head = document.getElementsByTagName('head')[0],
+ favicon = document.createElement('link'),
+ multiplier, fontSize, context, xOffset, yOffset;
+
+ favicon.rel = 'icon';
+
+ // Scale the canvas based on favicon size
+ multiplier = img.width / 16;
+ fontSize = multiplier * 11;
+ xOffset = multiplier;
+ yOffset = multiplier * 11;
+
+ canvas.height = canvas.width = img.width;
+
+ context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0);
+ context.font = 'bold ' + fontSize + 'px "helvetica", sans-serif';
+
+ // Draw background for contrast
+ context.fillStyle = '#FFF';
+ context.fillText(count, xOffset, yOffset);
+ context.fillText(count, xOffset + 2, yOffset);
+ context.fillText(count, xOffset, yOffset + 2);
+ context.fillText(count, xOffset + 2, yOffset + 2);
+
+ // Draw count in foreground
+ context.fillStyle = '#000';
+ context.fillText(count, xOffset + 1, yOffset + 1);
+
+ // Replace the favicon
+ favicon.href = canvas.toDataURL('image/png');
+ head.removeChild(document.querySelector('link[rel=icon]'));
+ head.appendChild(favicon);
+ }
+
+ this.Favcount = Favcount;
+}).call(this);
+
+(function(){
+ Favcount.VERSION = '1.0.1';
+}).call(this);
diff --git a/app/assets/javascripts/external/jquery.faviconNotify.js b/app/assets/javascripts/external/jquery.faviconNotify.js
deleted file mode 100644
index 984bd2dd0d3..00000000000
--- a/app/assets/javascripts/external/jquery.faviconNotify.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
-* jQuery Favicon Notify
-*
-* Updates the favicon with a number to notify the user of changes.
-*
-* iconUrl: Url of favicon image or icon
-* count: Integer count to place above favicon
-*
-* $.faviconNotify(iconUrl, count)
-*/
-(function($){
- $.faviconNotify = function(iconUrl, count){
- var canvas = canvas || $('')[0],
- img = $('
')[0],
- multiplier, fontSize, context, xOffset, yOffset;
-
- if (canvas.getContext) {
- if (count < 1) { count = '' }
- else if (count < 10) { count = ' ' + count }
- else if (count > 99) { count = '99' }
-
- img.onload = function () {
- canvas.height = canvas.width = this.width;
- multiplier = (this.width / 16);
-
- fontSize = multiplier * 11;
- xOffset = multiplier;
- yOffset = multiplier * 11;
-
- context = canvas.getContext('2d');
- context.drawImage(this, 0, 0);
- context.font = 'bold ' + fontSize + 'px "helvetica", sans-serif';
-
- context.fillStyle = '#FFF';
- context.fillText(count, xOffset, yOffset);
- context.fillText(count, xOffset + 2, yOffset);
- context.fillText(count, xOffset, yOffset + 2);
- context.fillText(count, xOffset + 2, yOffset + 2);
-
- context.fillStyle = '#000';
- context.fillText(count, xOffset + 1, yOffset + 1);
-
- $('link[rel$=icon]').remove();
- $('head').append(
- $('').attr(
- 'href', canvas.toDataURL('image/png')
- )
- );
- };
- img.src = iconUrl;
- }
- };
-})(jQuery);
diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb
index 0edc93bbf3d..9d9ebb4779b 100644
--- a/app/controllers/users/omniauth_callbacks_controller.rb
+++ b/app/controllers/users/omniauth_callbacks_controller.rb
@@ -29,6 +29,10 @@ class Users::OmniauthCallbacksController < ApplicationController
create_or_sign_on_user_using_openid request.env["omniauth.auth"]
found = true
break
+ elsif p.name == provider && p.type == :oauth2
+ create_or_sign_on_user_using_oauth2 request.env["omniauth.auth"]
+ found = true
+ break
end
end
@@ -194,6 +198,58 @@ class Users::OmniauthCallbacksController < ApplicationController
end
+ def create_or_sign_on_user_using_oauth2(auth_token)
+ oauth2_provider = auth_token[:provider]
+ oauth2_uid = auth_token[:uid]
+ data = auth_token[:info]
+ email = data[:email]
+ name = data[:name]
+
+ oauth2_user_info = Oauth2UserInfo.where(uid: oauth2_uid, provider: oauth2_provider).first
+
+ if oauth2_user_info.blank? && user = User.find_by_email(email)
+ # TODO is only safe if we trust our oauth2 provider to return an email
+ # legitimately owned by our user
+ oauth2_user_info = Oauth2UserInfo.create(uid: oauth2_uid,
+ provider: oauth2_provider,
+ name: name,
+ email: name,
+ user: user)
+ end
+
+ authenticated = oauth2_user_info.present?
+
+ if authenticated
+ user = oauth2_user_info.user
+
+ # If we have to approve users
+ if Guardian.new(user).can_access_forum?
+ log_on_user(user)
+ @data = {authenticated: true}
+ else
+ @data = {awaiting_approval: true}
+ end
+ else
+ @data = {
+ email: email,
+ name: User.suggest_name(name),
+ username: UserNameSuggester.suggest(email),
+ email_valid: true ,
+ auth_provider: oauth2_provider
+ }
+
+ session[:authentication] = {
+ oauth2: {
+ provider: oauth2_provider,
+ uid: oauth2_uid,
+ },
+ name: name,
+ email: @data[:email],
+ email_valid: @data[:email_valid]
+ }
+ end
+ end
+
def create_or_sign_on_user_using_openid(auth_token)
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 681b4e9fef3..7addd1014fc 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -432,6 +432,16 @@ class UsersController < ApplicationController
github_user_id: auth[:github_user_id]
)
end
+
+ if oauth2_auth?(auth)
+ Oauth2UserInfo.create(
+ uid: auth[:oauth2][:uid],
+ provider: auth[:oauth2][:provider],
+ name: auth[:name],
+ email: auth[:email],
+ user_id: user.id
+ )
+ end
end
def twitter_auth?(auth)
@@ -448,4 +458,9 @@ class UsersController < ApplicationController
auth[:github_user_id] && auth[:github_screen_name] &&
GithubUserInfo.find_by_github_user_id(auth[:github_user_id]).nil?
end
+
+ def oauth2_auth?(auth)
+ auth[:oauth2].is_a?(Hash) && auth[:oauth2][:provider] && auth[:oauth2][:uid] &&
+ Oauth2UserInfo.where(provider: auth[:oauth2][:provider], uid: auth[:oauth2][:uid]).empty?
+ end
end
diff --git a/app/models/category.rb b/app/models/category.rb
index 853d94ddfde..942fa06a5aa 100644
--- a/app/models/category.rb
+++ b/app/models/category.rb
@@ -1,9 +1,17 @@
class Category < ActiveRecord::Base
belongs_to :topic, dependent: :destroy
- belongs_to :topic_only_relative_url,
+ if rails4?
+ belongs_to :topic_only_relative_url,
+ -> { select "id, title, slug" },
+ class_name: "Topic",
+ foreign_key: "topic_id"
+ else
+ belongs_to :topic_only_relative_url,
select: "id, title, slug",
class_name: "Topic",
foreign_key: "topic_id"
+ end
+
belongs_to :user
has_many :topics
diff --git a/app/models/oauth2_user_info.rb b/app/models/oauth2_user_info.rb
new file mode 100644
index 00000000000..048f05b781c
--- /dev/null
+++ b/app/models/oauth2_user_info.rb
@@ -0,0 +1,4 @@
+class Oauth2UserInfo < ActiveRecord::Base
+ belongs_to :user
+
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index b014563c42f..b9cb05f5961 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -34,6 +34,7 @@ class User < ActiveRecord::Base
has_one :twitter_user_info, dependent: :destroy
has_one :github_user_info, dependent: :destroy
has_one :cas_user_info, dependent: :destroy
+ has_one :oauth2_user_info, dependent: :destroy
belongs_to :approved_by, class_name: 'User'
has_many :group_users
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 91c9a5cb005..d6d2b2cad52 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -25,6 +25,14 @@ Rails.application.config.middleware.use OmniAuth::Builder do
:store => OpenID::Store::Redis.new($redis),
:require => "omniauth-openid"
}.merge(p.options)
+ elsif p.type == :oauth2
+ provider :oauth2,
+ p.options[:client_id],
+ p.options[:client_secret],
+ {
+ :name => p.name,
+ :require => "omniauth-oauth2"
+ }.merge(p.options)
end
end
diff --git a/db/migrate/20130816024250_create_oauth2_user_infos.rb b/db/migrate/20130816024250_create_oauth2_user_infos.rb
new file mode 100644
index 00000000000..d5a392fb61d
--- /dev/null
+++ b/db/migrate/20130816024250_create_oauth2_user_infos.rb
@@ -0,0 +1,14 @@
+class CreateOauth2UserInfos < ActiveRecord::Migration
+ def change
+ create_table :oauth2_user_infos do |t|
+ t.integer :user_id, null: false
+ t.string :uid, null: false
+ t.string :provider, null: false
+ t.string :email
+ t.string :name
+ t.timestamps
+ end
+
+ add_index :oauth2_user_infos, [:uid, :provider], unique: true
+ end
+end
diff --git a/lib/freedom_patches/active_record_relation.rb b/lib/freedom_patches/active_record_relation.rb
new file mode 100644
index 00000000000..ab5cb77eafa
--- /dev/null
+++ b/lib/freedom_patches/active_record_relation.rb
@@ -0,0 +1,14 @@
+unless Rails.version =~ /^4/
+ module ActiveRecord
+ class Relation
+ # Patch Rails 3 ActiveRecord::Relation to noop on Rails 4 references
+ # thereby getting code that works for rails 3 and 4 without
+ # deprecation warnings
+
+ def references(*args)
+ self
+ end
+
+ end
+ end
+end
diff --git a/lib/search.rb b/lib/search.rb
index 3d6b7001660..0d356323425 100644
--- a/lib/search.rb
+++ b/lib/search.rb
@@ -118,9 +118,7 @@ class Search
.order("topics_month DESC")
.secured(@guardian)
.limit(@limit)
- if rails4?
- categories = categories.references(:category_search_data)
- end
+ .references(:category_search_data)
categories.each do |c|
@results.add_result(SearchResult.from_category(c))
@@ -133,9 +131,7 @@ class Search
.order("CASE WHEN username_lower = '#{@original_term.downcase}' THEN 0 ELSE 1 END")
.order("last_posted_at DESC")
.limit(@limit)
- if rails4?
- users = users.references(:user_search_data)
- end
+ .references(:user_search_data)
users.each do |u|
@results.add_result(SearchResult.from_user(u))
@@ -148,10 +144,7 @@ class Search
.where("topics.deleted_at" => nil)
.where("topics.visible")
.where("topics.archetype <> ?", Archetype.private_message)
-
- if rails4?
- posts = posts.references(:post_search_data, {:topic => :category})
- end
+ .references(:post_search_data, {:topic => :category})
# If we have a search context, prioritize those posts first
if @search_context.present?
diff --git a/lib/topic_query.rb b/lib/topic_query.rb
index 58d6abef0fc..571f49fcd6e 100644
--- a/lib/topic_query.rb
+++ b/lib/topic_query.rb
@@ -212,23 +212,23 @@ class TopicQuery
end
result = result.listable_topics.includes(category: :topic_only_relative_url)
- result = result.where('categories.name is null or categories.name <> ?', options[:exclude_category]) if options[:exclude_category]
- result = result.where('categories.name = ?', options[:only_category]) if options[:only_category]
+ result = result.where('categories.name is null or categories.name <> ?', options[:exclude_category]).references(:categories) if options[:exclude_category]
+ result = result.where('categories.name = ?', options[:only_category]).references(:categories) if options[:only_category]
result = result.limit(options[:per_page]) unless options[:limit] == false
result = result.visible if options[:visible] || @user.nil? || @user.regular?
- result = result.where('topics.id <> ?', options[:except_topic_id]) if options[:except_topic_id]
+ result = result.where('topics.id <> ?', options[:except_topic_id]).references(:topics) if options[:except_topic_id]
result = result.offset(options[:page].to_i * options[:per_page]) if options[:page]
if options[:topic_ids]
- result = result.where('topics.id in (?)', options[:topic_ids])
+ result = result.where('topics.id in (?)', options[:topic_ids]).references(:topics)
end
unless @user && @user.moderator?
category_ids = @user.secure_category_ids if @user
if category_ids.present?
- result = result.where('categories.read_restricted IS NULL OR categories.read_restricted = ? OR categories.id IN (?)', false, category_ids)
+ result = result.where('categories.read_restricted IS NULL OR categories.read_restricted = ? OR categories.id IN (?)', false, category_ids).references(:categories)
else
- result = result.where('categories.read_restricted IS NULL OR categories.read_restricted = ?', false)
+ result = result.where('categories.read_restricted IS NULL OR categories.read_restricted = ?', false).references(:categories)
end
end
diff --git a/lib/trashable.rb b/lib/trashable.rb
index ef6fd724373..5d0e74b2850 100644
--- a/lib/trashable.rb
+++ b/lib/trashable.rb
@@ -2,7 +2,7 @@ module Trashable
extend ActiveSupport::Concern
included do
- default_scope where(with_deleted_scope_sql)
+ default_scope { where(with_deleted_scope_sql) }
# scope unscoped does not work
belongs_to :deleted_by, class_name: 'User'
@@ -15,11 +15,8 @@ module Trashable
#
# with this in place Post.limit(10).with_deleted, will work as expected
#
- if rails4?
- scope = self.all.with_default_scope
- else
- scope = self.scoped.with_default_scope
- end
+ scope = rails4? ? self.all.with_default_scope : self.scoped.with_default_scope
+
scope.where_values.delete(with_deleted_scope_sql)
scope
end
diff --git a/plugins/.gitkeep b/plugins/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index a7c41f18f9c..966fbef70b0 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -164,4 +164,25 @@ describe Users::OmniauthCallbacksController do
end
+ describe 'oauth2' do
+ before do
+ Discourse.stubs(:auth_providers).returns([stub(name: 'my_oauth2_provider', type: :oauth2)])
+ request.env["omniauth.auth"] = { uid: 'my-uid', provider: 'my-oauth-provider-domain.net', info: {email: 'eviltrout@made.up.email', name: 'Chatanooga'}}
+ end
+
+ describe "#create_or_sign_on_user_using_oauth2" do
+ context "User already exists" do
+ before do
+ User.stubs(:find_by_email).returns(Fabricate(:user))
+ end
+
+ it "should create an OauthUserInfo" do
+ expect {
+ post :complete, provider: 'my_oauth2_provider'
+ }.to change { Oauth2UserInfo.count }.by(1)
+ end
+ end
+ end
+ end
+
end