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