Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / specials / pagers / BlockListPager.php
blob6b2019e87d1fecfbf49c9df35bd42950c8013f68
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.
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
18 * @file
19 * @ingroup Pager
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;
44 use stdClass;
45 use Wikimedia\Rdbms\IConnectionProvider;
46 use Wikimedia\Rdbms\IResultWrapper;
48 /**
49 * @ingroup Pager
51 class BlockListPager extends TablePager {
53 /** @var array */
54 protected $conds;
56 /**
57 * Array of restrictions.
59 * @var Restriction[]
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;
72 /** @var string[] */
73 private $formattedComments = [];
75 /**
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
87 * @param array $conds
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,
101 $conds
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 ) {
124 $headers = [
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();
137 return $headers;
141 * @param string $name
142 * @param string|null $value
143 * @return string
144 * @suppress PhanTypeArraySuspicious
146 public function formatValue( $name, $value ) {
147 static $msg = null;
148 if ( $msg === null ) {
149 $keys = [
150 'anononlyblock',
151 'createaccountblock',
152 'noautoblockblock',
153 'emailblock',
154 'blocklist-nousertalk',
155 'unblocklink',
156 'change-blocklink',
157 'blocklist-editing',
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();
175 switch ( $name ) {
176 case 'bl_timestamp':
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}" ],
185 break;
187 case 'target':
188 $formatted = $this->formatTarget( $row );
189 break;
191 case 'bl_expiry':
192 $formatted = htmlspecialchars( $language->formatExpiry(
193 $value,
194 /* User preference timezone */true,
195 'infinity',
196 $this->getUser()
197 ) );
198 if ( $this->getAuthority()->isAllowed( 'block' ) ) {
199 $links = [];
200 if ( $row->bt_auto ) {
201 $links[] = $linkRenderer->makeKnownLink(
202 $this->specialPageFactory->getTitleForAlias( 'Unblock' ),
203 $msg['unblocklink'],
205 [ 'wpTarget' => "#{$row->bl_id}" ]
207 } else {
208 $target = $row->bt_address ?? $row->bt_user_text;
209 $links[] = $linkRenderer->makeKnownLink(
210 $this->specialPageFactory->getTitleForAlias( "Unblock/$target" ),
211 $msg['unblocklink']
213 $links[] = $linkRenderer->makeKnownLink(
214 $this->specialPageFactory->getTitleForAlias( "Block/$target" ),
215 $msg['change-blocklink']
218 $formatted .= ' ' . Html::rawElement(
219 'span',
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 ),
231 MWTimestamp::time(),
234 )->escaped();
236 break;
238 case 'by':
239 $formatted = Linker::userLink( (int)$value, $row->bl_by_text );
240 $formatted .= Linker::userToolLinks( (int)$value, $row->bl_by_text );
241 break;
243 case 'bl_reason':
244 $formatted = $this->formattedComments[$this->getResultOffset()];
245 break;
247 case 'params':
248 $properties = [];
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 );
259 if ( $list ) {
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(
283 'ul',
285 implode( '', array_map( static function ( $prop ) {
286 return Html::rawElement(
287 'li',
289 $prop
291 }, $properties ) )
293 break;
295 default:
296 $formatted = "Unable to format $name";
297 break;
300 return $formatted;
304 * Format the target field
305 * @param stdClass $row
306 * @return string
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 ) {
316 $userId = 0;
317 $userName = $target;
318 } elseif ( ( $row->hu_deleted ?? null )
319 && !$this->getAuthority()->isAllowed( 'hideuser' )
321 return Html::element(
322 'span',
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 );
331 } else {
332 return $this->msg( 'empty-username' )->escaped();
334 return Linker::userLink( $userId, $userName ) .
335 Linker::userToolLinks(
336 $userId,
337 $userName,
338 false,
339 Linker::TOOL_LINKS_NOBLOCK
344 * Get Restriction List HTML
346 * @param stdClass $row
348 * @return string
350 private function getRestrictionListHTML( stdClass $row ) {
351 $items = [];
352 $linkRenderer = $this->getLinkRenderer();
354 foreach ( $this->restrictions as $restriction ) {
355 if ( $restriction->getBlockId() !== (int)$row->bl_id ) {
356 continue;
359 switch ( $restriction->getType() ) {
360 case PageRestriction::TYPE:
361 '@phan-var PageRestriction $restriction';
362 if ( $restriction->getTitle() ) {
363 $items[$restriction->getType()][] = Html::rawElement(
364 'li',
366 $linkRenderer->makeLink( $restriction->getTitle() )
369 break;
370 case NamespaceRestriction::TYPE:
371 $text = $restriction->getValue() === NS_MAIN
372 ? $this->msg( 'blanknamespace' )->text()
373 : $this->getLanguage()->getFormattedNsText(
374 $restriction->getValue()
376 if ( $text ) {
377 $items[$restriction->getType()][] = Html::rawElement(
378 'li',
380 $linkRenderer->makeLink(
381 $this->specialPageFactory->getTitleForAlias( 'Allpages' ),
382 $text,
385 'namespace' => $restriction->getValue()
390 break;
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(
397 'li',
399 $this->msg( 'ipb-action-' .
400 $this->blockActionInfo->getActionFromId( $restriction->getValue() ) )->escaped()
403 break;
407 if ( !$items ) {
408 return '';
411 $sets = [];
412 foreach ( $items as $key => $value ) {
413 $sets[] = Html::rawElement(
414 'li',
416 $this->msg( 'blocklist-editing-' . $key ) . Html::rawElement(
417 'ul',
419 implode( '', $value )
424 return Html::rawElement(
425 'ul',
427 implode( '', $sets )
431 public function getQueryInfo() {
432 $db = $this->getDatabase();
433 $commentQuery = $this->commentStore->getJoin( 'bl_reason' );
434 $info = [
435 'tables' => array_merge(
437 'block',
438 'block_by_actor' => 'actor',
439 'block_target',
441 $commentQuery['tables']
443 'fields' => [
444 // The target fields should be those accepted by BlockUtils::parseBlockTargetRow()
445 'bt_address',
446 'bt_user_text',
447 'bt_user',
448 'bt_auto',
449 'bt_range_start',
450 'bt_range_end',
451 // Block fields and aliases
452 'bl_id',
453 'bl_by' => 'block_by_actor.actor_user',
454 'bl_by_text' => 'block_by_actor.actor_name',
455 'bl_timestamp',
456 'bl_anon_only',
457 'bl_create_account',
458 'bl_enable_autoblock',
459 'bl_expiry',
460 'bl_deleted',
461 'bl_block_email',
462 'bl_allow_usertalk',
463 'bl_sitewide',
464 ] + $commentQuery['fields'],
465 'conds' => $this->conds,
466 'join_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(
489 $db,
490 'block_target.bt_user',
491 HideUserUtils::HIDDEN_USERS );
492 return $info;
495 protected function getTableClass() {
496 return parent::getTableClass() . ' mw-blocklist';
499 public function getIndexField() {
500 return [ [ 'bl_timestamp', 'bl_id' ] ];
503 public function getDefaultSort() {
504 return '';
507 protected function isFieldSortable( $name ) {
508 return false;
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__ );
520 $partialBlocks = [];
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();
547 if ( $title ) {
548 $lb->addObj( $title );
554 $lb->execute();
556 // Format comments
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' );