diff --git a/app/assets/javascripts/discourse/app/lib/transform-post.js b/app/assets/javascripts/discourse/app/lib/transform-post.js index 89ec81ecbf5..689cfd0daeb 100644 --- a/app/assets/javascripts/discourse/app/lib/transform-post.js +++ b/app/assets/javascripts/discourse/app/lib/transform-post.js @@ -57,6 +57,7 @@ export function transformBasicPost(post) { canPermanentlyDelete: false, showFlagDelete: false, canRecover: post.can_recover, + canSeeHiddenPost: post.can_see_hidden_post, canEdit: post.can_edit, canFlag: !post.get("topic.deleted") && !isEmpty(post.get("flagsAvailable")), canReviewTopic: false, diff --git a/app/assets/javascripts/discourse/app/widgets/post.js b/app/assets/javascripts/discourse/app/widgets/post.js index 1aa59ae5885..ff04f2fbda7 100644 --- a/app/assets/javascripts/discourse/app/widgets/post.js +++ b/app/assets/javascripts/discourse/app/widgets/post.js @@ -497,10 +497,7 @@ createWidget("post-contents", { result = result.concat(applyDecorators(this, "after-cooked", attrs, state)); - if ( - attrs.cooked_hidden && - (this.currentUser?.isLeader || attrs.user_id === this.currentUser?.id) - ) { + if (attrs.cooked_hidden && attrs.canSeeHiddenPost) { result.push(this.attach("expand-hidden", attrs)); } diff --git a/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js index 6f7ca2c89a5..27be61e0c6c 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js @@ -495,7 +495,7 @@ module("Integration | Component | Widget | post", function (hooks) { }); test("cooked content hidden", async function (assert) { - this.set("args", { cooked_hidden: true }); + this.set("args", { cooked_hidden: true, canSeeHiddenPost: true }); this.set("expandHidden", () => (this.unhidden = true)); await render(hbs` @@ -506,6 +506,17 @@ module("Integration | Component | Widget | post", function (hooks) { assert.ok(this.unhidden, "triggers the action"); }); + test(`cooked content hidden - can't view hidden post`, async function (assert) { + this.set("args", { cooked_hidden: true, canSeeHiddenPost: false }); + this.set("expandHidden", () => (this.unhidden = true)); + + await render(hbs` + + `); + + assert.ok(!exists(".topic-body .expand-hidden"), "button is not displayed"); + }); + test("expand first post", async function (assert) { const store = getOwner(this).lookup("service:store"); this.set("args", { expandablePost: true }); diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index a35ea4b8ca4..e2a849d8704 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -43,6 +43,7 @@ class PostSerializer < BasicPostSerializer :can_delete, :can_permanently_delete, :can_recover, + :can_see_hidden_post, :can_wiki, :link_counts, :read, @@ -180,6 +181,10 @@ class PostSerializer < BasicPostSerializer scope.can_recover_post?(object) end + def can_see_hidden_post + scope.can_see_hidden_post?(object) + end + def can_wiki scope.can_wiki?(object) end diff --git a/app/serializers/web_hook_post_serializer.rb b/app/serializers/web_hook_post_serializer.rb index f804e00833e..c4b737703d2 100644 --- a/app/serializers/web_hook_post_serializer.rb +++ b/app/serializers/web_hook_post_serializer.rb @@ -20,6 +20,7 @@ class WebHookPostSerializer < PostSerializer can_edit can_delete can_recover + can_see_hidden_post can_wiki actions_summary can_view_edit_history diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 0a801ca3d0d..0ff51291927 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1700,6 +1700,7 @@ en: enable_badges: "Enable the badge system" max_favorite_badges: "Maximum number of badges that user can select" whispers_allowed_groups: "Allow private communication within topics for members of specified groups." + hidden_post_visible_groups: "Allow members of these groups to view hidden posts. Staff users can always view hidden posts." allow_index_in_robots_txt: "Specify in robots.txt that this site is allowed to be indexed by web search engines. In exceptional cases you can permanently override robots.txt." blocked_email_domains: "A pipe-delimited list of email domains that users are not allowed to register accounts with. Subdomains are automatically handled for the specified domains. Wildcard symbols * and ? are not supported. Example: mailinator.com|trashmail.net" diff --git a/config/site_settings.yml b/config/site_settings.yml index 5cc215c0e27..ebe289ba9e1 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -336,6 +336,12 @@ basic: default: "" allow_any: false refresh: true + hidden_post_visible_groups: + type: group_list + list_type: compact + default: "14" + allow_any: false + refresh: true enable_bookmarks_with_reminders: client: true default: true diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index a72b4befb45..6b485569571 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -284,8 +284,12 @@ module PostGuardian end def can_see_hidden_post?(post) + if SiteSetting.hidden_post_visible_groups_map.include?(Group::AUTO_GROUPS[:everyone]) + return true + end return false if anonymous? - post.user_id == @user.id || @user.has_trust_level_or_staff?(TrustLevel[4]) + return true if is_staff? + post.user_id == @user.id || @user.in_any_groups?(SiteSetting.hidden_post_visible_groups_map) end def can_view_edit_history?(post) diff --git a/spec/lib/guardian/post_guardian_spec.rb b/spec/lib/guardian/post_guardian_spec.rb index 713886a6f3b..c67579bd328 100644 --- a/spec/lib/guardian/post_guardian_spec.rb +++ b/spec/lib/guardian/post_guardian_spec.rb @@ -1,32 +1,59 @@ # frozen_string_literal: true RSpec.describe PostGuardian do + fab!(:groupless_user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) } fab!(:anon) { Fabricate(:anonymous) } fab!(:admin) { Fabricate(:admin) } - fab!(:tl3_user) { Fabricate(:trust_level_3) } - fab!(:tl4_user) { Fabricate(:trust_level_4) } fab!(:moderator) { Fabricate(:moderator) } + fab!(:group) { Fabricate(:group) } + fab!(:group_user) { Fabricate(:group_user, group: group, user: user) } fab!(:category) { Fabricate(:category) } fab!(:topic) { Fabricate(:topic, category: category) } fab!(:hidden_post) { Fabricate(:post, topic: topic, hidden: true) } describe "#can_see_hidden_post?" do - it "returns false for anonymous users" do - expect(Guardian.new(anon).can_see_hidden_post?(hidden_post)).to eq(false) + context "when the hidden_post_visible_groups contains everyone" do + before { SiteSetting.hidden_post_visible_groups = "#{Group::AUTO_GROUPS[:everyone]}" } + + it "returns true for everyone" do + expect(Guardian.new(anon).can_see_hidden_post?(hidden_post)).to eq(true) + expect(Guardian.new(user).can_see_hidden_post?(hidden_post)).to eq(true) + expect(Guardian.new(admin).can_see_hidden_post?(hidden_post)).to eq(true) + expect(Guardian.new(moderator).can_see_hidden_post?(hidden_post)).to eq(true) + end end - it "returns false for TL3 users" do - expect(Guardian.new(tl3_user).can_see_hidden_post?(hidden_post)).to eq(false) + context "when the post is a created by the user" do + fab!(:hidden_post) { Fabricate(:post, topic: topic, hidden: true, user: user) } + + before { SiteSetting.hidden_post_visible_groups = "" } + + it "returns true for the author" do + SiteSetting.hidden_post_visible_groups = "" + expect(Guardian.new(user).can_see_hidden_post?(hidden_post)).to eq(true) + end end - it "returns true for TL4 users" do - expect(Guardian.new(tl4_user).can_see_hidden_post?(hidden_post)).to eq(true) - end + context "when the post is a created by another user" do + before { SiteSetting.hidden_post_visible_groups = "14|#{group.id}" } - it "returns true for staff users" do - expect(Guardian.new(moderator).can_see_hidden_post?(hidden_post)).to eq(true) - expect(Guardian.new(admin).can_see_hidden_post?(hidden_post)).to eq(true) + it "returns true for staff users" do + expect(Guardian.new(admin).can_see_hidden_post?(hidden_post)).to eq(true) + expect(Guardian.new(moderator).can_see_hidden_post?(hidden_post)).to eq(true) + end + + it "returns false for anonymous users" do + expect(Guardian.new(anon).can_see_hidden_post?(hidden_post)).to eq(false) + end + + it "returns true if the user is in hidden_post_visible_groups" do + expect(Guardian.new(user).can_see_hidden_post?(hidden_post)).to eq(true) + end + + it "returns false if the user is not in hidden_post_visible_groups" do + expect(Guardian.new(groupless_user).can_see_hidden_post?(hidden_post)).to eq(false) + end end end end diff --git a/spec/requests/api/posts_spec.rb b/spec/requests/api/posts_spec.rb index d5a2e5dd303..399079c25c3 100644 --- a/spec/requests/api/posts_spec.rb +++ b/spec/requests/api/posts_spec.rb @@ -131,6 +131,9 @@ RSpec.describe "posts" do can_recover: { type: :boolean, }, + can_see_hidden_post: { + type: :boolean, + }, can_wiki: { type: :boolean, }, diff --git a/spec/requests/api/schemas/json/post_replies_response.json b/spec/requests/api/schemas/json/post_replies_response.json index 2527616f79a..5e762e15891 100644 --- a/spec/requests/api/schemas/json/post_replies_response.json +++ b/spec/requests/api/schemas/json/post_replies_response.json @@ -119,6 +119,9 @@ "can_recover": { "type": "boolean" }, + "can_see_hidden_post": { + "type": "boolean" + }, "can_wiki": { "type": "boolean" }, @@ -252,6 +255,7 @@ "can_edit", "can_delete", "can_recover", + "can_see_hidden_post", "can_wiki", "user_title", "reply_to_user", diff --git a/spec/requests/api/schemas/json/post_show_response.json b/spec/requests/api/schemas/json/post_show_response.json index 67a1dcb21e8..2010d8f6519 100644 --- a/spec/requests/api/schemas/json/post_show_response.json +++ b/spec/requests/api/schemas/json/post_show_response.json @@ -106,6 +106,9 @@ "can_recover": { "type": "boolean" }, + "can_see_hidden_post": { + "type": "boolean" + }, "can_wiki": { "type": "boolean" }, diff --git a/spec/requests/api/schemas/json/post_update_response.json b/spec/requests/api/schemas/json/post_update_response.json index 7c21daa521e..87ca79819d9 100644 --- a/spec/requests/api/schemas/json/post_update_response.json +++ b/spec/requests/api/schemas/json/post_update_response.json @@ -110,6 +110,9 @@ "can_recover": { "type": "boolean" }, + "can_see_hidden_post": { + "type": "boolean" + }, "can_wiki": { "type": "boolean" }, diff --git a/spec/requests/api/schemas/json/topic_create_response.json b/spec/requests/api/schemas/json/topic_create_response.json index e3bb4d5932b..02038d367b9 100644 --- a/spec/requests/api/schemas/json/topic_create_response.json +++ b/spec/requests/api/schemas/json/topic_create_response.json @@ -121,6 +121,9 @@ "can_recover": { "type": "boolean" }, + "can_see_hidden_post": { + "type": "boolean" + }, "can_wiki": { "type": "boolean" }, diff --git a/spec/requests/api/schemas/json/topic_show_response.json b/spec/requests/api/schemas/json/topic_show_response.json index d26043cf153..40e5bd5d16c 100644 --- a/spec/requests/api/schemas/json/topic_show_response.json +++ b/spec/requests/api/schemas/json/topic_show_response.json @@ -117,6 +117,9 @@ "can_recover": { "type": "boolean" }, + "can_see_hidden_post": { + "type": "boolean" + }, "can_wiki": { "type": "boolean" }, diff --git a/spec/serializers/post_serializer_spec.rb b/spec/serializers/post_serializer_spec.rb index b4eaa9ad34f..39d7cbe6c75 100644 --- a/spec/serializers/post_serializer_spec.rb +++ b/spec/serializers/post_serializer_spec.rb @@ -141,6 +141,13 @@ RSpec.describe PostSerializer do ) end + it "includes if the user can see it" do + expect(serialized_post_for_user(Fabricate(:moderator))[:can_see_hidden_post]).to eq(true) + expect(serialized_post_for_user(Fabricate(:admin))[:can_see_hidden_post]).to eq(true) + expect(serialized_post_for_user(user)[:can_see_hidden_post]).to eq(true) + expect(serialized_post_for_user(Fabricate(:user))[:can_see_hidden_post]).to eq(false) + end + it "shows the raw post only if authorized to see it" do expect(serialized_post_for_user(nil)[:raw]).to eq(nil) expect(serialized_post_for_user(Fabricate(:user))[:raw]).to eq(nil)