Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.97% covered (danger)
46.97%
155 / 330
8.33% covered (danger)
8.33%
1 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockListPager
47.11% covered (danger)
47.11%
155 / 329
8.33% covered (danger)
8.33%
1 / 12
731.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 getFieldNames
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 formatValue
85.00% covered (warning)
85.00%
85 / 100
0.00% covered (danger)
0.00%
0 / 1
24.79
 formatTarget
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
56
 getBlockChangeLinks
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
30
 getRestrictionListHTML
78.69% covered (warning)
78.69%
48 / 61
0.00% covered (danger)
0.00%
0 / 1
14.64
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
6
 getTableClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultSort
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isFieldSortable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 preprocessResults
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
9
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 * @ingroup Pager
20 */
21
22namespace MediaWiki\Pager;
23
24use MediaWiki\Block\Block;
25use MediaWiki\Block\BlockActionInfo;
26use MediaWiki\Block\BlockRestrictionStore;
27use MediaWiki\Block\BlockUtils;
28use MediaWiki\Block\HideUserUtils;
29use MediaWiki\Block\Restriction\ActionRestriction;
30use MediaWiki\Block\Restriction\NamespaceRestriction;
31use MediaWiki\Block\Restriction\PageRestriction;
32use MediaWiki\Block\Restriction\Restriction;
33use MediaWiki\Cache\LinkBatchFactory;
34use MediaWiki\CommentFormatter\RowCommentFormatter;
35use MediaWiki\CommentStore\CommentStore;
36use MediaWiki\Context\IContextSource;
37use MediaWiki\Html\Html;
38use MediaWiki\Linker\Linker;
39use MediaWiki\Linker\LinkRenderer;
40use MediaWiki\MainConfigNames;
41use MediaWiki\SpecialPage\SpecialPageFactory;
42use MediaWiki\User\UserIdentity;
43use MediaWiki\Utils\MWTimestamp;
44use stdClass;
45use Wikimedia\Rdbms\IConnectionProvider;
46use Wikimedia\Rdbms\IResultWrapper;
47
48/**
49 * @ingroup Pager
50 */
51class BlockListPager extends TablePager {
52
53    /** @var array */
54    protected $conds;
55
56    /**
57     * Array of restrictions.
58     *
59     * @var Restriction[]
60     */
61    protected $restrictions = [];
62
63    private BlockActionInfo $blockActionInfo;
64    private BlockRestrictionStore $blockRestrictionStore;
65    private BlockUtils $blockUtils;
66    private HideUserUtils $hideUserUtils;
67    private CommentStore $commentStore;
68    private LinkBatchFactory $linkBatchFactory;
69    private RowCommentFormatter $rowCommentFormatter;
70    private SpecialPageFactory $specialPageFactory;
71
72    /** @var string[] */
73    private $formattedComments = [];
74
75    /** @var string[] Cache of messages to avoid them being recreated for every row of the pager. */
76    private $messages = [];
77
78    /**
79     * @param IContextSource $context
80     * @param BlockActionInfo $blockActionInfo
81     * @param BlockRestrictionStore $blockRestrictionStore
82     * @param BlockUtils $blockUtils
83     * @param HideUserUtils $hideUserUtils
84     * @param CommentStore $commentStore
85     * @param LinkBatchFactory $linkBatchFactory
86     * @param LinkRenderer $linkRenderer
87     * @param IConnectionProvider $dbProvider
88     * @param RowCommentFormatter $rowCommentFormatter
89     * @param SpecialPageFactory $specialPageFactory
90     * @param array $conds
91     */
92    public function __construct(
93        IContextSource $context,
94        BlockActionInfo $blockActionInfo,
95        BlockRestrictionStore $blockRestrictionStore,
96        BlockUtils $blockUtils,
97        HideUserUtils $hideUserUtils,
98        CommentStore $commentStore,
99        LinkBatchFactory $linkBatchFactory,
100        LinkRenderer $linkRenderer,
101        IConnectionProvider $dbProvider,
102        RowCommentFormatter $rowCommentFormatter,
103        SpecialPageFactory $specialPageFactory,
104        $conds
105    ) {
106        // Set database before parent constructor to avoid setting it there
107        $this->mDb = $dbProvider->getReplicaDatabase();
108
109        parent::__construct( $context, $linkRenderer );
110
111        $this->blockActionInfo = $blockActionInfo;
112        $this->blockRestrictionStore = $blockRestrictionStore;
113        $this->blockUtils = $blockUtils;
114        $this->hideUserUtils = $hideUserUtils;
115        $this->commentStore = $commentStore;
116        $this->linkBatchFactory = $linkBatchFactory;
117        $this->rowCommentFormatter = $rowCommentFormatter;
118        $this->specialPageFactory = $specialPageFactory;
119        $this->conds = $conds;
120        $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
121    }
122
123    protected function getFieldNames() {
124        static $headers = null;
125
126        if ( $headers === null ) {
127            $headers = [
128                'bl_timestamp' => 'blocklist-timestamp',
129                'target' => 'blocklist-target',
130                'bl_expiry' => 'blocklist-expiry',
131                'by' => 'blocklist-by',
132                'params' => 'blocklist-params',
133                'bl_reason' => 'blocklist-reason',
134            ];
135            foreach ( $headers as $key => $val ) {
136                $headers[$key] = $this->msg( $val )->text();
137            }
138        }
139
140        return $headers;
141    }
142
143    /**
144     * @param string $name
145     * @param string|null $value
146     * @return string
147     */
148    public function formatValue( $name, $value ) {
149        if ( $this->messages === [] ) {
150            $keys = [
151                'anononlyblock',
152                'blanknamespace',
153                'createaccountblock',
154                'noautoblockblock',
155                'emailblock',
156                'blocklist-nousertalk',
157                'unblocklink',
158                'remove-blocklink',
159                'change-blocklink',
160                'blocklist-editing',
161                'blocklist-editing-sitewide',
162                'blocklist-hidden-param',
163                'blocklist-hidden-placeholder',
164            ];
165
166            foreach ( $keys as $key ) {
167                $this->messages[$key] = $this->msg( $key )->text();
168            }
169        }
170
171        /** @var stdClass $row */
172        $row = $this->mCurrentRow;
173
174        $language = $this->getLanguage();
175
176        $linkRenderer = $this->getLinkRenderer();
177
178        switch ( $name ) {
179            case 'bl_timestamp':
180                // Link the timestamp to the block ID. This allows users without permissions to change blocks
181                // to be able to generate a link to a specific block.
182                $formatted = $linkRenderer->makeKnownLink(
183                    $this->specialPageFactory->getTitleForAlias( 'BlockList' ),
184                    $language->userTimeAndDate( $value, $this->getUser() ),
185                    [],
186                    [ 'wpTarget' => "#{$row->bl_id}" ],
187                );
188                break;
189
190            case 'target':
191                $formatted = $this->formatTarget( $row );
192                break;
193
194            case 'bl_expiry':
195                $formatted = htmlspecialchars( $language->formatExpiry(
196                    $value,
197                    /* User preference timezone */true,
198                    'infinity',
199                    $this->getUser()
200                ) );
201                if ( $this->getAuthority()->isAllowed( 'block' ) ) {
202                    $links = $this->getBlockChangeLinks( $row );
203                    $formatted .= ' ' . Html::rawElement(
204                        'span',
205                        [ 'class' => 'mw-blocklist-actions' ],
206                        $this->msg( 'parentheses' )->rawParams(
207                            $language->pipeList( $links ) )->escaped()
208                    );
209                }
210                if ( $value !== 'infinity' ) {
211                    $timestamp = new MWTimestamp( $value );
212                    $formatted .= '<br />' . $this->msg(
213                        'ipb-blocklist-duration-left',
214                        $language->formatDurationBetweenTimestamps(
215                            (int)$timestamp->getTimestamp( TS_UNIX ),
216                            MWTimestamp::time(),
217                            4
218                        )
219                    )->escaped();
220                }
221                break;
222
223            case 'by':
224                $formatted = Linker::userLink( (int)$value, $row->bl_by_text );
225                $formatted .= Linker::userToolLinks( (int)$value, $row->bl_by_text );
226                break;
227
228            case 'bl_reason':
229                $formatted = $this->formattedComments[$this->getResultOffset()];
230                break;
231
232            case 'params':
233                $properties = [];
234
235                if ( $row->bl_deleted ) {
236                    $properties[] = htmlspecialchars( $this->messages['blocklist-hidden-param' ] );
237                }
238                if ( $row->bl_sitewide ) {
239                    $properties[] = htmlspecialchars( $this->messages['blocklist-editing-sitewide'] );
240                }
241
242                if ( !$row->bl_sitewide && $this->restrictions ) {
243                    $list = $this->getRestrictionListHTML( $row );
244                    if ( $list ) {
245                        $properties[] = htmlspecialchars( $this->messages['blocklist-editing'] ) . $list;
246                    }
247                }
248
249                if ( $row->bl_anon_only ) {
250                    $properties[] = htmlspecialchars( $this->messages['anononlyblock'] );
251                }
252                if ( $row->bl_create_account ) {
253                    $properties[] = htmlspecialchars( $this->messages['createaccountblock'] );
254                }
255                if ( $row->bt_user && !$row->bl_enable_autoblock ) {
256                    $properties[] = htmlspecialchars( $this->messages['noautoblockblock'] );
257                }
258
259                if ( $row->bl_block_email ) {
260                    $properties[] = htmlspecialchars( $this->messages['emailblock'] );
261                }
262
263                if ( !$row->bl_allow_usertalk ) {
264                    $properties[] = htmlspecialchars( $this->messages['blocklist-nousertalk'] );
265                }
266
267                $formatted = Html::rawElement(
268                    'ul',
269                    [],
270                    implode( '', array_map( static function ( $prop ) {
271                        return Html::rawElement(
272                            'li',
273                            [],
274                            $prop
275                        );
276                    }, $properties ) )
277                );
278                break;
279
280            default:
281                $formatted = "Unable to format $name";
282                break;
283        }
284
285        return $formatted;
286    }
287
288    /**
289     * Format the target field
290     * @param stdClass $row
291     * @return string
292     */
293    private function formatTarget( $row ) {
294        if ( $row->bt_auto ) {
295            return $this->msg( 'autoblockid', $row->bl_id )->parse();
296        }
297
298        [ $target, $type ] = $this->blockUtils->parseBlockTargetRow( $row );
299
300        if ( $type === Block::TYPE_RANGE ) {
301            $userId = 0;
302            $userName = $target;
303        } elseif ( ( $row->hu_deleted ?? null )
304            && !$this->getAuthority()->isAllowed( 'hideuser' )
305        ) {
306            return Html::element(
307                'span',
308                [ 'class' => 'mw-blocklist-hidden' ],
309                $this->messages['blocklist-hidden-placeholder']
310            );
311        } elseif ( $target instanceof UserIdentity ) {
312            $userId = $target->getId();
313            $userName = $target->getName();
314        } elseif ( is_string( $target ) ) {
315            return htmlspecialchars( $target );
316        } else {
317            return $this->msg( 'empty-username' )->escaped();
318        }
319        return Linker::userLink( $userId, $userName ) .
320            Linker::userToolLinks(
321                $userId,
322                $userName,
323                false,
324                Linker::TOOL_LINKS_NOBLOCK
325            );
326    }
327
328    /**
329     * Get unblock and change-block links.
330     *
331     * @param stdClass $row Block data.
332     * @return string[] Array of HTML links.
333     */
334    private function getBlockChangeLinks( $row ): array {
335        $linkRenderer = $this->getLinkRenderer();
336        $links = [];
337        if ( $row->bt_auto ) {
338            $target = "#{$row->bl_id}";
339        } else {
340            $target = $row->bt_address ?? $row->bt_user_text;
341        }
342        if ( $this->getConfig()->get( MainConfigNames::UseCodexSpecialBlock ) ) {
343            $query = [ 'id' => $row->bl_id ];
344            if ( $row->bt_auto ) {
345                $links[] = $linkRenderer->makeKnownLink(
346                    $this->specialPageFactory->getTitleForAlias( 'Block' ),
347                    $this->messages['remove-blocklink'],
348                    [],
349                    $query + [
350                        'wpTarget' => $target,
351                        'remove' => '1'
352                    ]
353                );
354            } else {
355                $specialBlock = $this->specialPageFactory->getTitleForAlias( "Block/$target" );
356                $links[] = $linkRenderer->makeKnownLink(
357                    $specialBlock,
358                    $this->messages['remove-blocklink'],
359                    [],
360                    $query + [ 'remove' => '1' ]
361                );
362                $links[] = $linkRenderer->makeKnownLink(
363                    $specialBlock,
364                    $this->messages['change-blocklink'],
365                    [],
366                    $query
367                );
368            }
369        } else {
370            if ( $row->bt_auto ) {
371                $links[] = $linkRenderer->makeKnownLink(
372                    $this->specialPageFactory->getTitleForAlias( 'Unblock' ),
373                    $this->messages['unblocklink'],
374                    [],
375                    [ 'wpTarget' => "#{$row->bl_id}" ]
376                );
377            } else {
378                $links[] = $linkRenderer->makeKnownLink(
379                    $this->specialPageFactory->getTitleForAlias( "Unblock/$target" ),
380                    $this->messages['unblocklink']
381                );
382                $links[] = $linkRenderer->makeKnownLink(
383                    $this->specialPageFactory->getTitleForAlias( "Block/$target" ),
384                    $this->messages['change-blocklink']
385                );
386            }
387        }
388        return $links;
389    }
390
391    /**
392     * Get Restriction List HTML
393     *
394     * @param stdClass $row
395     *
396     * @return string
397     */
398    private function getRestrictionListHTML( stdClass $row ) {
399        $items = [];
400        $linkRenderer = $this->getLinkRenderer();
401
402        foreach ( $this->restrictions as $restriction ) {
403            if ( $restriction->getBlockId() !== (int)$row->bl_id ) {
404                continue;
405            }
406
407            switch ( $restriction->getType() ) {
408                case PageRestriction::TYPE:
409                    '@phan-var PageRestriction $restriction';
410                    if ( $restriction->getTitle() ) {
411                        $items[$restriction->getType()][] = Html::rawElement(
412                            'li',
413                            [],
414                            $linkRenderer->makeLink( $restriction->getTitle() )
415                        );
416                    }
417                    break;
418                case NamespaceRestriction::TYPE:
419                    $text = $restriction->getValue() === NS_MAIN
420                        ? $this->messages['blanknamespace']
421                        : $this->getLanguage()->getFormattedNsText(
422                            $restriction->getValue()
423                        );
424                    if ( $text ) {
425                        $items[$restriction->getType()][] = Html::rawElement(
426                            'li',
427                            [],
428                            $linkRenderer->makeLink(
429                                $this->specialPageFactory->getTitleForAlias( 'Allpages' ),
430                                $text,
431                                [],
432                                [
433                                    'namespace' => $restriction->getValue()
434                                ]
435                            )
436                        );
437                    }
438                    break;
439                case ActionRestriction::TYPE:
440                    $actionName = $this->blockActionInfo->getActionFromId( $restriction->getValue() );
441                    $enablePartialActionBlocks =
442                        $this->getConfig()->get( MainConfigNames::EnablePartialActionBlocks );
443                    if ( $actionName && $enablePartialActionBlocks ) {
444                        $items[$restriction->getType()][] = Html::rawElement(
445                            'li',
446                            [],
447                            // The following messages may be used here:
448                            // * ipb-action-create
449                            // * ipb-action-move
450                            // * ipb-action-upload
451                            $this->msg( 'ipb-action-' .
452                                $this->blockActionInfo->getActionFromId( $restriction->getValue() ) )->escaped()
453                        );
454                    }
455                    break;
456            }
457        }
458
459        if ( !$items ) {
460            return '';
461        }
462
463        $sets = [];
464        foreach ( $items as $key => $value ) {
465            $sets[] = Html::rawElement(
466                'li',
467                [],
468                // The following messages may be used here:
469                // * blocklist-editing-sitewide
470                // * blocklist-editing-page
471                // * blocklist-editing-ns
472                // * blocklist-editing-action
473                $this->msg( 'blocklist-editing-' . $key ) . Html::rawElement(
474                    'ul',
475                    [],
476                    implode( '', $value )
477                )
478            );
479        }
480
481        return Html::rawElement(
482            'ul',
483            [],
484            implode( '', $sets )
485        );
486    }
487
488    public function getQueryInfo() {
489        $db = $this->getDatabase();
490        $commentQuery = $this->commentStore->getJoin( 'bl_reason' );
491        $info = [
492            'tables' => array_merge(
493                [
494                    'block',
495                    'block_by_actor' => 'actor',
496                    'block_target',
497                ],
498                $commentQuery['tables']
499            ),
500            'fields' => [
501                // The target fields should be those accepted by BlockUtils::parseBlockTargetRow()
502                'bt_address',
503                'bt_user_text',
504                'bt_user',
505                'bt_auto',
506                'bt_range_start',
507                'bt_range_end',
508                // Block fields and aliases
509                'bl_id',
510                'bl_by' => 'block_by_actor.actor_user',
511                'bl_by_text' => 'block_by_actor.actor_name',
512                'bl_timestamp',
513                'bl_anon_only',
514                'bl_create_account',
515                'bl_enable_autoblock',
516                'bl_expiry',
517                'bl_deleted',
518                'bl_block_email',
519                'bl_allow_usertalk',
520                'bl_sitewide',
521            ] + $commentQuery['fields'],
522            'conds' => $this->conds,
523            'join_conds' => [
524                'block_by_actor' => [ 'JOIN', 'actor_id=bl_by_actor' ],
525                'block_target' => [ 'JOIN', 'bt_id=bl_target' ],
526            ] + $commentQuery['joins']
527        ];
528
529        # Filter out any expired blocks
530        $info['conds'][] = $db->expr( 'bl_expiry', '>', $db->timestamp() );
531
532        # Filter out blocks with the deleted option if the user doesn't
533        # have permission to see hidden users
534        # TODO: consider removing this -- we could just redact them instead.
535        # The mere fact that an admin has deleted a user does not need to
536        # be private and could be included in block lists and logs for
537        # transparency purposes. Previously, filtering out deleted blocks
538        # was a convenient way to avoid showing the target name.
539        if ( !$this->getAuthority()->isAllowed( 'hideuser' ) ) {
540            $info['conds']['bl_deleted'] = 0;
541        }
542
543        # Determine if the user is hidden
544        # With multiblocks we can't just rely on bl_deleted in the row being formatted
545        $info['fields']['hu_deleted'] = $this->hideUserUtils->getExpression(
546            $db,
547            'block_target.bt_user',
548            HideUserUtils::HIDDEN_USERS );
549        return $info;
550    }
551
552    protected function getTableClass() {
553        return parent::getTableClass() . ' mw-blocklist';
554    }
555
556    public function getIndexField() {
557        return [ [ 'bl_timestamp', 'bl_id' ] ];
558    }
559
560    public function getDefaultSort() {
561        return '';
562    }
563
564    protected function isFieldSortable( $name ) {
565        return false;
566    }
567
568    /**
569     * Do a LinkBatch query to minimise database load when generating all these links
570     * @param IResultWrapper $result
571     */
572    public function preprocessResults( $result ) {
573        // Do a link batch query
574        $lb = $this->linkBatchFactory->newLinkBatch();
575        $lb->setCaller( __METHOD__ );
576
577        $partialBlocks = [];
578        foreach ( $result as $row ) {
579            $target = $row->bt_address ?? $row->bt_user_text;
580            if ( $target !== null ) {
581                $lb->add( NS_USER, $target );
582                $lb->add( NS_USER_TALK, $target );
583            }
584
585            if ( isset( $row->bl_by_text ) ) {
586                $lb->add( NS_USER, $row->bl_by_text );
587                $lb->add( NS_USER_TALK, $row->bl_by_text );
588            }
589
590            if ( !$row->bl_sitewide ) {
591                $partialBlocks[] = (int)$row->bl_id;
592            }
593        }
594
595        if ( $partialBlocks ) {
596            // Mutations to the $row object are not persisted. The restrictions will
597            // need be stored in a separate store.
598            $this->restrictions = $this->blockRestrictionStore->loadByBlockId( $partialBlocks );
599
600            foreach ( $this->restrictions as $restriction ) {
601                if ( $restriction->getType() === PageRestriction::TYPE ) {
602                    '@phan-var PageRestriction $restriction';
603                    $title = $restriction->getTitle();
604                    if ( $title ) {
605                        $lb->addObj( $title );
606                    }
607                }
608            }
609        }
610
611        $lb->execute();
612
613        // Format comments
614        // The keys of formattedComments will be the corresponding offset into $result
615        $this->formattedComments = $this->rowCommentFormatter->formatRows( $result, 'bl_reason' );
616    }
617
618}
619
620/**
621 * Retain the old class name for backwards compatibility.
622 * @deprecated since 1.41
623 */
624class_alias( BlockListPager::class, 'BlockListPager' );