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