Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
73.04% |
149 / 204 |
|
70.00% |
7 / 10 |
CRAP | |
0.00% |
0 / 1 |
BlockLogFormatter | |
73.40% |
149 / 203 |
|
70.00% |
7 / 10 |
137.71 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getMessageParameters | |
88.06% |
59 / 67 |
|
0.00% |
0 / 1 |
17.49 | |||
extractParameters | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
getPreloadTitles | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
6.97 | |||
getActionLinks | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
72 | |||
formatBlockFlags | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
formatBlockFlag | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getParametersForApi | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
10 | |||
formatParametersForApi | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
getMessageKey | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 |
1 | <?php |
2 | /** |
3 | * Formatter for block log entries. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @license GPL-2.0-or-later |
22 | * @since 1.25 |
23 | */ |
24 | |
25 | namespace MediaWiki\Logging; |
26 | |
27 | use MediaWiki\Api\ApiResult; |
28 | use MediaWiki\Language\Language; |
29 | use MediaWiki\Linker\Linker; |
30 | use MediaWiki\MainConfigNames; |
31 | use MediaWiki\Message\Message; |
32 | use MediaWiki\SpecialPage\SpecialPage; |
33 | use MediaWiki\Title\MalformedTitleException; |
34 | use MediaWiki\Title\NamespaceInfo; |
35 | use MediaWiki\Title\Title; |
36 | use MediaWiki\Title\TitleParser; |
37 | use MediaWiki\User\User; |
38 | |
39 | /** |
40 | * This class formats block log entries. |
41 | * |
42 | * @since 1.25 |
43 | */ |
44 | class BlockLogFormatter extends LogFormatter { |
45 | private TitleParser $titleParser; |
46 | private NamespaceInfo $namespaceInfo; |
47 | |
48 | public function __construct( |
49 | LogEntry $entry, |
50 | TitleParser $titleParser, |
51 | NamespaceInfo $namespaceInfo |
52 | ) { |
53 | parent::__construct( $entry ); |
54 | $this->titleParser = $titleParser; |
55 | $this->namespaceInfo = $namespaceInfo; |
56 | } |
57 | |
58 | protected function getMessageParameters() { |
59 | $params = parent::getMessageParameters(); |
60 | |
61 | $title = $this->entry->getTarget(); |
62 | if ( str_starts_with( $title->getText(), '#' ) ) { |
63 | // autoblock - no user link possible |
64 | $params[2] = $title->getText(); |
65 | $params[3] = ''; // no user name for gender use |
66 | } else { |
67 | // Create a user link for the blocked |
68 | $username = $title->getText(); |
69 | // @todo Store the user identifier in the parameters |
70 | // to make this faster for future log entries |
71 | $targetUser = User::newFromName( $username, false ); |
72 | $params[2] = Message::rawParam( $this->makeUserLink( $targetUser, Linker::TOOL_LINKS_NOBLOCK ) ); |
73 | $params[3] = $username; // plain user name for gender use |
74 | } |
75 | |
76 | $subtype = $this->entry->getSubtype(); |
77 | if ( $subtype === 'block' || $subtype === 'reblock' ) { |
78 | if ( !isset( $params[4] ) ) { |
79 | // Very old log entry without duration: means infinity |
80 | $params[4] = 'infinity'; |
81 | } |
82 | // Localize the duration, and add a tooltip |
83 | // in English to help visitors from other wikis. |
84 | // The lrm is needed to make sure that the number |
85 | // is shown on the correct side of the tooltip text. |
86 | // @phan-suppress-next-line SecurityCheck-DoubleEscaped |
87 | $durationTooltip = '‎' . htmlspecialchars( $params[4] ); |
88 | $blockExpiry = $this->context->getLanguage()->translateBlockExpiry( |
89 | $params[4], |
90 | $this->context->getUser(), |
91 | (int)wfTimestamp( TS_UNIX, $this->entry->getTimestamp() ) |
92 | ); |
93 | if ( $this->plaintext ) { |
94 | // @phan-suppress-next-line SecurityCheck-XSS Unlikely positive, only if language format is bad |
95 | $params[4] = Message::rawParam( $blockExpiry ); |
96 | } else { |
97 | $params[4] = Message::rawParam( |
98 | "<span class=\"blockExpiry\" title=\"$durationTooltip\">" . |
99 | // @phan-suppress-next-line SecurityCheck-DoubleEscaped language class does not escape |
100 | htmlspecialchars( $blockExpiry ) . |
101 | '</span>' |
102 | ); |
103 | } |
104 | $params[5] = isset( $params[5] ) ? |
105 | self::formatBlockFlags( $params[5], $this->context->getLanguage() ) : ''; |
106 | |
107 | // block restrictions |
108 | if ( isset( $params[6] ) ) { |
109 | $pages = $params[6]['pages'] ?? []; |
110 | $pageLinks = []; |
111 | foreach ( $pages as $page ) { |
112 | $pageLinks[] = $this->makePageLink( Title::newFromText( $page ) ); |
113 | } |
114 | |
115 | $rawNamespaces = $params[6]['namespaces'] ?? []; |
116 | $namespaces = []; |
117 | foreach ( $rawNamespaces as $ns ) { |
118 | $text = (int)$ns === NS_MAIN |
119 | ? $this->msg( 'blanknamespace' )->escaped() |
120 | : htmlspecialchars( $this->context->getLanguage()->getFormattedNsText( $ns ) ); |
121 | if ( $this->plaintext ) { |
122 | // Because the plaintext cannot link to the Special:AllPages |
123 | // link that is linked to in non-plaintext mode, just return |
124 | // the name of the namespace. |
125 | $namespaces[] = $text; |
126 | } else { |
127 | $namespaces[] = $this->makePageLink( |
128 | SpecialPage::getTitleFor( 'Allpages' ), |
129 | [ 'namespace' => $ns ], |
130 | $text |
131 | ); |
132 | } |
133 | } |
134 | |
135 | $rawActions = $params[6]['actions'] ?? []; |
136 | $actions = []; |
137 | foreach ( $rawActions as $action ) { |
138 | $actions[] = $this->msg( 'ipb-action-' . $action )->escaped(); |
139 | } |
140 | |
141 | $restrictions = []; |
142 | if ( $pageLinks ) { |
143 | $restrictions[] = $this->msg( 'logentry-partialblock-block-page' ) |
144 | ->numParams( count( $pageLinks ) ) |
145 | ->rawParams( $this->context->getLanguage()->listToText( $pageLinks ) )->escaped(); |
146 | } |
147 | |
148 | if ( $namespaces ) { |
149 | $restrictions[] = $this->msg( 'logentry-partialblock-block-ns' ) |
150 | ->numParams( count( $namespaces ) ) |
151 | ->rawParams( $this->context->getLanguage()->listToText( $namespaces ) )->escaped(); |
152 | } |
153 | $enablePartialActionBlocks = $this->context->getConfig() |
154 | ->get( MainConfigNames::EnablePartialActionBlocks ); |
155 | if ( $actions && $enablePartialActionBlocks ) { |
156 | $restrictions[] = $this->msg( 'logentry-partialblock-block-action' ) |
157 | ->numParams( count( $actions ) ) |
158 | ->rawParams( $this->context->getLanguage()->listToText( $actions ) )->escaped(); |
159 | } |
160 | |
161 | $params[6] = Message::rawParam( $this->context->getLanguage()->listToText( $restrictions ) ); |
162 | } |
163 | } |
164 | |
165 | return $params; |
166 | } |
167 | |
168 | protected function extractParameters() { |
169 | $params = parent::extractParameters(); |
170 | // Legacy log params returning the params in index 3 and 4, moved to 4 and 5 |
171 | if ( $this->entry->isLegacy() && isset( $params[3] ) ) { |
172 | if ( isset( $params[4] ) ) { |
173 | $params[5] = $params[4]; |
174 | } |
175 | $params[4] = $params[3]; |
176 | $params[3] = ''; |
177 | } |
178 | return $params; |
179 | } |
180 | |
181 | public function getPreloadTitles() { |
182 | $title = $this->entry->getTarget(); |
183 | $preload = []; |
184 | // Preload user page for non-autoblocks |
185 | if ( substr( $title->getText(), 0, 1 ) !== '#' && $title->canExist() ) { |
186 | $preload[] = $this->namespaceInfo->getTalkPage( $title ); |
187 | } |
188 | // Preload page restriction |
189 | $params = $this->extractParameters(); |
190 | if ( isset( $params[6]['pages'] ) ) { |
191 | foreach ( $params[6]['pages'] as $page ) { |
192 | try { |
193 | $preload[] = $this->titleParser->parseTitle( $page ); |
194 | } catch ( MalformedTitleException ) { |
195 | } |
196 | } |
197 | } |
198 | return $preload; |
199 | } |
200 | |
201 | public function getActionLinks() { |
202 | $subtype = $this->entry->getSubtype(); |
203 | $linkRenderer = $this->getLinkRenderer(); |
204 | |
205 | // Don't show anything if the action is hidden |
206 | if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) |
207 | || !( $subtype === 'block' || $subtype === 'reblock' ) |
208 | || !$this->context->getAuthority()->isAllowed( 'block' ) |
209 | ) { |
210 | return ''; |
211 | } |
212 | |
213 | $title = $this->entry->getTarget(); |
214 | if ( $this->context->getConfig()->get( MainConfigNames::UseCodexSpecialBlock ) ) { |
215 | $params = $this->entry->getParameters(); |
216 | if ( isset( $params['blockId'] ) ) { |
217 | // If we have a block ID, show remove/change links |
218 | $query = isset( $params['blockId'] ) ? [ 'id' => $params['blockId'] ] : []; |
219 | $links = [ |
220 | $linkRenderer->makeKnownLink( |
221 | SpecialPage::getTitleFor( 'Block', $title->getDBkey() ), |
222 | $this->msg( 'remove-blocklink' )->text(), |
223 | [], |
224 | $query + [ 'remove' => '1' ] |
225 | ), |
226 | $linkRenderer->makeKnownLink( |
227 | SpecialPage::getTitleFor( 'Block', $title->getDBkey() ), |
228 | $this->msg( 'change-blocklink' )->text(), |
229 | [], |
230 | $query |
231 | ) |
232 | ]; |
233 | } else { |
234 | // For legacy log entries, just show "manage blocks" since the |
235 | // Codex block page doesn't have an "unblock by target" mode |
236 | $links = [ |
237 | $linkRenderer->makeKnownLink( |
238 | SpecialPage::getTitleFor( 'Block', $title->getDBkey() ), |
239 | $this->msg( 'manage-blocklink' )->text(), |
240 | ), |
241 | ]; |
242 | } |
243 | } else { |
244 | // Show unblock/change links |
245 | $links = [ |
246 | $linkRenderer->makeKnownLink( |
247 | SpecialPage::getTitleFor( 'Unblock', $title->getDBkey() ), |
248 | $this->msg( 'unblocklink' )->text() |
249 | ), |
250 | $linkRenderer->makeKnownLink( |
251 | SpecialPage::getTitleFor( 'Block', $title->getDBkey() ), |
252 | $this->msg( 'change-blocklink' )->text() |
253 | ) |
254 | ]; |
255 | } |
256 | |
257 | return $this->msg( 'parentheses' )->rawParams( |
258 | $this->context->getLanguage()->pipeList( $links ) )->escaped(); |
259 | } |
260 | |
261 | /** |
262 | * Convert a comma-delimited list of block log flags |
263 | * into a more readable (and translated) form |
264 | * |
265 | * @param string $flags Flags to format |
266 | * @param Language $lang |
267 | * @return string |
268 | */ |
269 | public static function formatBlockFlags( $flags, Language $lang ) { |
270 | $flags = trim( $flags ); |
271 | if ( $flags === '' ) { |
272 | return ''; // nothing to do |
273 | } |
274 | $flags = explode( ',', $flags ); |
275 | $flagsCount = count( $flags ); |
276 | |
277 | for ( $i = 0; $i < $flagsCount; $i++ ) { |
278 | $flags[$i] = self::formatBlockFlag( $flags[$i], $lang ); |
279 | } |
280 | |
281 | return wfMessage( 'parentheses' )->inLanguage( $lang ) |
282 | ->rawParams( $lang->commaList( $flags ) )->escaped(); |
283 | } |
284 | |
285 | /** |
286 | * Translate a block log flag if possible |
287 | * |
288 | * @param string $flag Flag to translate |
289 | * @param Language $lang Language object to use |
290 | * @return string |
291 | */ |
292 | public static function formatBlockFlag( $flag, Language $lang ) { |
293 | static $messages = []; |
294 | |
295 | if ( !isset( $messages[$flag] ) ) { |
296 | $messages[$flag] = htmlspecialchars( $flag ); // Fallback |
297 | |
298 | // For grepping. The following core messages can be used here: |
299 | // * block-log-flags-angry-autoblock |
300 | // * block-log-flags-anononly |
301 | // * block-log-flags-hiddenname |
302 | // * block-log-flags-noautoblock |
303 | // * block-log-flags-nocreate |
304 | // * block-log-flags-noemail |
305 | // * block-log-flags-nousertalk |
306 | $msg = wfMessage( 'block-log-flags-' . $flag )->inLanguage( $lang ); |
307 | |
308 | if ( $msg->exists() ) { |
309 | $messages[$flag] = $msg->escaped(); |
310 | } |
311 | } |
312 | |
313 | return $messages[$flag]; |
314 | } |
315 | |
316 | protected function getParametersForApi() { |
317 | $entry = $this->entry; |
318 | $params = $entry->getParameters(); |
319 | |
320 | static $map = [ |
321 | // While this looks wrong to be starting at 5 rather than 4, it's |
322 | // because getMessageParameters uses $4 for its own purposes. |
323 | '5::duration', |
324 | '6:array:flags', |
325 | '6::flags' => '6:array:flags', |
326 | ]; |
327 | |
328 | foreach ( $map as $index => $key ) { |
329 | if ( isset( $params[$index] ) ) { |
330 | $params[$key] = $params[$index]; |
331 | unset( $params[$index] ); |
332 | } |
333 | } |
334 | |
335 | ksort( $params ); |
336 | |
337 | $subtype = $entry->getSubtype(); |
338 | if ( $subtype === 'block' || $subtype === 'reblock' ) { |
339 | // Defaults for old log entries missing some fields |
340 | $params += [ |
341 | '5::duration' => 'infinity', |
342 | '6:array:flags' => [], |
343 | ]; |
344 | |
345 | if ( !is_array( $params['6:array:flags'] ) ) { |
346 | // @phan-suppress-next-line PhanSuspiciousValueComparison |
347 | $params['6:array:flags'] = $params['6:array:flags'] === '' |
348 | ? [] |
349 | : explode( ',', $params['6:array:flags'] ); |
350 | } |
351 | |
352 | if ( wfIsInfinity( $params['5::duration'] ) ) { |
353 | // Normalize all possible values to one for pre-T241709 rows |
354 | $params['5::duration'] = 'infinity'; |
355 | $params[':plain:duration-l10n'] = $this->msg( 'infiniteblock' )->plain(); |
356 | } else { |
357 | $ts = (int)wfTimestamp( TS_UNIX, $entry->getTimestamp() ); |
358 | $expiry = strtotime( $params['5::duration'], $ts ); |
359 | if ( $expiry !== false && $expiry > 0 ) { |
360 | $params[':timestamp:expiry'] = $expiry; |
361 | } |
362 | $params[':plain:duration-l10n'] = $this->context->getLanguage() |
363 | ->formatDurationBetweenTimestamps( $ts, $expiry ); |
364 | } |
365 | } |
366 | |
367 | return $params; |
368 | } |
369 | |
370 | /** |
371 | * @inheritDoc |
372 | * @suppress PhanTypeInvalidDimOffset |
373 | */ |
374 | public function formatParametersForApi() { |
375 | $ret = parent::formatParametersForApi(); |
376 | if ( isset( $ret['flags'] ) ) { |
377 | ApiResult::setIndexedTagName( $ret['flags'], 'f' ); |
378 | } |
379 | |
380 | if ( isset( $ret['restrictions']['pages'] ) ) { |
381 | $ret['restrictions']['pages'] = array_map( function ( $title ) { |
382 | return $this->formatParameterValueForApi( 'page', 'title-link', $title ); |
383 | }, $ret['restrictions']['pages'] ); |
384 | ApiResult::setIndexedTagName( $ret['restrictions']['pages'], 'p' ); |
385 | } |
386 | |
387 | if ( isset( $ret['restrictions']['namespaces'] ) ) { |
388 | // @phan-suppress-next-line PhanTypeMismatchArgument False positive |
389 | ApiResult::setIndexedTagName( $ret['restrictions']['namespaces'], 'ns' ); |
390 | } |
391 | |
392 | return $ret; |
393 | } |
394 | |
395 | protected function getMessageKey() { |
396 | $type = $this->entry->getType(); |
397 | $subtype = $this->entry->getSubtype(); |
398 | $params = $this->entry->getParameters(); |
399 | $sitewide = $params['sitewide'] ?? true; |
400 | $count = $params['finalTargetCount'] ?? 0; |
401 | |
402 | $key = "logentry-$type-$subtype"; |
403 | if ( ( $subtype === 'block' || $subtype === 'reblock' ) && !$sitewide ) { |
404 | // $this->getMessageParameters is doing too much. We just need |
405 | // to check the presence of restrictions ($param[6]) and calling |
406 | // on parent gives us that |
407 | $params = parent::getMessageParameters(); |
408 | |
409 | // message changes depending on whether there are editing restrictions or not |
410 | if ( isset( $params[6] ) ) { |
411 | $key = "logentry-partial$type-$subtype"; |
412 | } else { |
413 | $key = "logentry-non-editing-$type-$subtype"; |
414 | } |
415 | } |
416 | if ( $subtype === 'block' && $count > 1 ) { |
417 | // logentry-block-block-multi, logentry-partialblock-block-multi, |
418 | // logentry-non-editing-block-block-multi |
419 | $key .= '-multi'; |
420 | } |
421 | |
422 | return $key; |
423 | } |
424 | } |
425 | |
426 | /** @deprecated class alias since 1.44 */ |
427 | class_alias( BlockLogFormatter::class, 'BlockLogFormatter' ); |