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