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