Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.53% covered (warning)
89.53%
77 / 86
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockErrorFormatter
89.53% covered (warning)
89.53%
77 / 86
90.00% covered (success)
90.00%
9 / 10
25.72
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMessage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getMessages
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getBlockErrorInfo
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getFormattedBlockErrorInfo
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 formatBlockReason
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 formatBlockerLink
18.18% covered (danger)
18.18%
2 / 11
0.00% covered (danger)
0.00%
0 / 1
7.93
 getBlockErrorMessageKey
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 getBlockErrorMessageParams
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Block;
22
23use Language;
24use MediaWiki\CommentStore\CommentStoreComment;
25use MediaWiki\HookContainer\HookContainer;
26use MediaWiki\HookContainer\HookRunner;
27use MediaWiki\Language\LocalizationContext;
28use MediaWiki\Languages\LanguageFactory;
29use MediaWiki\Message\Message;
30use MediaWiki\Page\PageReferenceValue;
31use MediaWiki\Title\TitleFormatter;
32use MediaWiki\User\UserIdentity;
33use MediaWiki\User\UserIdentityUtils;
34
35/**
36 * A service class for getting formatted information about a block.
37 * To obtain an instance, use MediaWikiServices::getInstance()->getBlockErrorFormatter().
38 *
39 * @since 1.35
40 */
41class BlockErrorFormatter {
42
43    private TitleFormatter $titleFormatter;
44    private HookRunner $hookRunner;
45    private UserIdentityUtils $userIdentityUtils;
46    private LocalizationContext $uiContext;
47    private LanguageFactory $languageFactory;
48
49    public function __construct(
50        TitleFormatter $titleFormatter,
51        HookContainer $hookContainer,
52        UserIdentityUtils $userIdentityUtils,
53        LanguageFactory $languageFactory,
54        LocalizationContext $uiContext
55    ) {
56        $this->titleFormatter = $titleFormatter;
57        $this->hookRunner = new HookRunner( $hookContainer );
58        $this->userIdentityUtils = $userIdentityUtils;
59
60        $this->languageFactory = $languageFactory;
61        $this->uiContext = $uiContext;
62    }
63
64    /**
65     * @return Language
66     */
67    private function getLanguage(): Language {
68        return $this->languageFactory->getLanguage( $this->uiContext->getLanguageCode() );
69    }
70
71    /**
72     * Get a block error message. Different message keys are chosen depending on the
73     * block features. Message parameters are formatted for the specified user and
74     * language.
75     *
76     * If passed a CompositeBlock, will get a generic message stating that there are
77     * multiple blocks. To get all the block messages, use getMessages instead.
78     *
79     * @param Block $block
80     * @param UserIdentity $user
81     * @param mixed $language Unused since 1.42
82     * @param string $ip
83     * @return Message
84     */
85    public function getMessage(
86        Block $block,
87        UserIdentity $user,
88        $language,
89        string $ip
90    ): Message {
91        $key = $this->getBlockErrorMessageKey( $block, $user );
92        $params = $this->getBlockErrorMessageParams( $block, $user, $ip );
93        return $this->uiContext->msg( $key, $params );
94    }
95
96    /**
97     * Get block error messages for all of the blocks that apply to a user.
98     *
99     * @since 1.42
100     * @param Block $block
101     * @param UserIdentity $user
102     * @param string $ip
103     * @return Message[]
104     */
105    public function getMessages(
106        Block $block,
107        UserIdentity $user,
108        string $ip
109    ): array {
110        $messages = [];
111        foreach ( $block->toArray() as $singleBlock ) {
112            $messages[] = $this->getMessage( $singleBlock, $user, null, $ip );
113        }
114
115        return $messages;
116    }
117
118    /**
119     * Get a standard set of block details for building a block error message.
120     *
121     * @param Block $block
122     * @return mixed[]
123     *  - identifier: Information for looking up the block
124     *  - targetName: The target, as a string
125     *  - blockerName: The blocker, as a string
126     *  - reason: Reason for the block
127     *  - expiry: Expiry time
128     *  - timestamp: Time the block was created
129     */
130    private function getBlockErrorInfo( Block $block ) {
131        $blocker = $block->getBlocker();
132        return [
133            'identifier' => $block->getIdentifier(),
134            'targetName' => $block->getTargetName(),
135            'blockerName' => $blocker ? $blocker->getName() : '',
136            'reason' => $block->getReasonComment(),
137            'expiry' => $block->getExpiry(),
138            'timestamp' => $block->getTimestamp(),
139        ];
140    }
141
142    /**
143     * Get a standard set of block details for building a block error message,
144     * formatted for a specified user and language.
145     *
146     * @since 1.35
147     * @param Block $block
148     * @param UserIdentity $user
149     * @return mixed[] See getBlockErrorInfo
150     */
151    private function getFormattedBlockErrorInfo(
152        Block $block,
153        UserIdentity $user
154    ) {
155        $info = $this->getBlockErrorInfo( $block );
156
157        $language = $this->getLanguage();
158
159        $info['expiry'] = $language->formatExpiry( $info['expiry'], true, 'infinity', $user );
160        $info['timestamp'] = $language->userTimeAndDate( $info['timestamp'], $user );
161        $info['blockerName'] = $language->embedBidi( $info['blockerName'] );
162        $info['targetName'] = $language->embedBidi( $info['targetName'] );
163
164        $info['reason'] = $this->formatBlockReason( $info['reason'], $language );
165
166        return $info;
167    }
168
169    /**
170     * Format the block reason as plain wikitext in the specified language.
171     *
172     * @param CommentStoreComment $reason
173     * @param Language $language
174     * @return string
175     */
176    private function formatBlockReason( CommentStoreComment $reason, Language $language ) {
177        if ( $reason->text === '' ) {
178            $message = new Message( 'blockednoreason', [], $language );
179            return $message->plain();
180        }
181        return $reason->message->inLanguage( $language )->plain();
182    }
183
184    /**
185     * Create a link to the blocker's user page. This must be done here rather than in
186     * the message translation, because the blocker may not be a local user, in which
187     * case their page cannot be linked.
188     *
189     * @param ?UserIdentity $blocker
190     * @return string Link to the blocker's page; blocker's name if not a local user
191     */
192    private function formatBlockerLink( ?UserIdentity $blocker ) {
193        if ( !$blocker ) {
194            // TODO should we say something? This is just matching the code before
195            // the refactoring in late July 2021
196            return '';
197        }
198
199        $language = $this->getLanguage();
200
201        if ( $blocker->getId() === 0 ) {
202            // Foreign user
203            // TODO what about blocks placed by IPs? Shouldn't we check based on
204            // $blocker's wiki instead? This is just matching the code before the
205            // refactoring in late July 2021.
206            return $language->embedBidi( $blocker->getName() );
207        }
208
209        $blockerUserpage = PageReferenceValue::localReference( NS_USER, $blocker->getName() );
210        $blockerText = $language->embedBidi(
211            $this->titleFormatter->getText( $blockerUserpage )
212        );
213        $prefixedText = $this->titleFormatter->getPrefixedText( $blockerUserpage );
214        return "[[{$prefixedText}|{$blockerText}]]";
215    }
216
217    /**
218     * Determine the block error message key by examining the block.
219     *
220     * @param Block $block
221     * @param UserIdentity $user
222     * @return string Message key
223     */
224    private function getBlockErrorMessageKey( Block $block, UserIdentity $user ) {
225        $isTempUser = $this->userIdentityUtils->isTemp( $user );
226        $key = $isTempUser ? 'blockedtext-tempuser' : 'blockedtext';
227        if ( $block instanceof DatabaseBlock ) {
228            if ( $block->getType() === Block::TYPE_AUTO ) {
229                $key = $isTempUser ? 'autoblockedtext-tempuser' : 'autoblockedtext';
230            } elseif ( !$block->isSitewide() ) {
231                $key = 'blockedtext-partial';
232            }
233        } elseif ( $block instanceof SystemBlock ) {
234            $key = 'systemblockedtext';
235        } elseif ( $block instanceof CompositeBlock ) {
236            $key = 'blockedtext-composite';
237        }
238
239        // Allow extensions to modify the block error message
240        $this->hookRunner->onGetBlockErrorMessageKey( $block, $key );
241
242        return $key;
243    }
244
245    /**
246     * Get the formatted parameters needed to build the block error messages handled by
247     * getBlockErrorMessageKey.
248     *
249     * @param Block $block
250     * @param UserIdentity $user
251     * @param string $ip
252     * @return mixed[] Params used by standard block error messages, in order:
253     *  - blockerLink: Link to the blocker's user page, if any; otherwise same as blockerName
254     *  - reason: Reason for the block
255     *  - ip: IP address of the user attempting to perform an action
256     *  - blockerName: The blocker, as a bidi-embedded string
257     *  - identifier: Information for looking up the block
258     *  - expiry: Expiry time, in the specified language
259     *  - targetName: The target, as a bidi-embedded string
260     *  - timestamp: Time the block was created, in the specified language
261     */
262    private function getBlockErrorMessageParams(
263        Block $block,
264        UserIdentity $user,
265        string $ip
266    ) {
267        $info = $this->getFormattedBlockErrorInfo( $block, $user );
268
269        // Add params that are specific to the standard block errors
270        $info['ip'] = $ip;
271        $info['blockerLink'] = $this->formatBlockerLink( $block->getBlocker() );
272
273        // Display the CompositeBlock identifier as a message containing relevant block IDs
274        if ( $block instanceof CompositeBlock ) {
275            $ids = $this->getLanguage()->commaList( array_map(
276                static function ( $id ) {
277                    return '#' . $id;
278                },
279                array_filter( $info['identifier'], 'is_int' )
280            ) );
281            if ( $ids === '' ) {
282                $idsMsg = $this->uiContext->msg( 'blockedtext-composite-no-ids', [] );
283            } else {
284                $idsMsg = $this->uiContext->msg( 'blockedtext-composite-ids', [ $ids ] );
285            }
286            $info['identifier'] = $idsMsg->plain();
287        }
288
289        // Messages expect the params in this order
290        $order = [
291            'blockerLink',
292            'reason',
293            'ip',
294            'blockerName',
295            'identifier',
296            'expiry',
297            'targetName',
298            'timestamp',
299        ];
300
301        $params = [];
302        foreach ( $order as $item ) {
303            $params[] = $info[$item];
304        }
305
306        return $params;
307    }
308
309}