MediaWiki master
BlockListPager.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Pager;
23
45use stdClass;
48
53
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 $readStage;
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 $this->readStage = $this->getConfig()->get( MainConfigNames::BlockTargetMigrationStage )
110 if ( $this->readStage !== SCHEMA_COMPAT_READ_OLD
111 && $this->readStage !== SCHEMA_COMPAT_READ_NEW
112 ) {
113 throw new ConfigException(
114 '$wgBlockTargetMigrationStage has an invalid read stage' );
115 }
116
117 parent::__construct( $context, $linkRenderer );
118
119 $this->blockActionInfo = $blockActionInfo;
120 $this->blockRestrictionStore = $blockRestrictionStore;
121 $this->blockUtils = $blockUtils;
122 $this->hideUserUtils = $hideUserUtils;
123 $this->commentStore = $commentStore;
124 $this->linkBatchFactory = $linkBatchFactory;
125 $this->rowCommentFormatter = $rowCommentFormatter;
126 $this->specialPageFactory = $specialPageFactory;
127 $this->conds = $conds;
128 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
129 }
130
131 protected function getFieldNames() {
132 static $headers = null;
133
134 if ( $headers === null ) {
135 $headers = [
136 'bl_timestamp' => 'blocklist-timestamp',
137 'target' => 'blocklist-target',
138 'bl_expiry' => 'blocklist-expiry',
139 'by' => 'blocklist-by',
140 'params' => 'blocklist-params',
141 'bl_reason' => 'blocklist-reason',
142 ];
143 foreach ( $headers as $key => $val ) {
144 $headers[$key] = $this->msg( $val )->text();
145 }
146 }
147
148 return $headers;
149 }
150
157 public function formatValue( $name, $value ) {
158 static $msg = null;
159 if ( $msg === null ) {
160 $keys = [
161 'anononlyblock',
162 'createaccountblock',
163 'noautoblockblock',
164 'emailblock',
165 'blocklist-nousertalk',
166 'unblocklink',
167 'change-blocklink',
168 'blocklist-editing',
169 'blocklist-editing-sitewide',
170 'blocklist-hidden-param',
171 ];
172
173 foreach ( $keys as $key ) {
174 $msg[$key] = $this->msg( $key )->text();
175 }
176 }
177 '@phan-var string[] $msg';
178
180 $row = $this->mCurrentRow;
181
182 $language = $this->getLanguage();
183
184 $formatted = '';
185
186 $linkRenderer = $this->getLinkRenderer();
187
188 switch ( $name ) {
189 case 'bl_timestamp':
190 $formatted = htmlspecialchars( $language->userTimeAndDate( $value, $this->getUser() ) );
191 break;
192
193 case 'target':
194 $formatted = $this->formatTarget( $row );
195 break;
196
197 case 'bl_expiry':
198 $formatted = htmlspecialchars( $language->formatExpiry(
199 $value,
200 /* User preference timezone */true,
201 'infinity',
202 $this->getUser()
203 ) );
204 if ( $this->getAuthority()->isAllowed( 'block' ) ) {
205 $links = [];
206 if ( $row->bt_auto ) {
207 $links[] = $linkRenderer->makeKnownLink(
208 $this->specialPageFactory->getTitleForAlias( 'Unblock' ),
209 $msg['unblocklink'],
210 [],
211 [ 'wpTarget' => "#{$row->bl_id}" ]
212 );
213 } else {
214 $target = $row->bt_address ?? $row->bt_user_text;
215 $links[] = $linkRenderer->makeKnownLink(
216 $this->specialPageFactory->getTitleForAlias( "Unblock/$target" ),
217 $msg['unblocklink']
218 );
219 $links[] = $linkRenderer->makeKnownLink(
220 $this->specialPageFactory->getTitleForAlias( "Block/$target" ),
221 $msg['change-blocklink']
222 );
223 }
224 $formatted .= ' ' . Html::rawElement(
225 'span',
226 [ 'class' => 'mw-blocklist-actions' ],
227 $this->msg( 'parentheses' )->rawParams(
228 $language->pipeList( $links ) )->escaped()
229 );
230 }
231 if ( $value !== 'infinity' ) {
232 $timestamp = new MWTimestamp( $value );
233 $formatted .= '<br />' . $this->msg(
234 'ipb-blocklist-duration-left',
235 $language->formatDuration(
236 (int)$timestamp->getTimestamp( TS_UNIX ) - MWTimestamp::time(),
237 // reasonable output
238 [
239 'minutes',
240 'hours',
241 'days',
242 'years',
243 ]
244 )
245 )->escaped();
246 }
247 break;
248
249 case 'by':
250 $formatted = Linker::userLink( (int)$value, $row->bl_by_text );
251 $formatted .= Linker::userToolLinks( (int)$value, $row->bl_by_text );
252 break;
253
254 case 'bl_reason':
255 $formatted = $this->formattedComments[$this->getResultOffset()];
256 break;
257
258 case 'params':
259 $properties = [];
260
261 if ( $row->bl_deleted ) {
262 $properties[] = htmlspecialchars( $msg['blocklist-hidden-param' ] );
263 }
264 if ( $row->bl_sitewide ) {
265 $properties[] = htmlspecialchars( $msg['blocklist-editing-sitewide'] );
266 }
267
268 if ( !$row->bl_sitewide && $this->restrictions ) {
269 $list = $this->getRestrictionListHTML( $row );
270 if ( $list ) {
271 $properties[] = htmlspecialchars( $msg['blocklist-editing'] ) . $list;
272 }
273 }
274
275 if ( $row->bl_anon_only ) {
276 $properties[] = htmlspecialchars( $msg['anononlyblock'] );
277 }
278 if ( $row->bl_create_account ) {
279 $properties[] = htmlspecialchars( $msg['createaccountblock'] );
280 }
281 if ( $row->bt_user && !$row->bl_enable_autoblock ) {
282 $properties[] = htmlspecialchars( $msg['noautoblockblock'] );
283 }
284
285 if ( $row->bl_block_email ) {
286 $properties[] = htmlspecialchars( $msg['emailblock'] );
287 }
288
289 if ( !$row->bl_allow_usertalk ) {
290 $properties[] = htmlspecialchars( $msg['blocklist-nousertalk'] );
291 }
292
293 $formatted = Html::rawElement(
294 'ul',
295 [],
296 implode( '', array_map( static function ( $prop ) {
297 return Html::rawElement(
298 'li',
299 [],
300 $prop
301 );
302 }, $properties ) )
303 );
304 break;
305
306 default:
307 $formatted = "Unable to format $name";
308 break;
309 }
310
311 return $formatted;
312 }
313
319 private function formatTarget( $row ) {
320 if ( $row->bt_auto ) {
321 return $this->msg( 'autoblockid', $row->bl_id )->parse();
322 }
323
324 [ $target, $type ] = $this->blockUtils->parseBlockTargetRow( $row );
325
326 if ( $type === Block::TYPE_RANGE ) {
327 $userId = 0;
328 $userName = $target;
329 } elseif ( ( $row->hu_deleted ?? null )
330 && !$this->getAuthority()->isAllowed( 'hideuser' )
331 ) {
332 return Html::element(
333 'span',
334 [ 'class' => 'mw-blocklist-hidden' ],
335 $this->msg( 'blocklist-hidden-placeholder' )->text()
336 );
337 } elseif ( $target instanceof UserIdentity ) {
338 $userId = $target->getId();
339 $userName = $target->getName();
340 } elseif ( is_string( $target ) ) {
341 return htmlspecialchars( $target );
342 } else {
343 return $this->msg( 'empty-username' )->escaped();
344 }
345 return Linker::userLink( $userId, $userName ) .
346 Linker::userToolLinks(
347 $userId,
348 $userName,
349 false,
350 Linker::TOOL_LINKS_NOBLOCK
351 );
352 }
353
361 private function getRestrictionListHTML( stdClass $row ) {
362 $items = [];
363 $linkRenderer = $this->getLinkRenderer();
364
365 foreach ( $this->restrictions as $restriction ) {
366 if ( $restriction->getBlockId() !== (int)$row->bl_id ) {
367 continue;
368 }
369
370 switch ( $restriction->getType() ) {
371 case PageRestriction::TYPE:
372 '@phan-var PageRestriction $restriction';
373 if ( $restriction->getTitle() ) {
374 $items[$restriction->getType()][] = Html::rawElement(
375 'li',
376 [],
377 $linkRenderer->makeLink( $restriction->getTitle() )
378 );
379 }
380 break;
381 case NamespaceRestriction::TYPE:
382 $text = $restriction->getValue() === NS_MAIN
383 ? $this->msg( 'blanknamespace' )->text()
384 : $this->getLanguage()->getFormattedNsText(
385 $restriction->getValue()
386 );
387 if ( $text ) {
388 $items[$restriction->getType()][] = Html::rawElement(
389 'li',
390 [],
391 $linkRenderer->makeLink(
392 $this->specialPageFactory->getTitleForAlias( 'Allpages' ),
393 $text,
394 [],
395 [
396 'namespace' => $restriction->getValue()
397 ]
398 )
399 );
400 }
401 break;
402 case ActionRestriction::TYPE:
403 $actionName = $this->blockActionInfo->getActionFromId( $restriction->getValue() );
404 $enablePartialActionBlocks =
406 if ( $actionName && $enablePartialActionBlocks ) {
407 $items[$restriction->getType()][] = Html::rawElement(
408 'li',
409 [],
410 $this->msg( 'ipb-action-' .
411 $this->blockActionInfo->getActionFromId( $restriction->getValue() ) )->escaped()
412 );
413 }
414 break;
415 }
416 }
417
418 if ( !$items ) {
419 return '';
420 }
421
422 $sets = [];
423 foreach ( $items as $key => $value ) {
424 $sets[] = Html::rawElement(
425 'li',
426 [],
427 $this->msg( 'blocklist-editing-' . $key ) . Html::rawElement(
428 'ul',
429 [],
430 implode( '', $value )
431 )
432 );
433 }
434
435 return Html::rawElement(
436 'ul',
437 [],
438 implode( '', $sets )
439 );
440 }
441
442 public function getQueryInfo() {
443 $db = $this->getDatabase();
444 if ( $this->readStage === SCHEMA_COMPAT_READ_OLD ) {
445 $commentQuery = $this->commentStore->getJoin( 'ipb_reason' );
446 $info = [
447 'tables' => array_merge(
448 [ 'ipblocks', 'ipblocks_by_actor' => 'actor' ],
449 $commentQuery['tables']
450 ),
451 'fields' => [
452 'bt_address' => 'ipb_address',
453 'bt_user_text' => 'ipb_address',
454 'bt_user' => 'ipb_user',
455 'bt_auto' => 'ipb_auto',
456 'bt_range_start' => 'ipb_range_start',
457 'bt_range_end' => 'ipb_range_end',
458 'bl_id' => 'ipb_id',
459 'bl_by' => 'ipblocks_by_actor.actor_user',
460 'bl_by_text' => 'ipblocks_by_actor.actor_name',
461 'bl_timestamp' => 'ipb_timestamp',
462 'bl_anon_only' => 'ipb_anon_only',
463 'bl_create_account' => 'ipb_create_account',
464 'bl_enable_autoblock' => 'ipb_enable_autoblock',
465 'bl_expiry' => 'ipb_expiry',
466 'bl_deleted' => 'ipb_deleted',
467 'bl_block_email' => 'ipb_block_email',
468 'bl_allow_usertalk' => 'ipb_allow_usertalk',
469 'bl_sitewide' => 'ipb_sitewide',
470 'bl_reason_text' => $commentQuery['fields']['ipb_reason_text'],
471 'bl_reason_data' => $commentQuery['fields']['ipb_reason_data'],
472 'bl_reason_cid' => $commentQuery['fields']['ipb_reason_cid'],
473 // Aliases for IndexPager::extractResultInfo()
474 'ipb_id',
475 'ipb_timestamp',
476 ] + $commentQuery['fields'],
477 'conds' => $this->conds,
478 'join_conds' => [
479 'ipblocks_by_actor' => [ 'JOIN', 'actor_id=ipb_by_actor' ]
480 ] + $commentQuery['joins']
481 ];
482 # Filter out any expired blocks
483 $info['conds'][] = $db->expr( 'ipb_expiry', '>', $db->timestamp() );
484
485 # Is the user allowed to see hidden blocks?
486 if ( !$this->getAuthority()->isAllowed( 'hideuser' ) ) {
487 $info['conds']['ipb_deleted'] = 0;
488 }
489 } else {
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, 'block_target.bt_user', HideUserUtils::HIDDEN_USERS );
547 }
548 return $info;
549 }
550
551 protected function getTableClass() {
552 return parent::getTableClass() . ' mw-blocklist';
553 }
554
555 public function getIndexField() {
556 if ( $this->readStage === SCHEMA_COMPAT_READ_OLD ) {
557 return [ [ 'ipb_timestamp', 'ipb_id' ] ];
558 } else {
559 return [ [ 'bl_timestamp', 'bl_id' ] ];
560 }
561 }
562
563 public function getDefaultSort() {
564 return '';
565 }
566
567 protected function isFieldSortable( $name ) {
568 return false;
569 }
570
575 public function preprocessResults( $result ) {
576 // Do a link batch query
577 $lb = $this->linkBatchFactory->newLinkBatch();
578 $lb->setCaller( __METHOD__ );
579
580 $partialBlocks = [];
581 $userIds = [];
582 foreach ( $result as $row ) {
583 $target = $row->bt_address ?? $row->bt_user_text;
584 if ( $target !== null ) {
585 $lb->add( NS_USER, $target );
586 $lb->add( NS_USER_TALK, $target );
587 }
588
589 if ( isset( $row->bl_by_text ) ) {
590 $lb->add( NS_USER, $row->bl_by_text );
591 $lb->add( NS_USER_TALK, $row->bl_by_text );
592 }
593
594 if ( !$row->bl_sitewide ) {
595 $partialBlocks[] = (int)$row->bl_id;
596 }
597
598 if ( $row->bt_user ) {
599 $userIds[] = $row->bt_user;
600 }
601 }
602
603 if ( $partialBlocks ) {
604 // Mutations to the $row object are not persisted. The restrictions will
605 // need be stored in a separate store.
606 $this->restrictions = $this->blockRestrictionStore->loadByBlockId( $partialBlocks );
607
608 foreach ( $this->restrictions as $restriction ) {
609 if ( $restriction->getType() === PageRestriction::TYPE ) {
610 '@phan-var PageRestriction $restriction';
611 $title = $restriction->getTitle();
612 if ( $title ) {
613 $lb->addObj( $title );
614 }
615 }
616 }
617 }
618
619 $lb->execute();
620
621 // Format comments
622 // The keys of formattedComments will be the corresponding offset into $result
623 $this->formattedComments = $this->rowCommentFormatter->formatRows( $result, 'bl_reason' );
624 }
625
626}
627
632class_alias( BlockListPager::class, 'BlockListPager' );
const SCHEMA_COMPAT_READ_NEW
Definition Defines.php:279
const NS_USER
Definition Defines.php:66
const NS_MAIN
Definition Defines.php:64
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:275
const NS_USER_TALK
Definition Defines.php:67
const SCHEMA_COMPAT_READ_MASK
Definition Defines.php:281
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.
Exceptions for config failures.
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:65
A class containing constants representing the names of configuration variables.
const BlockTargetMigrationStage
Name constant for the BlockTargetMigrationStage 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.
getDatabase()
Get the Database object in use.
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)