Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.04% covered (warning)
73.04%
149 / 204
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockLogFormatter
73.40% covered (warning)
73.40%
149 / 203
70.00% covered (warning)
70.00%
7 / 10
137.71
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getMessageParameters
88.06% covered (warning)
88.06%
59 / 67
0.00% covered (danger)
0.00%
0 / 1
17.49
 extractParameters
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getPreloadTitles
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
6.97
 getActionLinks
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
72
 formatBlockFlags
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 formatBlockFlag
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getParametersForApi
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
10
 formatParametersForApi
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 getMessageKey
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
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
25namespace MediaWiki\Logging;
26
27use MediaWiki\Api\ApiResult;
28use MediaWiki\Language\Language;
29use MediaWiki\Linker\Linker;
30use MediaWiki\MainConfigNames;
31use MediaWiki\Message\Message;
32use MediaWiki\SpecialPage\SpecialPage;
33use MediaWiki\Title\MalformedTitleException;
34use MediaWiki\Title\NamespaceInfo;
35use MediaWiki\Title\Title;
36use MediaWiki\Title\TitleParser;
37use MediaWiki\User\User;
38
39/**
40 * This class formats block log entries.
41 *
42 * @since 1.25
43 */
44class 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 = '&lrm;' . 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 */
427class_alias( BlockLogFormatter::class, 'BlockLogFormatter' );