Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
GlobalBlockLogFormatter
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
6 / 6
33
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getMessageKey
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 getMessageParameters
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
12
 getActionLinks
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getUserIdentityForTarget
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 checkAuthorityCanSeeUser
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3namespace MediaWiki\Extension\GlobalBlocking;
4
5use ExtensionRegistry;
6use LogEntry;
7use LogFormatter;
8use LogPage;
9use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
10use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockingLinkBuilder;
11use MediaWiki\Html\Html;
12use MediaWiki\Linker\Linker;
13use MediaWiki\Message\Message;
14use MediaWiki\User\UserFactory;
15use MediaWiki\User\UserIdentity;
16use MediaWiki\User\UserIdentityLookup;
17use MediaWiki\User\UserIdentityValue;
18use UnexpectedValueException;
19use Wikimedia\IPUtils;
20
21/**
22 * Log formatter for gblblock/* entries
23 */
24class GlobalBlockLogFormatter extends LogFormatter {
25
26    private UserFactory $userFactory;
27    private UserIdentityLookup $userIdentityLookup;
28    private GlobalBlockingLinkBuilder $globalBlockingLinkBuilder;
29
30    /**
31     * @param LogEntry $entry
32     * @param UserFactory $userFactory
33     * @param UserIdentityLookup $userIdentityLookup
34     * @param GlobalBlockingLinkBuilder $globalBlockingLinkBuilder
35     */
36    public function __construct(
37        LogEntry $entry,
38        UserFactory $userFactory,
39        UserIdentityLookup $userIdentityLookup,
40        GlobalBlockingLinkBuilder $globalBlockingLinkBuilder
41    ) {
42        parent::__construct( $entry );
43        $this->userFactory = $userFactory;
44        $this->userIdentityLookup = $userIdentityLookup;
45        $this->globalBlockingLinkBuilder = $globalBlockingLinkBuilder;
46    }
47
48    protected function getMessageKey(): string {
49        $subtype = $this->entry->getSubtype();
50
51        if ( $subtype === 'whitelist' ) {
52            $key = 'globalblocking-logentry-whitelist';
53        } elseif ( $subtype === 'dwhitelist' ) {
54            $key = 'globalblocking-logentry-dewhitelist';
55        } elseif ( $subtype === 'gunblock' ) {
56            $key = 'globalblocking-logentry-unblock';
57        } elseif ( $subtype === 'gblock' ) {
58            // The 'gblock' subtype is used by both legacy and non-legacy log entries. However, the order of the
59            // parameters between the legacy and non-legacy format is the same and as such we can use the same i18n
60            // message key.
61            $key = 'globalblocking-logentry-block';
62        } elseif ( $subtype === 'gblock2' ) {
63            $key = 'globalblocking-logentry-block-old-format';
64        } elseif ( $subtype === 'modify' ) {
65            if ( $this->entry->isLegacy() ) {
66                $key = 'globalblocking-logentry-modify-old-format';
67            } else {
68                $key = 'globalblocking-logentry-modify';
69            }
70        } else {
71            throw new UnexpectedValueException( "Unknown log subtype: $subtype" );
72        }
73
74        return $key;
75    }
76
77    /**
78     * Formats parameters intended for action message from array of all parameters.
79     * There are four hardcoded parameters:
80     *  - $1: user name with premade link
81     *  - $2: the performer of the block usable of the block used for the gender magic function
82     *  - $3: user page with a premade link
83     *  - $4: the target of the block used for the gender magic function
84     *
85     * For the 'gblock', and 'modify' subtypes when the log is not legacy, the parameters also include:
86     *  - $5: the expiration date of the block
87     *  - $6: the flags for the block in a localised comma separated list
88     *
89     * For the 'gblock2' and 'modify' subtypes when the log is legacy, the parameters also include:
90     *  - $5: the flags for the block
91     *
92     * For the 'gblock' subtype when the log is legacy, the parameters also include:
93     *  - $5: the expiration date of the block
94     *
95     * @return array
96     */
97    protected function getMessageParameters(): array {
98        $params = parent::getMessageParameters();
99
100        if ( $this->entry->isLegacy() ) {
101            if ( in_array( $this->entry->getSubtype(), [ 'gblock2', 'modify', 'gblock' ] ) ) {
102                // If the entry parameters are in the legacy format and the log subtype has parameters defined,
103                // then we need to increase the index of the parameters by one to allow space for the GENDER parameter
104                // for the target.
105                array_splice( $params, 3, 0, '' );
106                if ( $this->entry->getSubtype() === 'gblock' ) {
107                    if ( !array_key_exists( 5, $params ) ) {
108                        // No flags may exist for the legacy format for the 'gblock' subtype.
109                        $params[5] = '';
110                    } elseif ( $params[5] === 'anon-only' ) {
111                        // Convert the anon-only flag into a localised message.
112                        $params[5] = $this->msg( 'globalblocking-block-flag-anon-only' )->text();
113                    }
114                    if ( $params[5] !== '' ) {
115                        // Wrap the flags in parentheses.
116                        $params[5] = $this->msg( 'parentheses', $params[5] )->text();
117                    }
118                }
119            }
120        } elseif ( in_array( $this->entry->getSubtype(), [ 'gblock', 'modify' ] ) ) {
121            if ( !wfIsInfinity( $params[4] ) ) {
122                // Ignoring expiry values of 'infinity', treat the expiry parameter as a datetime parameter.
123                $params[4] = Message::dateTimeParam( $params[4] );
124            }
125            // Convert the flags to a localised comma separated list
126            $flags = [];
127            if ( in_array( 'anon-only', $params[5] ) ) {
128                $flags[] = $this->msg( 'globalblocking-block-flag-anon-only' )->text();
129            }
130            // Only display the flags if there are any set.
131            if ( count( $flags ) ) {
132                $params[5] = $this->msg(
133                    'parentheses',
134                    $this->context->getLanguage()->commaList( $flags )
135                )->text();
136            } else {
137                $params[5] = '';
138            }
139        }
140
141        $targetUserIdentity = $this->getUserIdentityForTarget();
142        $canViewTarget = $this->checkAuthorityCanSeeUser( $targetUserIdentity );
143
144        if ( !$canViewTarget ) {
145            // If the current authority cannot view the target of the block, then replace the user link with a message
146            // indicating that the target of the block is hidden.
147            $params[2] = Message::rawParam( Html::element(
148                'span',
149                [ 'class' => 'history-deleted' ],
150                $this->msg( 'rev-deleted-user' )->text()
151            ) );
152            $params[3] = '';
153        } else {
154            // Overwrite the third parameter (index 2) with a user link to provide a talk page link and link the
155            // contributions page for IP addresses.
156            $params[2] = Message::rawParam( $this->makeUserLink( $targetUserIdentity, Linker::TOOL_LINKS_NOBLOCK ) );
157            $params[3] = $targetUserIdentity->getName();
158        }
159
160        return $params;
161    }
162
163    /**
164     * Adds the action links for global block log entries which depend on what rights that the
165     * user has. This is the same as the action links used on Special:GlobalBlockList entries.
166     *
167     * @return string
168     */
169    public function getActionLinks(): string {
170        $targetUserIdentity = $this->getUserIdentityForTarget();
171        if (
172            !$this->checkAuthorityCanSeeUser( $targetUserIdentity ) ||
173            !$this->canView( LogPage::DELETED_ACTION )
174        ) {
175            // Don't show the action links if the current authority cannot view the target of the block.
176            return '';
177        }
178        // Get the action links for the log entry which are the same as those used on Special:GlobalBlockList.
179        return $this->globalBlockingLinkBuilder->getActionLinks(
180            $this->context->getAuthority(), $targetUserIdentity->getName()
181        );
182    }
183
184    /**
185     * Get the UserIdentity object for the target of the block referenced in the current log entry.
186     *
187     * @return UserIdentity This can be a IP address, range, or username (which exists or does not exist).
188     */
189    private function getUserIdentityForTarget(): UserIdentity {
190        $targetTitle = $this->entry->getTarget();
191        $userText = $targetTitle->getText();
192        if ( $targetTitle->getNamespace() === NS_SPECIAL ) {
193            // Some very old log entries (pre-2010) have the title as the Special:Contributions page for the target.
194            // In this case, the target text is the subpage of the Special:Contributions page (T362700).
195            // We also cannot use Title::getSubpageText here because the NS_SPECIAL namespace does not have subpages
196            // by default.
197            $userText = substr( $userText, strlen( 'Contributions/' ) );
198        }
199        return $this->userIdentityLookup->getUserIdentityByName( $userText )
200            ?? UserIdentityValue::newAnonymous( $userText );
201    }
202
203    /**
204     * Returns whether the current authority can see the target of the block.
205     *
206     * @param UserIdentity $userIdentity The object returned by ::getUserIdentityForTarget
207     * @return bool
208     */
209    private function checkAuthorityCanSeeUser( UserIdentity $userIdentity ): bool {
210        if ( IPUtils::isIPAddress( $userIdentity->getName() ) ) {
211            // IP addresses cannot be hidden, so the authority will always be able to see them.
212            return true;
213        }
214
215        // Assume that the authority has the rights to see the user by default.
216        $canViewTarget = true;
217        $authority = $this->context->getAuthority();
218
219        // If the user exists locally, then we can check if the user is hidden locally.
220        if ( $userIdentity->isRegistered() ) {
221            $user = $this->userFactory->newFromUserIdentity( $userIdentity );
222            $canViewTarget = !( $user->isHidden() && !$authority->isAllowed( 'hideuser' ) );
223        }
224
225        // If CentralAuth is loaded, then we can check if the central user is hidden.
226        // This is necessary if the user does not exist on this wiki but their global
227        // account is hidden.
228        if ( $canViewTarget && ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) ) {
229            $centralUser = CentralAuthUser::getInstance( $userIdentity );
230            $canViewTarget = !( $centralUser->isHidden() && !$authority->isAllowed( 'centralauth-suppress' ) );
231        }
232
233        return $canViewTarget;
234    }
235}