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