Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.17% covered (warning)
79.17%
133 / 168
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockLogFormatter
79.17% covered (warning)
79.17%
133 / 168
44.44% covered (danger)
44.44%
4 / 9
84.36
0.00% covered (danger)
0.00%
0 / 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
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
5.27
 getActionLinks
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 formatBlockFlags
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 formatBlockFlag
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getParametersForApi
82.76% covered (warning)
82.76%
24 / 29
0.00% covered (danger)
0.00%
0 / 1
10.51
 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%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
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
25use MediaWiki\Linker\Linker;
26use MediaWiki\MainConfigNames;
27use MediaWiki\Message\Message;
28use MediaWiki\SpecialPage\SpecialPage;
29use MediaWiki\Title\Title;
30use MediaWiki\User\User;
31
32/**
33 * This class formats block log entries.
34 *
35 * @since 1.25
36 */
37class BlockLogFormatter extends LogFormatter {
38    protected function getMessageParameters() {
39        $params = parent::getMessageParameters();
40
41        $title = $this->entry->getTarget();
42        if ( substr( $title->getText(), 0, 1 ) === '#' ) {
43            // autoblock - no user link possible
44            $params[2] = $title->getText();
45            $params[3] = ''; // no user name for gender use
46        } else {
47            // Create a user link for the blocked
48            $username = $title->getText();
49            // @todo Store the user identifier in the parameters
50            // to make this faster for future log entries
51            $targetUser = User::newFromName( $username, false );
52            $params[2] = Message::rawParam( $this->makeUserLink( $targetUser, Linker::TOOL_LINKS_NOBLOCK ) );
53            $params[3] = $username; // plain user name for gender use
54        }
55
56        $subtype = $this->entry->getSubtype();
57        if ( $subtype === 'block' || $subtype === 'reblock' ) {
58            if ( !isset( $params[4] ) ) {
59                // Very old log entry without duration: means infinity
60                $params[4] = 'infinity';
61            }
62            // Localize the duration, and add a tooltip
63            // in English to help visitors from other wikis.
64            // The lrm is needed to make sure that the number
65            // is shown on the correct side of the tooltip text.
66            // @phan-suppress-next-line SecurityCheck-DoubleEscaped
67            $durationTooltip = '&lrm;' . htmlspecialchars( $params[4] );
68            $blockExpiry = $this->context->getLanguage()->translateBlockExpiry(
69                $params[4],
70                $this->context->getUser(),
71                (int)wfTimestamp( TS_UNIX, $this->entry->getTimestamp() )
72            );
73            if ( $this->plaintext ) {
74                // @phan-suppress-next-line SecurityCheck-XSS Unlikely positive, only if language format is bad
75                $params[4] = Message::rawParam( $blockExpiry );
76            } else {
77                $params[4] = Message::rawParam(
78                    "<span class=\"blockExpiry\" title=\"$durationTooltip\">" .
79                    // @phan-suppress-next-line SecurityCheck-DoubleEscaped language class does not escape
80                    htmlspecialchars( $blockExpiry ) .
81                    '</span>'
82                );
83            }
84            $params[5] = isset( $params[5] ) ?
85                self::formatBlockFlags( $params[5], $this->context->getLanguage() ) : '';
86
87            // block restrictions
88            if ( isset( $params[6] ) ) {
89                $pages = $params[6]['pages'] ?? [];
90                $pageLinks = [];
91                foreach ( $pages as $page ) {
92                    $pageLinks[] = $this->makePageLink( Title::newFromText( $page ) );
93                }
94
95                $rawNamespaces = $params[6]['namespaces'] ?? [];
96                $namespaces = [];
97                foreach ( $rawNamespaces as $ns ) {
98                    $text = (int)$ns === NS_MAIN
99                        ? $this->msg( 'blanknamespace' )->escaped()
100                        : htmlspecialchars( $this->context->getLanguage()->getFormattedNsText( $ns ) );
101                    if ( $this->plaintext ) {
102                        // Because the plaintext cannot link to the Special:AllPages
103                        // link that is linked to in non-plaintext mode, just return
104                        // the name of the namespace.
105                        $namespaces[] = $text;
106                    } else {
107                        $namespaces[] = $this->makePageLink(
108                            SpecialPage::getTitleFor( 'Allpages' ),
109                            [ 'namespace' => $ns ],
110                            $text
111                        );
112                    }
113                }
114
115                $rawActions = $params[6]['actions'] ?? [];
116                $actions = [];
117                foreach ( $rawActions as $action ) {
118                    $actions[] = $this->msg( 'ipb-action-' . $action )->escaped();
119                }
120
121                $restrictions = [];
122                if ( $pageLinks ) {
123                    $restrictions[] = $this->msg( 'logentry-partialblock-block-page' )
124                        ->numParams( count( $pageLinks ) )
125                        ->rawParams( $this->context->getLanguage()->listToText( $pageLinks ) )->escaped();
126                }
127
128                if ( $namespaces ) {
129                    $restrictions[] = $this->msg( 'logentry-partialblock-block-ns' )
130                        ->numParams( count( $namespaces ) )
131                        ->rawParams( $this->context->getLanguage()->listToText( $namespaces ) )->escaped();
132                }
133                $enablePartialActionBlocks = $this->context->getConfig()
134                    ->get( MainConfigNames::EnablePartialActionBlocks );
135                if ( $actions && $enablePartialActionBlocks ) {
136                    $restrictions[] = $this->msg( 'logentry-partialblock-block-action' )
137                        ->numParams( count( $actions ) )
138                        ->rawParams( $this->context->getLanguage()->listToText( $actions ) )->escaped();
139                }
140
141                $params[6] = Message::rawParam( $this->context->getLanguage()->listToText( $restrictions ) );
142            }
143        }
144
145        return $params;
146    }
147
148    protected function extractParameters() {
149        $params = parent::extractParameters();
150        // Legacy log params returning the params in index 3 and 4, moved to 4 and 5
151        if ( $this->entry->isLegacy() && isset( $params[3] ) ) {
152            if ( isset( $params[4] ) ) {
153                $params[5] = $params[4];
154            }
155            $params[4] = $params[3];
156            $params[3] = '';
157        }
158        return $params;
159    }
160
161    public function getPreloadTitles() {
162        $title = $this->entry->getTarget();
163        $preload = [];
164        // Preload user page for non-autoblocks
165        if ( substr( $title->getText(), 0, 1 ) !== '#' && $title->canExist() ) {
166            $preload[] = $title->getTalkPage();
167        }
168        // Preload page restriction
169        $params = $this->extractParameters();
170        if ( isset( $params[6]['pages'] ) ) {
171            foreach ( $params[6]['pages'] as $page ) {
172                $preload[] = Title::newFromText( $page );
173            }
174        }
175        return $preload;
176    }
177
178    public function getActionLinks() {
179        $subtype = $this->entry->getSubtype();
180        $linkRenderer = $this->getLinkRenderer();
181        if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden
182            || !( $subtype === 'block' || $subtype === 'reblock' )
183            || !$this->context->getAuthority()->isAllowed( 'block' )
184        ) {
185            return '';
186        }
187
188        // Show unblock/change block link
189        $title = $this->entry->getTarget();
190        $links = [
191            $linkRenderer->makeKnownLink(
192                SpecialPage::getTitleFor( 'Unblock', $title->getDBkey() ),
193                $this->msg( 'unblocklink' )->text()
194            ),
195            $linkRenderer->makeKnownLink(
196                SpecialPage::getTitleFor( 'Block', $title->getDBkey() ),
197                $this->msg( 'change-blocklink' )->text()
198            )
199        ];
200
201        return $this->msg( 'parentheses' )->rawParams(
202            $this->context->getLanguage()->pipeList( $links ) )->escaped();
203    }
204
205    /**
206     * Convert a comma-delimited list of block log flags
207     * into a more readable (and translated) form
208     *
209     * @param string $flags Flags to format
210     * @param Language $lang
211     * @return string
212     */
213    public static function formatBlockFlags( $flags, Language $lang ) {
214        $flags = trim( $flags );
215        if ( $flags === '' ) {
216            return ''; // nothing to do
217        }
218        $flags = explode( ',', $flags );
219        $flagsCount = count( $flags );
220
221        for ( $i = 0; $i < $flagsCount; $i++ ) {
222            $flags[$i] = self::formatBlockFlag( $flags[$i], $lang );
223        }
224
225        return wfMessage( 'parentheses' )->inLanguage( $lang )
226            ->rawParams( $lang->commaList( $flags ) )->escaped();
227    }
228
229    /**
230     * Translate a block log flag if possible
231     *
232     * @param string $flag Flag to translate
233     * @param Language $lang Language object to use
234     * @return string
235     */
236    public static function formatBlockFlag( $flag, Language $lang ) {
237        static $messages = [];
238
239        if ( !isset( $messages[$flag] ) ) {
240            $messages[$flag] = htmlspecialchars( $flag ); // Fallback
241
242            // For grepping. The following core messages can be used here:
243            // * block-log-flags-angry-autoblock
244            // * block-log-flags-anononly
245            // * block-log-flags-hiddenname
246            // * block-log-flags-noautoblock
247            // * block-log-flags-nocreate
248            // * block-log-flags-noemail
249            // * block-log-flags-nousertalk
250            $msg = wfMessage( 'block-log-flags-' . $flag )->inLanguage( $lang );
251
252            if ( $msg->exists() ) {
253                $messages[$flag] = $msg->escaped();
254            }
255        }
256
257        return $messages[$flag];
258    }
259
260    protected function getParametersForApi() {
261        $entry = $this->entry;
262        $params = $entry->getParameters();
263
264        static $map = [
265            // While this looks wrong to be starting at 5 rather than 4, it's
266            // because getMessageParameters uses $4 for its own purposes.
267            '5::duration',
268            '6:array:flags',
269            '6::flags' => '6:array:flags',
270        ];
271
272        foreach ( $map as $index => $key ) {
273            if ( isset( $params[$index] ) ) {
274                $params[$key] = $params[$index];
275                unset( $params[$index] );
276            }
277        }
278
279        ksort( $params );
280
281        $subtype = $entry->getSubtype();
282        if ( $subtype === 'block' || $subtype === 'reblock' ) {
283            // Defaults for old log entries missing some fields
284            $params += [
285                '5::duration' => 'infinity',
286                '6:array:flags' => [],
287            ];
288
289            if ( !is_array( $params['6:array:flags'] ) ) {
290                // @phan-suppress-next-line PhanSuspiciousValueComparison
291                $params['6:array:flags'] = $params['6:array:flags'] === ''
292                    ? []
293                    : explode( ',', $params['6:array:flags'] );
294            }
295
296            if ( wfIsInfinity( $params['5::duration'] ) ) {
297                // Normalize all possible values to one for pre-T241709 rows
298                $params['5::duration'] = 'infinity';
299            } else {
300                $ts = (int)wfTimestamp( TS_UNIX, $entry->getTimestamp() );
301                $expiry = strtotime( $params['5::duration'], $ts );
302                if ( $expiry !== false && $expiry > 0 ) {
303                    $params[':timestamp:expiry'] = $expiry;
304                }
305            }
306        }
307
308        return $params;
309    }
310
311    /**
312     * @inheritDoc
313     * @suppress PhanTypeInvalidDimOffset
314     */
315    public function formatParametersForApi() {
316        $ret = parent::formatParametersForApi();
317        if ( isset( $ret['flags'] ) ) {
318            ApiResult::setIndexedTagName( $ret['flags'], 'f' );
319        }
320
321        if ( isset( $ret['restrictions']['pages'] ) ) {
322            $ret['restrictions']['pages'] = array_map( function ( $title ) {
323                return $this->formatParameterValueForApi( 'page', 'title-link', $title );
324            }, $ret['restrictions']['pages'] );
325            ApiResult::setIndexedTagName( $ret['restrictions']['pages'], 'p' );
326        }
327
328        if ( isset( $ret['restrictions']['namespaces'] ) ) {
329            // @phan-suppress-next-line PhanTypeMismatchArgument False positive
330            ApiResult::setIndexedTagName( $ret['restrictions']['namespaces'], 'ns' );
331        }
332
333        return $ret;
334    }
335
336    protected function getMessageKey() {
337        $type = $this->entry->getType();
338        $subtype = $this->entry->getSubtype();
339        $sitewide = $this->entry->getParameters()['sitewide'] ?? true;
340
341        $key = "logentry-$type-$subtype";
342        if ( ( $subtype === 'block' || $subtype === 'reblock' ) && !$sitewide ) {
343            // $this->getMessageParameters is doing too much. We just need
344            // to check the presence of restrictions ($param[6]) and calling
345            // on parent gives us that
346            $params = parent::getMessageParameters();
347
348            // message changes depending on whether there are editing restrictions or not
349            if ( isset( $params[6] ) ) {
350                $key = "logentry-partial$type-$subtype";
351            } else {
352                $key = "logentry-non-editing-$type-$subtype";
353            }
354        }
355
356        return $key;
357    }
358}