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