MediaWiki master
BlockListPager.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Pager;
23
44use stdClass;
47
52
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
89 public function __construct(
90 IContextSource $context,
91 BlockActionInfo $blockActionInfo,
92 BlockRestrictionStore $blockRestrictionStore,
93 BlockUtils $blockUtils,
94 HideUserUtils $hideUserUtils,
95 CommentStore $commentStore,
96 LinkBatchFactory $linkBatchFactory,
97 LinkRenderer $linkRenderer,
98 IConnectionProvider $dbProvider,
99 RowCommentFormatter $rowCommentFormatter,
100 SpecialPageFactory $specialPageFactory,
101 $conds
102 ) {
103 // Set database before parent constructor to avoid setting it there
104 $this->mDb = $dbProvider->getReplicaDatabase();
105
106 parent::__construct( $context, $linkRenderer );
107
108 $this->blockActionInfo = $blockActionInfo;
109 $this->blockRestrictionStore = $blockRestrictionStore;
110 $this->blockUtils = $blockUtils;
111 $this->hideUserUtils = $hideUserUtils;
112 $this->commentStore = $commentStore;
113 $this->linkBatchFactory = $linkBatchFactory;
114 $this->rowCommentFormatter = $rowCommentFormatter;
115 $this->specialPageFactory = $specialPageFactory;
116 $this->conds = $conds;
117 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
118 }
119
120 protected function getFieldNames() {
121 static $headers = null;
122
123 if ( $headers === null ) {
124 $headers = [
125 'bl_timestamp' => 'blocklist-timestamp',
126 'target' => 'blocklist-target',
127 'bl_expiry' => 'blocklist-expiry',
128 'by' => 'blocklist-by',
129 'params' => 'blocklist-params',
130 'bl_reason' => 'blocklist-reason',
131 ];
132 foreach ( $headers as $key => $val ) {
133 $headers[$key] = $this->msg( $val )->text();
134 }
135 }
136
137 return $headers;
138 }
139
146 public function formatValue( $name, $value ) {
147 static $msg = null;
148 if ( $msg === null ) {
149 $keys = [
150 'anononlyblock',
151 'createaccountblock',
152 'noautoblockblock',
153 'emailblock',
154 'blocklist-nousertalk',
155 'unblocklink',
156 'change-blocklink',
157 'blocklist-editing',
158 'blocklist-editing-sitewide',
159 'blocklist-hidden-param',
160 ];
161
162 foreach ( $keys as $key ) {
163 $msg[$key] = $this->msg( $key )->text();
164 }
165 }
166 '@phan-var string[] $msg';
167
169 $row = $this->mCurrentRow;
170
171 $language = $this->getLanguage();
172
173 $linkRenderer = $this->getLinkRenderer();
174
175 switch ( $name ) {
176 case 'bl_timestamp':
177 // Link the timestamp to the block ID. This allows users without permissions to change blocks
178 // to be able to generate a link to a specific block.
179 $formatted = $linkRenderer->makeKnownLink(
180 $this->specialPageFactory->getTitleForAlias( 'BlockList' ),
181 $language->userTimeAndDate( $value, $this->getUser() ),
182 [],
183 [ 'wpTarget' => "#{$row->bl_id}" ],
184 );
185 break;
186
187 case 'target':
188 $formatted = $this->formatTarget( $row );
189 break;
190
191 case 'bl_expiry':
192 $formatted = htmlspecialchars( $language->formatExpiry(
193 $value,
194 /* User preference timezone */true,
195 'infinity',
196 $this->getUser()
197 ) );
198 if ( $this->getAuthority()->isAllowed( 'block' ) ) {
199 $links = [];
200 if ( $row->bt_auto ) {
201 $links[] = $linkRenderer->makeKnownLink(
202 $this->specialPageFactory->getTitleForAlias( 'Unblock' ),
203 $msg['unblocklink'],
204 [],
205 [ 'wpTarget' => "#{$row->bl_id}" ]
206 );
207 } else {
208 $target = $row->bt_address ?? $row->bt_user_text;
209 $links[] = $linkRenderer->makeKnownLink(
210 $this->specialPageFactory->getTitleForAlias( "Unblock/$target" ),
211 $msg['unblocklink']
212 );
213 $links[] = $linkRenderer->makeKnownLink(
214 $this->specialPageFactory->getTitleForAlias( "Block/$target" ),
215 $msg['change-blocklink']
216 );
217 }
218 $formatted .= ' ' . Html::rawElement(
219 'span',
220 [ 'class' => 'mw-blocklist-actions' ],
221 $this->msg( 'parentheses' )->rawParams(
222 $language->pipeList( $links ) )->escaped()
223 );
224 }
225 if ( $value !== 'infinity' ) {
226 $timestamp = new MWTimestamp( $value );
227 $formatted .= '<br />' . $this->msg(
228 'ipb-blocklist-duration-left',
229 $language->formatDurationBetweenTimestamps(
230 (int)$timestamp->getTimestamp( TS_UNIX ),
231 MWTimestamp::time(),
232 4
233 )
234 )->escaped();
235 }
236 break;
237
238 case 'by':
239 $formatted = Linker::userLink( (int)$value, $row->bl_by_text );
240 $formatted .= Linker::userToolLinks( (int)$value, $row->bl_by_text );
241 break;
242
243 case 'bl_reason':
244 $formatted = $this->formattedComments[$this->getResultOffset()];
245 break;
246
247 case 'params':
248 $properties = [];
249
250 if ( $row->bl_deleted ) {
251 $properties[] = htmlspecialchars( $msg['blocklist-hidden-param' ] );
252 }
253 if ( $row->bl_sitewide ) {
254 $properties[] = htmlspecialchars( $msg['blocklist-editing-sitewide'] );
255 }
256
257 if ( !$row->bl_sitewide && $this->restrictions ) {
258 $list = $this->getRestrictionListHTML( $row );
259 if ( $list ) {
260 $properties[] = htmlspecialchars( $msg['blocklist-editing'] ) . $list;
261 }
262 }
263
264 if ( $row->bl_anon_only ) {
265 $properties[] = htmlspecialchars( $msg['anononlyblock'] );
266 }
267 if ( $row->bl_create_account ) {
268 $properties[] = htmlspecialchars( $msg['createaccountblock'] );
269 }
270 if ( $row->bt_user && !$row->bl_enable_autoblock ) {
271 $properties[] = htmlspecialchars( $msg['noautoblockblock'] );
272 }
273
274 if ( $row->bl_block_email ) {
275 $properties[] = htmlspecialchars( $msg['emailblock'] );
276 }
277
278 if ( !$row->bl_allow_usertalk ) {
279 $properties[] = htmlspecialchars( $msg['blocklist-nousertalk'] );
280 }
281
282 $formatted = Html::rawElement(
283 'ul',
284 [],
285 implode( '', array_map( static function ( $prop ) {
286 return Html::rawElement(
287 'li',
288 [],
289 $prop
290 );
291 }, $properties ) )
292 );
293 break;
294
295 default:
296 $formatted = "Unable to format $name";
297 break;
298 }
299
300 return $formatted;
301 }
302
308 private function formatTarget( $row ) {
309 if ( $row->bt_auto ) {
310 return $this->msg( 'autoblockid', $row->bl_id )->parse();
311 }
312
313 [ $target, $type ] = $this->blockUtils->parseBlockTargetRow( $row );
314
315 if ( $type === Block::TYPE_RANGE ) {
316 $userId = 0;
317 $userName = $target;
318 } elseif ( ( $row->hu_deleted ?? null )
319 && !$this->getAuthority()->isAllowed( 'hideuser' )
320 ) {
321 return Html::element(
322 'span',
323 [ 'class' => 'mw-blocklist-hidden' ],
324 $this->msg( 'blocklist-hidden-placeholder' )->text()
325 );
326 } elseif ( $target instanceof UserIdentity ) {
327 $userId = $target->getId();
328 $userName = $target->getName();
329 } elseif ( is_string( $target ) ) {
330 return htmlspecialchars( $target );
331 } else {
332 return $this->msg( 'empty-username' )->escaped();
333 }
334 return Linker::userLink( $userId, $userName ) .
335 Linker::userToolLinks(
336 $userId,
337 $userName,
338 false,
339 Linker::TOOL_LINKS_NOBLOCK
340 );
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->msg( 'blanknamespace' )->text()
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 $enablePartialActionBlocks =
395 if ( $actionName && $enablePartialActionBlocks ) {
396 $items[$restriction->getType()][] = Html::rawElement(
397 'li',
398 [],
399 $this->msg( 'ipb-action-' .
400 $this->blockActionInfo->getActionFromId( $restriction->getValue() ) )->escaped()
401 );
402 }
403 break;
404 }
405 }
406
407 if ( !$items ) {
408 return '';
409 }
410
411 $sets = [];
412 foreach ( $items as $key => $value ) {
413 $sets[] = Html::rawElement(
414 'li',
415 [],
416 $this->msg( 'blocklist-editing-' . $key ) . Html::rawElement(
417 'ul',
418 [],
419 implode( '', $value )
420 )
421 );
422 }
423
424 return Html::rawElement(
425 'ul',
426 [],
427 implode( '', $sets )
428 );
429 }
430
431 public function getQueryInfo() {
432 $db = $this->getDatabase();
433 $commentQuery = $this->commentStore->getJoin( 'bl_reason' );
434 $info = [
435 'tables' => array_merge(
436 [
437 'block',
438 'block_by_actor' => 'actor',
439 'block_target',
440 ],
441 $commentQuery['tables']
442 ),
443 'fields' => [
444 // The target fields should be those accepted by BlockUtils::parseBlockTargetRow()
445 'bt_address',
446 'bt_user_text',
447 'bt_user',
448 'bt_auto',
449 'bt_range_start',
450 'bt_range_end',
451 // Block fields and aliases
452 'bl_id',
453 'bl_by' => 'block_by_actor.actor_user',
454 'bl_by_text' => 'block_by_actor.actor_name',
455 'bl_timestamp',
456 'bl_anon_only',
457 'bl_create_account',
458 'bl_enable_autoblock',
459 'bl_expiry',
460 'bl_deleted',
461 'bl_block_email',
462 'bl_allow_usertalk',
463 'bl_sitewide',
464 ] + $commentQuery['fields'],
465 'conds' => $this->conds,
466 'join_conds' => [
467 'block_by_actor' => [ 'JOIN', 'actor_id=bl_by_actor' ],
468 'block_target' => [ 'JOIN', 'bt_id=bl_target' ],
469 ] + $commentQuery['joins']
470 ];
471
472 # Filter out any expired blocks
473 $info['conds'][] = $db->expr( 'bl_expiry', '>', $db->timestamp() );
474
475 # Filter out blocks with the deleted option if the user doesn't
476 # have permission to see hidden users
477 # TODO: consider removing this -- we could just redact them instead.
478 # The mere fact that an admin has deleted a user does not need to
479 # be private and could be included in block lists and logs for
480 # transparency purposes. Previously, filtering out deleted blocks
481 # was a convenient way to avoid showing the target name.
482 if ( !$this->getAuthority()->isAllowed( 'hideuser' ) ) {
483 $info['conds']['bl_deleted'] = 0;
484 }
485
486 # Determine if the user is hidden
487 # With multiblocks we can't just rely on bl_deleted in the row being formatted
488 $info['fields']['hu_deleted'] = $this->hideUserUtils->getExpression(
489 $db,
490 $db->tableName( 'block_target' ) . '.bt_user',
491 HideUserUtils::HIDDEN_USERS );
492 return $info;
493 }
494
495 protected function getTableClass() {
496 return parent::getTableClass() . ' mw-blocklist';
497 }
498
499 public function getIndexField() {
500 return [ [ 'bl_timestamp', 'bl_id' ] ];
501 }
502
503 public function getDefaultSort() {
504 return '';
505 }
506
507 protected function isFieldSortable( $name ) {
508 return false;
509 }
510
515 public function preprocessResults( $result ) {
516 // Do a link batch query
517 $lb = $this->linkBatchFactory->newLinkBatch();
518 $lb->setCaller( __METHOD__ );
519
520 $partialBlocks = [];
521 foreach ( $result as $row ) {
522 $target = $row->bt_address ?? $row->bt_user_text;
523 if ( $target !== null ) {
524 $lb->add( NS_USER, $target );
525 $lb->add( NS_USER_TALK, $target );
526 }
527
528 if ( isset( $row->bl_by_text ) ) {
529 $lb->add( NS_USER, $row->bl_by_text );
530 $lb->add( NS_USER_TALK, $row->bl_by_text );
531 }
532
533 if ( !$row->bl_sitewide ) {
534 $partialBlocks[] = (int)$row->bl_id;
535 }
536 }
537
538 if ( $partialBlocks ) {
539 // Mutations to the $row object are not persisted. The restrictions will
540 // need be stored in a separate store.
541 $this->restrictions = $this->blockRestrictionStore->loadByBlockId( $partialBlocks );
542
543 foreach ( $this->restrictions as $restriction ) {
544 if ( $restriction->getType() === PageRestriction::TYPE ) {
545 '@phan-var PageRestriction $restriction';
546 $title = $restriction->getTitle();
547 if ( $title ) {
548 $lb->addObj( $title );
549 }
550 }
551 }
552 }
553
554 $lb->execute();
555
556 // Format comments
557 // The keys of formattedComments will be the corresponding offset into $result
558 $this->formattedComments = $this->rowCommentFormatter->formatRows( $result, 'bl_reason' );
559 }
560
561}
562
567class_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.
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.
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:63
A class containing constants representing the names of configuration variables.
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)