Page MenuHomePhabricator
Authored By
Dreamy_Jazz
Feb 13 2026, 12:33 PM
Size
12 KB
Referenced Files
None
Subscribers
None

T411366.patch

From c51ba24e340496226e768ab99c87076f11f20ee2 Mon Sep 17 00:00:00 2001
From: Dreamy Jazz <wpgbrown@wikimedia.org>
Date: Fri, 13 Feb 2026 12:24:33 +0000
Subject: [PATCH] SECURITY: Hide hidden users in
Special:SuggestedInvestigations
Why:
* Special:SuggestedInvestigations shows users in cases to
users who are trusted on the wikis
** However, not all users with access to this page may have
the 'hideuser' right
* We should hide usernames which are blocked with a
'hide user' block from users without the associated right
What:
* Update SuggestedInvestigationsCasesPager::formatUsersCell
to replace usernames the user cannot see with the
'rev-deleted-user' i18n message, similar to how other
interfaces handle hidden users in CheckUser
** When hiding the username also remove the user toollinks
as the URLs of these toolinks would expose the hidden
username
* Update SuggestedInvestigationsCasesPager::formatActionsCell
to not include usernames the user cannot see in the
link to Special:Investigate
* Add PHPUnit tests to verify that these changes work
as expected
Bug: T411366
Change-Id: I3ad0ece14cd0b65468485e388716c06951bc6ab0
---
.../SuggestedInvestigationsCasesPager.php | 135 ++++++++++--------
.../SuggestedInvestigationsPagerFactory.php | 1 +
.../SuggestedInvestigationsCasesPagerTest.php | 65 +++++++++
3 files changed, 143 insertions(+), 58 deletions(-)
diff --git a/src/SuggestedInvestigations/Pagers/SuggestedInvestigationsCasesPager.php b/src/SuggestedInvestigations/Pagers/SuggestedInvestigationsCasesPager.php
index d0896822..705300db 100644
--- a/src/SuggestedInvestigations/Pagers/SuggestedInvestigationsCasesPager.php
+++ b/src/SuggestedInvestigations/Pagers/SuggestedInvestigationsCasesPager.php
@@ -39,6 +39,7 @@ use MediaWiki\SpecialPage\SpecialPage;
use MediaWiki\SpecialPage\SpecialPageFactory;
use MediaWiki\Title\Title;
use MediaWiki\User\UserEditTracker;
+use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityLookup;
use MediaWiki\User\UserIdentityValue;
@@ -130,6 +131,7 @@ class SuggestedInvestigationsCasesPager extends CodexTablePager {
private readonly CommentFormatter $commentFormatter,
private readonly ?CentralAuthEditCounter $centralAuthEditCounter,
private readonly LinkBatchFactory $linkBatchFactory,
+ private readonly UserFactory $userFactory,
LinkRenderer $linkRenderer,
IContextSource $context,
array $signals
@@ -279,66 +281,84 @@ class SuggestedInvestigationsCasesPager extends CodexTablePager {
$contributionsSpecialPage = $this->useGlobalContribs ? 'GlobalContributions' : 'Contributions';
foreach ( $users as $i => $user ) {
- $userLink = $this->getLinkRenderer()->makeUserLink( $user, $this->getContext() );
+ $userVisible = $this->getAuthority()->isAllowed( 'hideuser' ) ||
+ !$this->userFactory->newFromUserIdentity( $user )->isHidden();
+ if ( $userVisible ) {
+ $userLink = $this->getLinkRenderer()->makeUserLink( $user, $this->getContext() );
+
+ // Generate a link to Special:CheckUser with a prefilled 'reason' input field that links back to the
+ // case that this user is in.
+ $checkUserPrefilledReason = $this->msg( 'checkuser-suggestedinvestigations-user-check-reason-prefill' )
+ ->params( $detailViewLink )
+ ->numParams( $this->mCurrentRow->sic_id )
+ ->params( $user->getName() )
+ ->inContentLanguage()
+ ->text();
- // Generate a link to Special:CheckUser with a prefilled 'reason' input field that links back to the
- // case that this user is in.
- $checkUserPrefilledReason = $this->msg( 'checkuser-suggestedinvestigations-user-check-reason-prefill' )
- ->params( $detailViewLink )
- ->numParams( $this->mCurrentRow->sic_id )
- ->params( $user->getName() )
- ->inContentLanguage()
- ->text();
+ // Generate the link class for the "contribs" tool link
+ $userContribsLinkClass = 'mw-usertoollinks-contribs';
- // Generate the link class for the "contribs" tool link
- $userContribsLinkClass = 'mw-usertoollinks-contribs';
+ if ( $this->useGlobalContribs ) {
+ $editCount = $this->centralAuthEditCounter->getCount(
+ CentralAuthUser::getInstance( $user )
+ );
+ } else {
+ $editCount = $this->userEditTracker->getUserEditCount( $user );
+ }
+ if ( $editCount === 0 ) {
+ // Use same CSS classes as Linker::userToolLinkArray to get a red link when no contribs
+ $userContribsLinkClass .= ' mw-usertoollinks-contribs-no-edits';
+ }
- if ( $this->useGlobalContribs ) {
- $editCount = $this->centralAuthEditCounter->getCount(
- CentralAuthUser::getInstance( $user )
+ // Add link to either Special:Contributions or Special:GlobalContributions
+ $userToolLinks = [];
+ $userToolLinks[] = $this->getLinkRenderer()->makeKnownLink(
+ SpecialPage::getTitleFor( $contributionsSpecialPage, $user->getName() ),
+ $this->msg( 'contribslink' )
+ ->params( $user->getName() )
+ ->text(),
+ [ 'class' => $userContribsLinkClass ]
);
- } else {
- $editCount = $this->userEditTracker->getUserEditCount( $user );
- }
- if ( $editCount === 0 ) {
- // Use same CSS classes as Linker::userToolLinkArray to get a red link when no contribs
- $userContribsLinkClass .= ' mw-usertoollinks-contribs-no-edits';
- }
- // Add link to either Special:Contributions or Special:GlobalContributions
- $userToolLinks = [];
- $userToolLinks[] = $this->getLinkRenderer()->makeKnownLink(
- SpecialPage::getTitleFor( $contributionsSpecialPage, $user->getName() ),
- $this->msg( 'contribslink' )
- ->params( $user->getName() )
- ->text(),
- [ 'class' => $userContribsLinkClass ]
- );
+ // Add link to Special:CheckUserLog if the user has been checked before and the
+ // viewing authority has the 'checkuser-log' right
+ if (
+ in_array( $user->getId(), $this->usersWhoHaveBeenChecked ) &&
+ $this->getAuthority()->isAllowed( 'checkuser-log' )
+ ) {
+ $userToolLinks[] = $this->getLinkRenderer()->makeKnownLink(
+ SpecialPage::getTitleFor( 'CheckUserLog', $user->getName() ),
+ $this->msg( 'checkuser-suggestedinvestigations-user-past-checks-link-text' )
+ ->params( $user->getName() )
+ ->text()
+ );
+ }
- // Add link to Special:CheckUserLog if the user has been checked before and the
- // viewing authority has the 'checkuser-log' right
- if (
- in_array( $user->getId(), $this->usersWhoHaveBeenChecked ) &&
- $this->getAuthority()->isAllowed( 'checkuser-log' )
- ) {
- $userToolLinks[] = $this->getLinkRenderer()->makeKnownLink(
- SpecialPage::getTitleFor( 'CheckUserLog', $user->getName() ),
- $this->msg( 'checkuser-suggestedinvestigations-user-past-checks-link-text' )
- ->params( $user->getName() )
- ->text()
+ // Add link to Special:CheckUser if the user has the 'checkuser' right
+ if ( $this->getAuthority()->isAllowed( 'checkuser' ) ) {
+ $userToolLinks[] = $this->getLinkRenderer()->makeKnownLink(
+ SpecialPage::getTitleFor( 'CheckUser', $user->getName() ),
+ $this->msg( 'checkuser-suggestedinvestigations-user-check-link-text' )
+ ->params( $user->getName() )
+ ->text(),
+ [],
+ [ 'reason' => $checkUserPrefilledReason ]
+ );
+ }
+ } else {
+ $userLink = Html::element(
+ 'span',
+ [ 'class' => 'history-deleted' ],
+ $this->msg( 'rev-deleted-user' )->text()
);
+ $userToolLinks = [];
}
- // Add link to Special:CheckUser if the user has the 'checkuser' right
- if ( $this->getAuthority()->isAllowed( 'checkuser' ) ) {
- $userToolLinks[] = $this->getLinkRenderer()->makeKnownLink(
- SpecialPage::getTitleFor( 'CheckUser', $user->getName() ),
- $this->msg( 'checkuser-suggestedinvestigations-user-check-link-text' )
- ->params( $user->getName() )
- ->text(),
- [],
- [ 'reason' => $checkUserPrefilledReason ]
- );
+ $userToolLinksHtml = '';
+ if ( $userToolLinks !== [] ) {
+ $userToolLinksHtml = $this->msg( 'parentheses' )
+ ->rawParams( $this->getLanguage()->pipeList( $userToolLinks ) )
+ ->escaped();
}
$formattedUsers .= Html::rawElement(
@@ -348,12 +368,7 @@ class SuggestedInvestigationsCasesPager extends CodexTablePager {
: '',
],
$this->msg( 'checkuser-suggestedinvestigations-user' )
- ->rawParams(
- $userLink,
- $this->msg( 'parentheses' )
- ->rawParams( $this->getLanguage()->pipeList( $userToolLinks ) )
- ->escaped()
- )
+ ->rawParams( $userLink, $userToolLinksHtml )
->parse()
);
}
@@ -440,7 +455,11 @@ class SuggestedInvestigationsCasesPager extends CodexTablePager {
] );
/** @var UserIdentity[] $users */
- $users = $this->mCurrentRow->users;
+ $users = array_filter(
+ $this->mCurrentRow->users,
+ fn ( UserIdentity $user ) => $this->getAuthority()->isAllowed( 'hideuser' ) ||
+ !$this->userFactory->newFromUserIdentity( $user )->isHidden()
+ );
$investigateEnabled = false;
$investigateUrl = null;
diff --git a/src/SuggestedInvestigations/Pagers/SuggestedInvestigationsPagerFactory.php b/src/SuggestedInvestigations/Pagers/SuggestedInvestigationsPagerFactory.php
index ae4f702b..525068f9 100644
--- a/src/SuggestedInvestigations/Pagers/SuggestedInvestigationsPagerFactory.php
+++ b/src/SuggestedInvestigations/Pagers/SuggestedInvestigationsPagerFactory.php
@@ -96,6 +96,7 @@ class SuggestedInvestigationsPagerFactory {
$this->commentFormatter,
$this->centralAuthEditCounter,
$this->linkBatchFactory,
+ $this->userFactory,
$this->linkRenderer,
$context,
$signals
diff --git a/tests/phpunit/integration/SuggestedInvestigations/Pagers/SuggestedInvestigationsCasesPagerTest.php b/tests/phpunit/integration/SuggestedInvestigations/Pagers/SuggestedInvestigationsCasesPagerTest.php
index ff13faa0..d491e74a 100644
--- a/tests/phpunit/integration/SuggestedInvestigations/Pagers/SuggestedInvestigationsCasesPagerTest.php
+++ b/tests/phpunit/integration/SuggestedInvestigations/Pagers/SuggestedInvestigationsCasesPagerTest.php
@@ -374,6 +374,7 @@ class SuggestedInvestigationsCasesPagerTest extends MediaWikiIntegrationTestCase
$this->addCaseWithTwoUsers();
$context = RequestContext::getMain();
$context->setTitle( Title::newFromText( 'Special:SuggestedInvestigations' ) );
+ $context->setAuthority( $this->mockRegisteredUltimateAuthority() );
// Mock the global edit counts for our test users so that the first test user has no edits
// and all other users have one edit
@@ -558,6 +559,70 @@ class SuggestedInvestigationsCasesPagerTest extends MediaWikiIntegrationTestCase
);
}
+ public function testOutputWhenUsersHidden() {
+ $this->overrideConfigValues( [
+ 'CheckUserSuggestedInvestigationsUseGlobalContributionsLink' => false,
+ MainConfigNames::LanguageCode => 'qqx',
+ ] );
+ ConvertibleTimestamp::setFakeTime( '20250403020100' );
+
+ // Get a case with one of the users blocked with a 'hideuser' block
+ $this->addCaseWithTwoUsers();
+ $this->getServiceContainer()->getBlockUserFactory()
+ ->newBlockUser(
+ self::$testUser1,
+ $this->mockRegisteredUltimateAuthority(),
+ 'indefinite',
+ 'Test reason',
+ [ 'isHideUser' => true ]
+ )
+ ->placeBlock();
+
+ // Load the special page with a user who cannot see hidden users
+ $context = RequestContext::getMain();
+ $context->setTitle( Title::newFromText( 'Special:SuggestedInvestigations' ) );
+ $context->setLanguage( 'qqx' );
+ $context->setAuthority( $this->mockRegisteredAuthorityWithoutPermissions( [ 'hideuser' ] ) );
+
+ $pager = $this->getPager( $context );
+
+ $html = $pager->getFullOutput()->getContentHolder()->getAsHtmlString();
+
+ $this->assertStringContainsString(
+ '(rev-deleted-user)',
+ $html,
+ 'First test username should be replaced with the rev-deleted-user message'
+ );
+
+ $this->assertStringNotContainsString(
+ '?title=Special:CheckUser/' . str_replace( ' ', '_', self::$testUser1->getName() ) .
+ '&amp;reason=%28checkuser-suggestedinvestigations-user-check-reason-prefill',
+ $html,
+ 'Should not contain link to Special:CheckUser for the first user'
+ );
+ $this->assertStringContainsString(
+ '?title=Special:CheckUser/' . str_replace( ' ', '_', self::$testUser2->getName() ) .
+ '&amp;reason=%28checkuser-suggestedinvestigations-user-check-reason-prefill',
+ $html,
+ 'Should contain link to Special:CheckUser for the second user'
+ );
+
+ $name2 = urlencode( self::$testUser2->getName() );
+ $this->assertStringContainsString(
+ '?title=Special:Investigate&amp;targets=' . $name2 .
+ '&amp;reason=%28checkuser-suggestedinvestigations-user-investigate-reason-prefill',
+ $html,
+ 'Should contain link to Special:Investigate in the case row with only the second user'
+ );
+
+ $this->assertStringNotContainsString(
+ self::$testUser1->getName(),
+ $html,
+ 'As the first test user is not visible by the viewing authority, ' .
+ 'their name should be not visible anywhere on the page'
+ );
+ }
+
public function testInvestigateDisabledWhenTooManyUsers() {
$caseId = $this->addCaseWithManyUsers();
--
2.34.1

File Metadata

Mime Type
text/x-diff
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
23162890
Default Alt Text
T411366.patch (12 KB)

Event Timeline