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->formatDurationBetweenTimestamps(
229 (int)$timestamp->getTimestamp( TS_UNIX ),
230 MWTimestamp::time(),
231 4
232 )
233 )->escaped();
234 }
235 break;
236
237 case 'by':
238 $formatted = Linker::userLink( (int)$value, $row->bl_by_text );
239 $formatted .= Linker::userToolLinks( (int)$value, $row->bl_by_text );
240 break;
241
242 case 'bl_reason':
243 $formatted = $this->formattedComments[$this->getResultOffset()];
244 break;
245
246 case 'params':
247 $properties = [];
248
249 if ( $row->bl_deleted ) {
250 $properties[] = htmlspecialchars( $msg['blocklist-hidden-param' ] );
251 }
252 if ( $row->bl_sitewide ) {
253 $properties[] = htmlspecialchars( $msg['blocklist-editing-sitewide'] );
254 }
255
256 if ( !$row->bl_sitewide && $this->restrictions ) {
257 $list = $this->getRestrictionListHTML( $row );
258 if ( $list ) {
259 $properties[] = htmlspecialchars( $msg['blocklist-editing'] ) . $list;
260 }
261 }
262
263 if ( $row->bl_anon_only ) {
264 $properties[] = htmlspecialchars( $msg['anononlyblock'] );
265 }
266 if ( $row->bl_create_account ) {
267 $properties[] = htmlspecialchars( $msg['createaccountblock'] );
268 }
269 if ( $row->bt_user && !$row->bl_enable_autoblock ) {
270 $properties[] = htmlspecialchars( $msg['noautoblockblock'] );
271 }
272
273 if ( $row->bl_block_email ) {
274 $properties[] = htmlspecialchars( $msg['emailblock'] );
275 }
276
277 if ( !$row->bl_allow_usertalk ) {
278 $properties[] = htmlspecialchars( $msg['blocklist-nousertalk'] );
279 }
280
281 $formatted = Html::rawElement(
282 'ul',
283 [],
284 implode( '', array_map( static function ( $prop ) {
285 return Html::rawElement(
286 'li',
287 [],
288 $prop
289 );
290 }, $properties ) )
291 );
292 break;
293
294 default:
295 $formatted = "Unable to format $name";
296 break;
297 }
298
299 return $formatted;
300 }
301
307 private function formatTarget( $row ) {
308 if ( $row->bt_auto ) {
309 return $this->msg( 'autoblockid', $row->bl_id )->parse();
310 }
311
312 [ $target, $type ] = $this->blockUtils->parseBlockTargetRow( $row );
313
314 if ( $type === Block::TYPE_RANGE ) {
315 $userId = 0;
316 $userName = $target;
317 } elseif ( ( $row->hu_deleted ?? null )
318 && !$this->getAuthority()->isAllowed( 'hideuser' )
319 ) {
320 return Html::element(
321 'span',
322 [ 'class' => 'mw-blocklist-hidden' ],
323 $this->msg( 'blocklist-hidden-placeholder' )->text()
324 );
325 } elseif ( $target instanceof UserIdentity ) {
326 $userId = $target->getId();
327 $userName = $target->getName();
328 } elseif ( is_string( $target ) ) {
329 return htmlspecialchars( $target );
330 } else {
331 return $this->msg( 'empty-username' )->escaped();
332 }
333 return Linker::userLink( $userId, $userName ) .
334 Linker::userToolLinks(
335 $userId,
336 $userName,
337 false,
338 Linker::TOOL_LINKS_NOBLOCK
339 );
340 }
341
349 private function getRestrictionListHTML( stdClass $row ) {
350 $items = [];
351 $linkRenderer = $this->getLinkRenderer();
352
353 foreach ( $this->restrictions as $restriction ) {
354 if ( $restriction->getBlockId() !== (int)$row->bl_id ) {
355 continue;
356 }
357
358 switch ( $restriction->getType() ) {
359 case PageRestriction::TYPE:
360 '@phan-var PageRestriction $restriction';
361 if ( $restriction->getTitle() ) {
362 $items[$restriction->getType()][] = Html::rawElement(
363 'li',
364 [],
365 $linkRenderer->makeLink( $restriction->getTitle() )
366 );
367 }
368 break;
369 case NamespaceRestriction::TYPE:
370 $text = $restriction->getValue() === NS_MAIN
371 ? $this->msg( 'blanknamespace' )->text()
372 : $this->getLanguage()->getFormattedNsText(
373 $restriction->getValue()
374 );
375 if ( $text ) {
376 $items[$restriction->getType()][] = Html::rawElement(
377 'li',
378 [],
379 $linkRenderer->makeLink(
380 $this->specialPageFactory->getTitleForAlias( 'Allpages' ),
381 $text,
382 [],
383 [
384 'namespace' => $restriction->getValue()
385 ]
386 )
387 );
388 }
389 break;
390 case ActionRestriction::TYPE:
391 $actionName = $this->blockActionInfo->getActionFromId( $restriction->getValue() );
392 $enablePartialActionBlocks =
394 if ( $actionName && $enablePartialActionBlocks ) {
395 $items[$restriction->getType()][] = Html::rawElement(
396 'li',
397 [],
398 $this->msg( 'ipb-action-' .
399 $this->blockActionInfo->getActionFromId( $restriction->getValue() ) )->escaped()
400 );
401 }
402 break;
403 }
404 }
405
406 if ( !$items ) {
407 return '';
408 }
409
410 $sets = [];
411 foreach ( $items as $key => $value ) {
412 $sets[] = Html::rawElement(
413 'li',
414 [],
415 $this->msg( 'blocklist-editing-' . $key ) . Html::rawElement(
416 'ul',
417 [],
418 implode( '', $value )
419 )
420 );
421 }
422
423 return Html::rawElement(
424 'ul',
425 [],
426 implode( '', $sets )
427 );
428 }
429
430 public function getQueryInfo() {
431 $db = $this->getDatabase();
432 $commentQuery = $this->commentStore->getJoin( 'bl_reason' );
433 $info = [
434 'tables' => array_merge(
435 [
436 'block',
437 'block_by_actor' => 'actor',
438 'block_target',
439 ],
440 $commentQuery['tables']
441 ),
442 'fields' => [
443 // The target fields should be those accepted by BlockUtils::parseBlockTargetRow()
444 'bt_address',
445 'bt_user_text',
446 'bt_user',
447 'bt_auto',
448 'bt_range_start',
449 'bt_range_end',
450 // Block fields and aliases
451 'bl_id',
452 'bl_by' => 'block_by_actor.actor_user',
453 'bl_by_text' => 'block_by_actor.actor_name',
454 'bl_timestamp',
455 'bl_anon_only',
456 'bl_create_account',
457 'bl_enable_autoblock',
458 'bl_expiry',
459 'bl_deleted',
460 'bl_block_email',
461 'bl_allow_usertalk',
462 'bl_sitewide',
463 ] + $commentQuery['fields'],
464 'conds' => $this->conds,
465 'join_conds' => [
466 'block_by_actor' => [ 'JOIN', 'actor_id=bl_by_actor' ],
467 'block_target' => [ 'JOIN', 'bt_id=bl_target' ],
468 ] + $commentQuery['joins']
469 ];
470
471 # Filter out any expired blocks
472 $info['conds'][] = $db->expr( 'bl_expiry', '>', $db->timestamp() );
473
474 # Filter out blocks with the deleted option if the user doesn't
475 # have permission to see hidden users
476 # TODO: consider removing this -- we could just redact them instead.
477 # The mere fact that an admin has deleted a user does not need to
478 # be private and could be included in block lists and logs for
479 # transparency purposes. Previously, filtering out deleted blocks
480 # was a convenient way to avoid showing the target name.
481 if ( !$this->getAuthority()->isAllowed( 'hideuser' ) ) {
482 $info['conds']['bl_deleted'] = 0;
483 }
484
485 # Determine if the user is hidden
486 # With multiblocks we can't just rely on bl_deleted in the row being formatted
487 $info['fields']['hu_deleted'] = $this->hideUserUtils->getExpression(
488 $db,
489 $db->tableName( 'block_target' ) . '.bt_user',
490 HideUserUtils::HIDDEN_USERS );
491 return $info;
492 }
493
494 protected function getTableClass() {
495 return parent::getTableClass() . ' mw-blocklist';
496 }
497
498 public function getIndexField() {
499 return [ [ 'bl_timestamp', 'bl_id' ] ];
500 }
501
502 public function getDefaultSort() {
503 return '';
504 }
505
506 protected function isFieldSortable( $name ) {
507 return false;
508 }
509
514 public function preprocessResults( $result ) {
515 // Do a link batch query
516 $lb = $this->linkBatchFactory->newLinkBatch();
517 $lb->setCaller( __METHOD__ );
518
519 $partialBlocks = [];
520 foreach ( $result as $row ) {
521 $target = $row->bt_address ?? $row->bt_user_text;
522 if ( $target !== null ) {
523 $lb->add( NS_USER, $target );
524 $lb->add( NS_USER_TALK, $target );
525 }
526
527 if ( isset( $row->bl_by_text ) ) {
528 $lb->add( NS_USER, $row->bl_by_text );
529 $lb->add( NS_USER_TALK, $row->bl_by_text );
530 }
531
532 if ( !$row->bl_sitewide ) {
533 $partialBlocks[] = (int)$row->bl_id;
534 }
535 }
536
537 if ( $partialBlocks ) {
538 // Mutations to the $row object are not persisted. The restrictions will
539 // need be stored in a separate store.
540 $this->restrictions = $this->blockRestrictionStore->loadByBlockId( $partialBlocks );
541
542 foreach ( $this->restrictions as $restriction ) {
543 if ( $restriction->getType() === PageRestriction::TYPE ) {
544 '@phan-var PageRestriction $restriction';
545 $title = $restriction->getTitle();
546 if ( $title ) {
547 $lb->addObj( $title );
548 }
549 }
550 }
551 }
552
553 $lb->execute();
554
555 // Format comments
556 // The keys of formattedComments will be the corresponding offset into $result
557 $this->formattedComments = $this->rowCommentFormatter->formatRows( $result, 'bl_reason' );
558 }
559
560}
561
566class_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)