diff --git a/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 index 910b8d661..e10c01c84 100644 --- a/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 @@ -56,6 +56,13 @@ export default Ember.Component.extend({ }); }, + @computed() + showUserDirectoryLink() { + if (!this.siteSettings.enable_user_directory) return false; + if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) return false; + return true; + }, + actions: { keyboardShortcuts() { this.sendAction('showKeyboardAction'); diff --git a/app/assets/javascripts/discourse/controllers/user-card.js.es6 b/app/assets/javascripts/discourse/controllers/user-card.js.es6 index ce2e065a8..c67125cdb 100644 --- a/app/assets/javascripts/discourse/controllers/user-card.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-card.js.es6 @@ -39,6 +39,11 @@ export default Ember.Controller.extend({ // XSS protection (should be encapsulated) username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, ""); + // No user card for anon + if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) { + return; + } + // Don't show on mobile if (Discourse.Mobile.mobileView) { const url = "/users/" + username; diff --git a/app/assets/javascripts/discourse/routes/user.js.es6 b/app/assets/javascripts/discourse/routes/user.js.es6 index 7fb8fffe6..040f949b5 100644 --- a/app/assets/javascripts/discourse/routes/user.js.es6 +++ b/app/assets/javascripts/discourse/routes/user.js.es6 @@ -33,6 +33,12 @@ export default Discourse.Route.extend({ } }, + beforeModel() { + if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) { + this.replaceWith("discovery"); + } + }, + model(params) { // If we're viewing the currently logged in user, return that object instead const currentUser = this.currentUser; diff --git a/app/assets/javascripts/discourse/routes/users.js.es6 b/app/assets/javascripts/discourse/routes/users.js.es6 index beb25276f..ab3cbe320 100644 --- a/app/assets/javascripts/discourse/routes/users.js.es6 +++ b/app/assets/javascripts/discourse/routes/users.js.es6 @@ -23,6 +23,12 @@ export default Discourse.Route.extend({ } }, + beforeModel() { + if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) { + this.replaceWith("discovery"); + } + }, + model(params) { // If we refresh via `refreshModel` set the old model to loading this._params = params; diff --git a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs index 0deea8e1a..3d875eb60 100644 --- a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs +++ b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs @@ -57,7 +57,7 @@
  • {{d-link route="badges" class="badge-link" label="badges.title"}}
  • {{/if}} - {{#if siteSettings.enable_user_directory}} + {{#if showUserDirectoryLink}}
  • {{d-link route="users" class="user-directory-link" label="directory.title"}}
  • {{/if}} diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 12abf12a9..1c2be6d1b 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -29,6 +29,8 @@ class UsersController < ApplicationController end def show + raise Discourse::InvalidAccess if SiteSetting.hide_user_profiles_from_public && !current_user + @user = fetch_user_from_params user_serializer = UserSerializer.new(@user, scope: guardian, root: 'user') if params[:stats].to_s == "false" @@ -162,7 +164,6 @@ class UsersController < ApplicationController end def my_redirect - raise Discourse::NotFound if params[:path] !~ /^[a-z\-\/]+$/ if current_user.blank? diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index dd4240c33..c5784bd24 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1162,6 +1162,8 @@ en: anonymous_posting_min_trust_level: "Minimum trust level required to enable anonymous posting" anonymous_account_duration_minutes: "To protect anonymity create a new anonymous account every N minutes for each user. Example: if set to 600, as soon as 600 minutes elapse from last post AND user switches to anon, a new anonymous account is created." + hide_user_profiles_from_public: "Disable user cards, user profiles and user directory for anonymous users." + allow_profile_backgrounds: "Allow users to upload profile backgrounds." sequential_replies_threshold: "Number posts a user has to make in a row in a topic before being reminded about too many sequential replies. " diff --git a/config/site_settings.yml b/config/site_settings.yml index 73c801b2c..149713872 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -342,6 +342,9 @@ users: client: true anonymous_account_duration_minutes: default: 10080 + hide_user_profiles_from_public: + default: false + client: true posting: min_post_length: diff --git a/lib/search.rb b/lib/search.rb index e99e60361..4c60c318f 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -379,6 +379,8 @@ class Search end def user_search + return if SiteSetting.hide_user_profiles_from_public && !@guardian.user + users = User.includes(:user_search_data) .where("active = true AND user_search_data.search_data @@ #{ts_query("simple")}") .order("CASE WHEN username_lower = '#{@original_term.downcase}' THEN 0 ELSE 1 END") diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index 462b11911..093aeb766 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -85,6 +85,21 @@ describe Search do expect(result.users.length).to eq(1) expect(result.users[0].id).to eq(user.id) end + + context 'hiding user profiles' do + before { SiteSetting.stubs(:hide_user_profiles_from_public).returns(true) } + + it 'returns no result for anon' do + expect(result.users.length).to eq(0) + end + + it 'returns a result for logged in users' do + result = Search.execute('bruce', type_filter: 'user', guardian: Guardian.new(user)) + expect(result.users.length).to eq(1) + end + + end + end context 'inactive users' do @@ -119,7 +134,6 @@ describe Search do TopicAllowedUser.create!(user_id: reply.user_id, topic_id: topic.id) TopicAllowedUser.create!(user_id: post.user_id, topic_id: topic.id) - results = Search.execute('mars', type_filter: 'private_messages', guardian: Guardian.new(reply.user)) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index e693da022..fb49b404c 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -3,71 +3,93 @@ require 'spec_helper' describe UsersController do describe '.show' do - let(:user) { log_in } - it 'returns success' do - xhr :get, :show, username: user.username, format: :json - expect(response).to be_success - json = JSON.parse(response.body) + context "anon" do - expect(json["user"]["has_title_badges"]).to eq(false) + let(:user) { Discourse.system_user } - end - - it "returns not found when the username doesn't exist" do - xhr :get, :show, username: 'madeuppity' - expect(response).not_to be_success - end - - it 'returns not found when the user is inactive' do - inactive = Fabricate(:user, active: false) - xhr :get, :show, username: inactive.username - expect(response).not_to be_success - end - - it "raises an error on invalid access" do - Guardian.any_instance.expects(:can_see?).with(user).returns(false) - xhr :get, :show, username: user.username - expect(response).to be_forbidden - end - - describe "user profile views" do - let(:other_user) { Fabricate(:user) } - - it "should track a user profile view for a signed in user" do - UserProfileView.expects(:add).with(other_user.user_profile.id, request.remote_ip, user.id) - xhr :get, :show, username: other_user.username - end - - it "should not track a user profile view for a user viewing his own profile" do - UserProfileView.expects(:add).never - xhr :get, :show, username: user.username - end - - it "should track a user profile view for an anon user" do - UserProfileView.expects(:add).with(other_user.user_profile.id, request.remote_ip, nil) - xhr :get, :show, username: other_user.username - end - - it "skips tracking" do - UserProfileView.expects(:add).never - xhr :get, :show, { username: user.username, skip_track_visit: true } - end - end - - context "fetching a user by external_id" do - before { user.create_single_sign_on_record(external_id: '997', last_payload: '') } - - it "returns fetch for a matching external_id" do - xhr :get, :show, external_id: '997' + it "returns success" do + xhr :get, :show, username: user.username, format: :json expect(response).to be_success end - it "returns not found when external_id doesn't match" do - xhr :get, :show, external_id: '99' + it "raises an error for anon when profiles are hidden" do + SiteSetting.stubs(:hide_user_profiles_from_public).returns(true) + xhr :get, :show, username: user.username, format: :json expect(response).not_to be_success end + end + + context "logged in" do + + let(:user) { log_in } + + it 'returns success' do + xhr :get, :show, username: user.username, format: :json + expect(response).to be_success + json = JSON.parse(response.body) + + expect(json["user"]["has_title_badges"]).to eq(false) + end + + it "returns not found when the username doesn't exist" do + xhr :get, :show, username: 'madeuppity' + expect(response).not_to be_success + end + + it 'returns not found when the user is inactive' do + inactive = Fabricate(:user, active: false) + xhr :get, :show, username: inactive.username + expect(response).not_to be_success + end + + it "raises an error on invalid access" do + Guardian.any_instance.expects(:can_see?).with(user).returns(false) + xhr :get, :show, username: user.username + expect(response).to be_forbidden + end + + describe "user profile views" do + let(:other_user) { Fabricate(:user) } + + it "should track a user profile view for a signed in user" do + UserProfileView.expects(:add).with(other_user.user_profile.id, request.remote_ip, user.id) + xhr :get, :show, username: other_user.username + end + + it "should not track a user profile view for a user viewing his own profile" do + UserProfileView.expects(:add).never + xhr :get, :show, username: user.username + end + + it "should track a user profile view for an anon user" do + UserProfileView.expects(:add).with(other_user.user_profile.id, request.remote_ip, nil) + xhr :get, :show, username: other_user.username + end + + it "skips tracking" do + UserProfileView.expects(:add).never + xhr :get, :show, { username: user.username, skip_track_visit: true } + end + end + + context "fetching a user by external_id" do + before { user.create_single_sign_on_record(external_id: '997', last_payload: '') } + + it "returns fetch for a matching external_id" do + xhr :get, :show, external_id: '997' + expect(response).to be_success + end + + it "returns not found when external_id doesn't match" do + xhr :get, :show, external_id: '99' + expect(response).not_to be_success + end + end + + end + end describe '.user_preferences_redirect' do