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