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.
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.
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
22 namespace MediaWiki\Pager
;
24 use MediaWiki\Block\Block
;
25 use MediaWiki\Block\BlockActionInfo
;
26 use MediaWiki\Block\BlockRestrictionStore
;
27 use MediaWiki\Block\BlockUtils
;
28 use MediaWiki\Block\HideUserUtils
;
29 use MediaWiki\Block\Restriction\ActionRestriction
;
30 use MediaWiki\Block\Restriction\NamespaceRestriction
;
31 use MediaWiki\Block\Restriction\PageRestriction
;
32 use MediaWiki\Block\Restriction\Restriction
;
33 use MediaWiki\Cache\LinkBatchFactory
;
34 use MediaWiki\CommentFormatter\RowCommentFormatter
;
35 use MediaWiki\CommentStore\CommentStore
;
36 use MediaWiki\Context\IContextSource
;
37 use MediaWiki\Html\Html
;
38 use MediaWiki\Linker\Linker
;
39 use MediaWiki\Linker\LinkRenderer
;
40 use MediaWiki\MainConfigNames
;
41 use MediaWiki\SpecialPage\SpecialPageFactory
;
42 use MediaWiki\User\UserIdentity
;
43 use MediaWiki\Utils\MWTimestamp
;
45 use Wikimedia\Rdbms\IConnectionProvider
;
46 use Wikimedia\Rdbms\IResultWrapper
;
51 class BlockListPager
extends TablePager
{
57 * Array of restrictions.
61 protected $restrictions = [];
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;
73 private $formattedComments = [];
76 * @param IContextSource $context
77 * @param BlockActionInfo $blockActionInfo
78 * @param BlockRestrictionStore $blockRestrictionStore
79 * @param BlockUtils $blockUtils
80 * @param HideUserUtils $hideUserUtils
81 * @param CommentStore $commentStore
82 * @param LinkBatchFactory $linkBatchFactory
83 * @param LinkRenderer $linkRenderer
84 * @param IConnectionProvider $dbProvider
85 * @param RowCommentFormatter $rowCommentFormatter
86 * @param SpecialPageFactory $specialPageFactory
89 public function __construct(
90 IContextSource
$context,
91 BlockActionInfo
$blockActionInfo,
92 BlockRestrictionStore
$blockRestrictionStore,
93 BlockUtils
$blockUtils,
94 HideUserUtils
$hideUserUtils,
95 CommentStore
$commentStore,
96 LinkBatchFactory
$linkBatchFactory,
97 LinkRenderer
$linkRenderer,
98 IConnectionProvider
$dbProvider,
99 RowCommentFormatter
$rowCommentFormatter,
100 SpecialPageFactory
$specialPageFactory,
103 // Set database before parent constructor to avoid setting it there
104 $this->mDb
= $dbProvider->getReplicaDatabase();
106 parent
::__construct( $context, $linkRenderer );
108 $this->blockActionInfo
= $blockActionInfo;
109 $this->blockRestrictionStore
= $blockRestrictionStore;
110 $this->blockUtils
= $blockUtils;
111 $this->hideUserUtils
= $hideUserUtils;
112 $this->commentStore
= $commentStore;
113 $this->linkBatchFactory
= $linkBatchFactory;
114 $this->rowCommentFormatter
= $rowCommentFormatter;
115 $this->specialPageFactory
= $specialPageFactory;
116 $this->conds
= $conds;
117 $this->mDefaultDirection
= IndexPager
::DIR_DESCENDING
;
120 protected function getFieldNames() {
121 static $headers = null;
123 if ( $headers === null ) {
125 'bl_timestamp' => 'blocklist-timestamp',
126 'target' => 'blocklist-target',
127 'bl_expiry' => 'blocklist-expiry',
128 'by' => 'blocklist-by',
129 'params' => 'blocklist-params',
130 'bl_reason' => 'blocklist-reason',
132 foreach ( $headers as $key => $val ) {
133 $headers[$key] = $this->msg( $val )->text();
141 * @param string $name
142 * @param string|null $value
144 * @suppress PhanTypeArraySuspicious
146 public function formatValue( $name, $value ) {
148 if ( $msg === null ) {
151 'createaccountblock',
154 'blocklist-nousertalk',
158 'blocklist-editing-sitewide',
159 'blocklist-hidden-param',
162 foreach ( $keys as $key ) {
163 $msg[$key] = $this->msg( $key )->text();
166 '@phan-var string[] $msg';
168 /** @var stdClass $row */
169 $row = $this->mCurrentRow
;
171 $language = $this->getLanguage();
173 $linkRenderer = $this->getLinkRenderer();
177 // Link the timestamp to the block ID. This allows users without permissions to change blocks
178 // to be able to generate a link to a specific block.
179 $formatted = $linkRenderer->makeKnownLink(
180 $this->specialPageFactory
->getTitleForAlias( 'BlockList' ),
181 $language->userTimeAndDate( $value, $this->getUser() ),
183 [ 'wpTarget' => "#{$row->bl_id}" ],
188 $formatted = $this->formatTarget( $row );
192 $formatted = htmlspecialchars( $language->formatExpiry(
194 /* User preference timezone */true,
198 if ( $this->getAuthority()->isAllowed( 'block' ) ) {
200 if ( $row->bt_auto
) {
201 $links[] = $linkRenderer->makeKnownLink(
202 $this->specialPageFactory
->getTitleForAlias( 'Unblock' ),
205 [ 'wpTarget' => "#{$row->bl_id}" ]
208 $target = $row->bt_address ??
$row->bt_user_text
;
209 $links[] = $linkRenderer->makeKnownLink(
210 $this->specialPageFactory
->getTitleForAlias( "Unblock/$target" ),
213 $links[] = $linkRenderer->makeKnownLink(
214 $this->specialPageFactory
->getTitleForAlias( "Block/$target" ),
215 $msg['change-blocklink']
218 $formatted .= ' ' . Html
::rawElement(
220 [ 'class' => 'mw-blocklist-actions' ],
221 $this->msg( 'parentheses' )->rawParams(
222 $language->pipeList( $links ) )->escaped()
225 if ( $value !== 'infinity' ) {
226 $timestamp = new MWTimestamp( $value );
227 $formatted .= '<br />' . $this->msg(
228 'ipb-blocklist-duration-left',
229 $language->formatDurationBetweenTimestamps(
230 (int)$timestamp->getTimestamp( TS_UNIX
),
239 $formatted = Linker
::userLink( (int)$value, $row->bl_by_text
);
240 $formatted .= Linker
::userToolLinks( (int)$value, $row->bl_by_text
);
244 $formatted = $this->formattedComments
[$this->getResultOffset()];
250 if ( $row->bl_deleted
) {
251 $properties[] = htmlspecialchars( $msg['blocklist-hidden-param' ] );
253 if ( $row->bl_sitewide
) {
254 $properties[] = htmlspecialchars( $msg['blocklist-editing-sitewide'] );
257 if ( !$row->bl_sitewide
&& $this->restrictions
) {
258 $list = $this->getRestrictionListHTML( $row );
260 $properties[] = htmlspecialchars( $msg['blocklist-editing'] ) . $list;
264 if ( $row->bl_anon_only
) {
265 $properties[] = htmlspecialchars( $msg['anononlyblock'] );
267 if ( $row->bl_create_account
) {
268 $properties[] = htmlspecialchars( $msg['createaccountblock'] );
270 if ( $row->bt_user
&& !$row->bl_enable_autoblock
) {
271 $properties[] = htmlspecialchars( $msg['noautoblockblock'] );
274 if ( $row->bl_block_email
) {
275 $properties[] = htmlspecialchars( $msg['emailblock'] );
278 if ( !$row->bl_allow_usertalk
) {
279 $properties[] = htmlspecialchars( $msg['blocklist-nousertalk'] );
282 $formatted = Html
::rawElement(
285 implode( '', array_map( static function ( $prop ) {
286 return Html
::rawElement(
296 $formatted = "Unable to format $name";
304 * Format the target field
305 * @param stdClass $row
308 private function formatTarget( $row ) {
309 if ( $row->bt_auto
) {
310 return $this->msg( 'autoblockid', $row->bl_id
)->parse();
313 [ $target, $type ] = $this->blockUtils
->parseBlockTargetRow( $row );
315 if ( $type === Block
::TYPE_RANGE
) {
318 } elseif ( ( $row->hu_deleted ??
null )
319 && !$this->getAuthority()->isAllowed( 'hideuser' )
321 return Html
::element(
323 [ 'class' => 'mw-blocklist-hidden' ],
324 $this->msg( 'blocklist-hidden-placeholder' )->text()
326 } elseif ( $target instanceof UserIdentity
) {
327 $userId = $target->getId();
328 $userName = $target->getName();
329 } elseif ( is_string( $target ) ) {
330 return htmlspecialchars( $target );
332 return $this->msg( 'empty-username' )->escaped();
334 return Linker
::userLink( $userId, $userName ) .
335 Linker
::userToolLinks(
339 Linker
::TOOL_LINKS_NOBLOCK
344 * Get Restriction List HTML
346 * @param stdClass $row
350 private function getRestrictionListHTML( stdClass
$row ) {
352 $linkRenderer = $this->getLinkRenderer();
354 foreach ( $this->restrictions
as $restriction ) {
355 if ( $restriction->getBlockId() !== (int)$row->bl_id
) {
359 switch ( $restriction->getType() ) {
360 case PageRestriction
::TYPE
:
361 '@phan-var PageRestriction $restriction';
362 if ( $restriction->getTitle() ) {
363 $items[$restriction->getType()][] = Html
::rawElement(
366 $linkRenderer->makeLink( $restriction->getTitle() )
370 case NamespaceRestriction
::TYPE
:
371 $text = $restriction->getValue() === NS_MAIN
372 ?
$this->msg( 'blanknamespace' )->text()
373 : $this->getLanguage()->getFormattedNsText(
374 $restriction->getValue()
377 $items[$restriction->getType()][] = Html
::rawElement(
380 $linkRenderer->makeLink(
381 $this->specialPageFactory
->getTitleForAlias( 'Allpages' ),
385 'namespace' => $restriction->getValue()
391 case ActionRestriction
::TYPE
:
392 $actionName = $this->blockActionInfo
->getActionFromId( $restriction->getValue() );
393 $enablePartialActionBlocks =
394 $this->getConfig()->get( MainConfigNames
::EnablePartialActionBlocks
);
395 if ( $actionName && $enablePartialActionBlocks ) {
396 $items[$restriction->getType()][] = Html
::rawElement(
399 $this->msg( 'ipb-action-' .
400 $this->blockActionInfo
->getActionFromId( $restriction->getValue() ) )->escaped()
412 foreach ( $items as $key => $value ) {
413 $sets[] = Html
::rawElement(
416 $this->msg( 'blocklist-editing-' . $key ) . Html
::rawElement(
419 implode( '', $value )
424 return Html
::rawElement(
431 public function getQueryInfo() {
432 $db = $this->getDatabase();
433 $commentQuery = $this->commentStore
->getJoin( 'bl_reason' );
435 'tables' => array_merge(
438 'block_by_actor' => 'actor',
441 $commentQuery['tables']
444 // The target fields should be those accepted by BlockUtils::parseBlockTargetRow()
451 // Block fields and aliases
453 'bl_by' => 'block_by_actor.actor_user',
454 'bl_by_text' => 'block_by_actor.actor_name',
458 'bl_enable_autoblock',
464 ] +
$commentQuery['fields'],
465 'conds' => $this->conds
,
467 'block_by_actor' => [ 'JOIN', 'actor_id=bl_by_actor' ],
468 'block_target' => [ 'JOIN', 'bt_id=bl_target' ],
469 ] +
$commentQuery['joins']
472 # Filter out any expired blocks
473 $info['conds'][] = $db->expr( 'bl_expiry', '>', $db->timestamp() );
475 # Filter out blocks with the deleted option if the user doesn't
476 # have permission to see hidden users
477 # TODO: consider removing this -- we could just redact them instead.
478 # The mere fact that an admin has deleted a user does not need to
479 # be private and could be included in block lists and logs for
480 # transparency purposes. Previously, filtering out deleted blocks
481 # was a convenient way to avoid showing the target name.
482 if ( !$this->getAuthority()->isAllowed( 'hideuser' ) ) {
483 $info['conds']['bl_deleted'] = 0;
486 # Determine if the user is hidden
487 # With multiblocks we can't just rely on bl_deleted in the row being formatted
488 $info['fields']['hu_deleted'] = $this->hideUserUtils
->getExpression(
490 'block_target.bt_user',
491 HideUserUtils
::HIDDEN_USERS
);
495 protected function getTableClass() {
496 return parent
::getTableClass() . ' mw-blocklist';
499 public function getIndexField() {
500 return [ [ 'bl_timestamp', 'bl_id' ] ];
503 public function getDefaultSort() {
507 protected function isFieldSortable( $name ) {
512 * Do a LinkBatch query to minimise database load when generating all these links
513 * @param IResultWrapper $result
515 public function preprocessResults( $result ) {
516 // Do a link batch query
517 $lb = $this->linkBatchFactory
->newLinkBatch();
518 $lb->setCaller( __METHOD__
);
521 foreach ( $result as $row ) {
522 $target = $row->bt_address ??
$row->bt_user_text
;
523 if ( $target !== null ) {
524 $lb->add( NS_USER
, $target );
525 $lb->add( NS_USER_TALK
, $target );
528 if ( isset( $row->bl_by_text
) ) {
529 $lb->add( NS_USER
, $row->bl_by_text
);
530 $lb->add( NS_USER_TALK
, $row->bl_by_text
);
533 if ( !$row->bl_sitewide
) {
534 $partialBlocks[] = (int)$row->bl_id
;
538 if ( $partialBlocks ) {
539 // Mutations to the $row object are not persisted. The restrictions will
540 // need be stored in a separate store.
541 $this->restrictions
= $this->blockRestrictionStore
->loadByBlockId( $partialBlocks );
543 foreach ( $this->restrictions
as $restriction ) {
544 if ( $restriction->getType() === PageRestriction
::TYPE
) {
545 '@phan-var PageRestriction $restriction';
546 $title = $restriction->getTitle();
548 $lb->addObj( $title );
557 // The keys of formattedComments will be the corresponding offset into $result
558 $this->formattedComments
= $this->rowCommentFormatter
->formatRows( $result, 'bl_reason' );
564 * Retain the old class name for backwards compatibility.
565 * @deprecated since 1.41
567 class_alias( BlockListPager
::class, 'BlockListPager' );