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