diff --git a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6
index 8cf32d9cc64..97cab16b0db 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6
@@ -31,7 +31,8 @@ export default Ember.Controller.extend(PreferencesTabController, {
"disable_jump_reply",
"automatically_unpin_topics",
"allow_private_messages",
- "homepage_id"
+ "homepage_id",
+ "hide_profile_and_presence"
];
if (makeDefault) {
diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6
index 96b2a333f23..80162bff88c 100644
--- a/app/assets/javascripts/discourse/controllers/user.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user.js.es6
@@ -16,9 +16,9 @@ export default Ember.Controller.extend(CanCheckEmails, {
return currentUser && username === currentUser.get("username");
},
- @computed("viewingSelf")
- canExpandProfile(viewingSelf) {
- return viewingSelf;
+ @computed("viewingSelf", "model.profile_hidden")
+ canExpandProfile(viewingSelf, profileHidden) {
+ return !profileHidden && viewingSelf;
},
@computed("model.profileBackground")
@@ -26,8 +26,11 @@ export default Ember.Controller.extend(CanCheckEmails, {
return !Ember.isEmpty(background.toString());
},
- @computed("indexStream", "viewingSelf", "forceExpand")
- collapsedInfo(indexStream, viewingSelf, forceExpand) {
+ @computed("model.profile_hidden", "indexStream", "viewingSelf", "forceExpand")
+ collapsedInfo(profileHidden, indexStream, viewingSelf, forceExpand) {
+ if (profileHidden) {
+ return true;
+ }
return (!indexStream || viewingSelf) && !forceExpand;
},
diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index b3e91306d34..acb2ad2ff41 100644
--- a/app/assets/javascripts/discourse/models/user.js.es6
+++ b/app/assets/javascripts/discourse/models/user.js.es6
@@ -284,7 +284,8 @@ const User = RestModel.extend({
"include_tl0_in_digests",
"theme_ids",
"allow_private_messages",
- "homepage_id"
+ "homepage_id",
+ "hide_profile_and_presence"
];
if (fields) {
diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
index df489fa5a12..30dd8fc7552 100644
--- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6
+++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
@@ -102,6 +102,7 @@ export default function() {
"user",
{ path: "/u/:username", resetNamespace: true },
function() {
+ this.route("profile-hidden");
this.route("summary");
this.route(
"userActivity",
diff --git a/app/assets/javascripts/discourse/routes/user-activity.js.es6 b/app/assets/javascripts/discourse/routes/user-activity.js.es6
index 546e135cbef..a827e3d9988 100644
--- a/app/assets/javascripts/discourse/routes/user-activity.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-activity.js.es6
@@ -1,6 +1,11 @@
export default Discourse.Route.extend({
model() {
- return this.modelFor("user");
+ let user = this.modelFor("user");
+ if (user.get("profile_hidden")) {
+ return this.replaceWith("user.profile-hidden");
+ }
+
+ return user;
},
setupController(controller, user) {
diff --git a/app/assets/javascripts/discourse/routes/user-summary.js.es6 b/app/assets/javascripts/discourse/routes/user-summary.js.es6
index 39d4571973d..95db3c3ae1f 100644
--- a/app/assets/javascripts/discourse/routes/user-summary.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-summary.js.es6
@@ -2,7 +2,12 @@ export default Discourse.Route.extend({
showFooter: true,
model() {
- return this.modelFor("user").summary();
+ let user = this.modelFor("user");
+ if (user.get("profile_hidden")) {
+ return this.replaceWith("user.profile-hidden");
+ }
+
+ return user.summary();
},
actions: {
diff --git a/app/assets/javascripts/discourse/templates/preferences/interface.hbs b/app/assets/javascripts/discourse/templates/preferences/interface.hbs
index 968737b6b60..da84c8c605a 100644
--- a/app/assets/javascripts/discourse/templates/preferences/interface.hbs
+++ b/app/assets/javascripts/discourse/templates/preferences/interface.hbs
@@ -37,10 +37,10 @@
{{preference-checkbox labelKey="user.enable_quoting" checked=model.user_option.enable_quoting}}
{{preference-checkbox labelKey="user.dynamic_favicon" checked=model.user_option.dynamic_favicon}}
{{preference-checkbox labelKey="user.disable_jump_reply" checked=model.user_option.disable_jump_reply}}
-
{{#if siteSettings.automatically_unpin_topics}}
{{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}}
{{/if}}
+ {{preference-checkbox labelKey="user.hide_profile_and_presence" checked=model.user_option.hide_profile_and_presence}}
{{plugin-outlet name="user-preferences-interface" args=(hash model=model save=(action "save"))}}
diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs
index 9cc4c27311c..fbbb9d1a660 100644
--- a/app/assets/javascripts/discourse/templates/user.hbs
+++ b/app/assets/javascripts/discourse/templates/user.hbs
@@ -195,8 +195,10 @@
{{#mobile-nav class='main-nav' desktopClass="nav nav-pills user-nav" currentPath=currentPath}}
-
{{#link-to 'user.summary'}}{{i18n 'user.summary.title'}}{{/link-to}}
- {{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}
+ {{#unless model.profile_hidden}}
+ {{#link-to 'user.summary'}}{{i18n 'user.summary.title'}}{{/link-to}}
+ {{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}
+ {{/unless}}
{{#if showNotificationsTab}}
{{#link-to 'userNotifications'}}
diff --git a/app/assets/javascripts/discourse/templates/user/profile-hidden.hbs b/app/assets/javascripts/discourse/templates/user/profile-hidden.hbs
new file mode 100644
index 00000000000..680e87157d5
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/user/profile-hidden.hbs
@@ -0,0 +1 @@
+{{i18n "user.profile_hidden"}}
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index c9470628d29..bb6eb683412 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -41,6 +41,10 @@
}
}
+.user-profile-hidden {
+ font-size: 1.5em;
+ text-align: center;
+}
.user-navigation {
display: table-cell;
vertical-align: top;
diff --git a/app/controllers/user_actions_controller.rb b/app/controllers/user_actions_controller.rb
index 417147ac5e0..d72b28d49f4 100644
--- a/app/controllers/user_actions_controller.rb
+++ b/app/controllers/user_actions_controller.rb
@@ -7,6 +7,8 @@ class UserActionsController < ApplicationController
per_chunk = 30
user = fetch_user_from_params(include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts))
+ raise Discourse::NotFound unless guardian.can_see_profile?(user)
+
action_types = (params[:filter] || "").split(",").map(&:to_i)
opts = { user_id: user.id,
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 12ada62ff06..c72ae0864c4 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -53,14 +53,18 @@ class UsersController < ApplicationController
include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts)
)
- user_serializer = UserSerializer.new(@user, scope: guardian, root: 'user')
+ user_serializer = nil
+ if guardian.can_see_profile?(@user)
+ user_serializer = UserSerializer.new(@user, scope: guardian, root: 'user')
+ # TODO remove this options from serializer
+ user_serializer.omit_stats = true
- # TODO remove this options from serializer
- user_serializer.omit_stats = true
-
- topic_id = params[:include_post_count_for].to_i
- if topic_id != 0
- user_serializer.topic_post_count = { topic_id => Post.secured(guardian).where(topic_id: topic_id, user_id: @user.id).count }
+ topic_id = params[:include_post_count_for].to_i
+ if topic_id != 0
+ user_serializer.topic_post_count = { topic_id => Post.secured(guardian).where(topic_id: topic_id, user_id: @user.id).count }
+ end
+ else
+ user_serializer = HiddenProfileSerializer.new(@user, scope: guardian, root: 'user')
end
if !params[:skip_track_visit] && (@user != current_user)
@@ -204,8 +208,15 @@ class UsersController < ApplicationController
end
end
+ def profile_hidden
+ render nothing: true
+ end
+
def summary
user = fetch_user_from_params(include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts))
+
+ raise Discourse::NotFound unless guardian.can_see_profile?(user)
+
summary = UserSummary.new(user, guardian)
serializer = UserSummarySerializer.new(summary, scope: guardian)
render_json_dump(serializer)
diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb
index 2ddfe2533ee..d7c683ddb82 100644
--- a/app/serializers/current_user_serializer.rb
+++ b/app/serializers/current_user_serializer.rb
@@ -42,7 +42,8 @@ class CurrentUserSerializer < BasicUserSerializer
:can_create_topic,
:link_posting_access,
:external_id,
- :top_category_ids
+ :top_category_ids,
+ :hide_profile_and_presence
def link_posting_access
scope.link_posting_access
@@ -68,6 +69,10 @@ class CurrentUserSerializer < BasicUserSerializer
object.user_stat.topic_reply_count
end
+ def hide_profile_and_presence
+ object.user_option.hide_profile_and_presence
+ end
+
def enable_quoting
object.user_option.enable_quoting
end
diff --git a/app/serializers/hidden_profile_serializer.rb b/app/serializers/hidden_profile_serializer.rb
new file mode 100644
index 00000000000..50075aee591
--- /dev/null
+++ b/app/serializers/hidden_profile_serializer.rb
@@ -0,0 +1,7 @@
+class HiddenProfileSerializer < BasicUserSerializer
+ attributes :profile_hidden?
+
+ def profile_hidden?
+ true
+ end
+end
diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb
index 23b86fddd2a..9df725e84cb 100644
--- a/app/serializers/user_option_serializer.rb
+++ b/app/serializers/user_option_serializer.rb
@@ -23,6 +23,7 @@ class UserOptionSerializer < ApplicationSerializer
:theme_key_seq,
:allow_private_messages,
:homepage_id,
+ :hide_profile_and_presence
def auto_track_topics_after_msecs
object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs
diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb
index 8724bc5b906..755844f0ffe 100644
--- a/app/services/user_updater.rb
+++ b/app/services/user_updater.rb
@@ -37,6 +37,7 @@ class UserUpdater
:theme_ids,
:allow_private_messages,
:homepage_id,
+ :hide_profile_and_presence
]
def initialize(actor, user)
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 7ced35f7812..63e17921742 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -618,6 +618,7 @@ en:
private_messages: "Messages"
activity_stream: "Activity"
preferences: "Preferences"
+ profile_hidden: "This user's public profile is hidden."
expand_profile: "Expand"
collapse_profile: "Collapse"
bookmarks: "Bookmarks"
@@ -887,6 +888,7 @@ en:
website: "Web Site"
email_settings: "Email"
+ hide_profile_and_presence: "Hide my public profile and presence features"
like_notification_frequency:
title: "Notify when liked"
always: "Always"
diff --git a/config/routes.rb b/config/routes.rb
index d1d9f36575a..11a6106c46f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -432,6 +432,7 @@ Discourse::Application.routes.draw do
get "#{root_path}/:username/flagged-posts" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/deleted-posts" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: { username: RouteFormat.username }
+ get "#{root_path}/:username/profile-hidden" => "users#profile_hidden"
end
get "user-badges/:username.json" => "user_badges#username", constraints: { username: RouteFormat.username }, defaults: { format: :json }
diff --git a/db/migrate/20181010150631_add_hide_profile_and_presence_to_user_options.rb b/db/migrate/20181010150631_add_hide_profile_and_presence_to_user_options.rb
new file mode 100644
index 00000000000..b5eef3b307c
--- /dev/null
+++ b/db/migrate/20181010150631_add_hide_profile_and_presence_to_user_options.rb
@@ -0,0 +1,5 @@
+class AddHideProfileAndPresenceToUserOptions < ActiveRecord::Migration[5.2]
+ def change
+ add_column :user_options, :hide_profile_and_presence, :boolean, default: false, null: false
+ end
+end
diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb
index 32dc6809084..aa502955fa7 100644
--- a/lib/guardian/user_guardian.rb
+++ b/lib/guardian/user_guardian.rb
@@ -93,4 +93,15 @@ module UserGuardian
user && can_administer_user?(user)
end
+ def can_see_profile?(user)
+ return false if user.blank?
+
+ # If a user has hidden their profile, restrict it to them and staff
+ if user.user_option.try(:hide_profile_and_presence?)
+ return is_me?(user) || is_staff?
+ end
+
+ true
+ end
+
end
diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6 b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6
index 431fec7d093..5c69c900998 100644
--- a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6
+++ b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6
@@ -113,6 +113,12 @@ export default Ember.Component.extend({
publish(data) {
this._lastPublish = new Date();
+
+ // Don't publish presence if disabled
+ if (this.currentUser.hide_profile_and_presence) {
+ return Ember.RSVP.Promise.resolve();
+ }
+
return ajax("/presence/publish", { type: "POST", data });
},
diff --git a/plugins/discourse-presence/plugin.rb b/plugins/discourse-presence/plugin.rb
index 2739eb9e2fe..02900e3f277 100644
--- a/plugins/discourse-presence/plugin.rb
+++ b/plugins/discourse-presence/plugin.rb
@@ -107,8 +107,7 @@ after_initialize do
ACTIONS ||= [-"edit", -"reply"].freeze
def publish
-
- raise Discourse::NotFound if !current_user
+ raise Discourse::NotFound if current_user.blank? || current_user.user_option.hide_profile_and_presence?
data = params.permit(
:response_needed,
diff --git a/plugins/discourse-presence/spec/requests/presence_controller_spec.rb b/plugins/discourse-presence/spec/requests/presence_controller_spec.rb
index 8a003a9e4e1..89e702021e9 100644
--- a/plugins/discourse-presence/spec/requests/presence_controller_spec.rb
+++ b/plugins/discourse-presence/spec/requests/presence_controller_spec.rb
@@ -37,6 +37,12 @@ describe ::Presence::PresencesController do
expect { post '/presence/publish.json' }.not_to raise_error
end
+ it "does not publish for users with disabled presence features" do
+ user1.user_option.update_column(:hide_profile_and_presence, true)
+ post '/presence/publish.json'
+ expect(response.code).to eq("404")
+ end
+
it "uses guardian to secure endpoint" do
private_post = Fabricate(:private_message_post)
diff --git a/spec/components/guardian/user_guardian_spec.rb b/spec/components/guardian/user_guardian_spec.rb
index 3281b140afb..248a81ff8e3 100644
--- a/spec/components/guardian/user_guardian_spec.rb
+++ b/spec/components/guardian/user_guardian_spec.rb
@@ -95,6 +95,41 @@ describe UserGuardian do
expect(guardian.can_pick_avatar?(user_avatar, nil)).to eq(true)
end
end
+ end
+ describe "#can_see_profile?" do
+
+ it "is false for no user" do
+ expect(Guardian.new.can_see_profile?(nil)).to eq(false)
+ end
+
+ it "is true for a user whose profile is public" do
+ expect(Guardian.new.can_see_profile?(user)).to eq(true)
+ end
+
+ context "hidden profile" do
+ let(:hidden_user) do
+ result = Fabricate(:user)
+ result.user_option.update_column(:hide_profile_and_presence, true)
+ result
+ end
+
+ it "is false for another user" do
+ expect(Guardian.new(user).can_see_profile?(hidden_user)).to eq(false)
+ end
+
+ it "is false for an anonymous user" do
+ expect(Guardian.new.can_see_profile?(hidden_user)).to eq(false)
+ end
+
+ it "is true for the user themselves" do
+ expect(Guardian.new(hidden_user).can_see_profile?(hidden_user)).to eq(true)
+ end
+
+ it "is true for a staff user" do
+ expect(Guardian.new(admin).can_see_profile?(hidden_user)).to eq(true)
+ end
+
+ end
end
end
diff --git a/spec/models/user_option_spec.rb b/spec/models/user_option_spec.rb
index 832de55276c..16b558d37fe 100644
--- a/spec/models/user_option_spec.rb
+++ b/spec/models/user_option_spec.rb
@@ -27,7 +27,14 @@ describe UserOption do
user.user_option.expects(:redirected_to_top).returns(nil)
expect(user.user_option.should_be_redirected_to_top).to eq(false)
end
+ end
+ describe "defaults" do
+ let(:user) { Fabricate(:user) }
+
+ it "should not hide the profile and presence by default" do
+ expect(user.user_option.hide_profile_and_presence).to eq(false)
+ end
end
describe "#mailing_list_mode" do
diff --git a/spec/requests/user_actions_controller_spec.rb b/spec/requests/user_actions_controller_spec.rb
index 3bae24ecfa6..471a66271ed 100644
--- a/spec/requests/user_actions_controller_spec.rb
+++ b/spec/requests/user_actions_controller_spec.rb
@@ -9,6 +9,15 @@ describe UserActionsController do
expect(response.status).to eq(400)
end
+ it "returns a 404 for a user with a hidden profile" do
+ UserActionCreator.enable
+ post = Fabricate(:post)
+ post.user.user_option.update_column(:hide_profile_and_presence, true)
+
+ get "/user_actions.json", params: { username: post.user.username }
+ expect(response.code).to eq("404")
+ end
+
it 'renders list correctly' do
UserActionCreator.enable
post = Fabricate(:post)
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index c7e76f97acb..f3bb8996b13 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -2171,6 +2171,14 @@ describe UsersController do
expect(json["user_summary"]["topic_count"]).to eq(1)
expect(json["user_summary"]["post_count"]).to eq(0)
end
+
+ it "returns 404 for a hidden profile" do
+ user = Fabricate(:user)
+ user.user_option.update_column(:hide_profile_and_presence, true)
+
+ get "/u/#{user.username_lower}/summary.json"
+ expect(response.status).to eq(404)
+ end
end
describe '#confirm_admin' do
@@ -2417,7 +2425,23 @@ describe UsersController do
it "returns success" do
get "/u/#{user.username}.json"
expect(response.status).to eq(200)
- expect(JSON.parse(response.body)["user"]["username"]).to eq(user.username)
+ parsed = JSON.parse(response.body)["user"]
+
+ expect(parsed['username']).to eq(user.username)
+ expect(parsed["profile_hidden"]).to be_blank
+ expect(parsed["trust_level"]).to be_present
+ end
+
+ it "returns a hidden profile" do
+ user.user_option.update_column(:hide_profile_and_presence, true)
+
+ get "/u/#{user.username}.json"
+ expect(response.status).to eq(200)
+ parsed = JSON.parse(response.body)["user"]
+
+ expect(parsed["username"]).to eq(user.username)
+ expect(parsed["profile_hidden"]).to eq(true)
+ expect(parsed["trust_level"]).to be_blank
end
it "should redirect to login page for anonymous user when profiles are hidden" do