MediaWiki master
BlockListPager.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Pager;
23
44use stdClass;
47
52
54 protected $conds;
55
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
73 private $formattedComments = [];
74
76 private $messages = [];
77
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
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
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
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
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
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
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
624class_alias( BlockListPager::class, 'BlockListPager' );
const NS_USER
Definition Defines.php:67
const NS_MAIN
Definition Defines.php:65
const NS_USER_TALK
Definition Defines.php:68
Defines the actions that can be blocked by a partial block.
Backend class for blocking utils.
Helpers for building queries that determine whether a user is hidden.
Restriction for partial blocks of actions.
This is basically a CommentFormatter with a CommentStore dependency, allowing it to retrieve comment ...
Handle database storage of comments such as edit summaries and log reasons.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:62
A class containing constants representing the names of configuration variables.
const UseCodexSpecialBlock
Name constant for the UseCodexSpecialBlock setting, for use with Config::get()
const EnablePartialActionBlocks
Name constant for the EnablePartialActionBlocks setting, for use with Config::get()
preprocessResults( $result)
Do a LinkBatch query to minimise database load when generating all these links.
Restriction[] $restrictions
Array of restrictions.
getTableClass()
TablePager relies on mw-datatable for styling, see T214208.
getFieldNames()
An array mapping database field names to a textual description of the field name, for use in the tabl...
getIndexField()
Returns the name of the index field.If the pager supports multiple orders, it may return an array of ...
getQueryInfo()
Provides all parameters needed for the main paged query.
__construct(IContextSource $context, BlockActionInfo $blockActionInfo, BlockRestrictionStore $blockRestrictionStore, BlockUtils $blockUtils, HideUserUtils $hideUserUtils, CommentStore $commentStore, LinkBatchFactory $linkBatchFactory, LinkRenderer $linkRenderer, IConnectionProvider $dbProvider, RowCommentFormatter $rowCommentFormatter, SpecialPageFactory $specialPageFactory, $conds)
isFieldSortable( $name)
Return true if the named field should be sortable by the UI, false otherwise.
getDefaultSort()
The database field name used as a default sort order.
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
Table-based display with a user-selectable sort order.
Factory for handling the special page list and generating SpecialPage objects.
Library for creating and parsing MW-style timestamps.
Represents a block that may prevent users from performing specific operations.
Definition Block.php:45
Interface for objects which can provide a MediaWiki context on request.
Interface for objects representing user identity.
Provide primary and replica IDatabase connections.
getReplicaDatabase( $domain=false, $group=null)
Get connection to a replica database.
Result wrapper for grabbing data queried from an IDatabase object.
element(SerializerNode $parent, SerializerNode $node, $contents)