1<?php 2/** 3 * This program is free software; you can redistribute it and/or modify 4 * it under the terms of the GNU General Public License as published by 5 * the Free Software Foundation; either version 2 of the License, or 6 * (at your option) any later version. 7 * 8 * This program is distributed in the hope that it will be useful, 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along 14 * with this program; if not, write to the Free Software Foundation, Inc., 15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 * http://www.gnu.org/copyleft/gpl.html 17 * 18 * @file 19 * @ingroup Pager 20 */ 21 22use MediaWiki\Block\BlockActionInfo; 23use MediaWiki\Block\BlockRestrictionStore; 24use MediaWiki\Block\BlockUtils; 25use MediaWiki\Block\Restriction\ActionRestriction; 26use MediaWiki\Block\Restriction\NamespaceRestriction; 27use MediaWiki\Block\Restriction\PageRestriction; 28use MediaWiki\Block\Restriction\Restriction; 29use MediaWiki\Cache\LinkBatchFactory; 30use MediaWiki\SpecialPage\SpecialPageFactory; 31use MediaWiki\User\UserIdentity; 32use Wikimedia\IPUtils; 33use Wikimedia\Rdbms\ILoadBalancer; 34use Wikimedia\Rdbms\IResultWrapper; 35 36/** 37 * @ingroup Pager 38 */ 39class BlockListPager extends TablePager { 40 41 protected $conds; 42 43 /** 44 * Array of restrictions. 45 * 46 * @var Restriction[] 47 */ 48 protected $restrictions = []; 49 50 /** @var LinkBatchFactory */ 51 private $linkBatchFactory; 52 53 /** @var BlockRestrictionStore */ 54 private $blockRestrictionStore; 55 56 /** @var SpecialPageFactory */ 57 private $specialPageFactory; 58 59 /** @var CommentStore */ 60 private $commentStore; 61 62 /** @var BlockUtils */ 63 private $blockUtils; 64 65 /** @var BlockActionInfo */ 66 private $blockActionInfo; 67 68 /** 69 * @param SpecialPage $page 70 * @param array $conds 71 * @param LinkBatchFactory $linkBatchFactory 72 * @param BlockRestrictionStore $blockRestrictionStore 73 * @param ILoadBalancer $loadBalancer 74 * @param SpecialPageFactory $specialPageFactory 75 * @param CommentStore $commentStore 76 * @param BlockUtils $blockUtils 77 * @param BlockActionInfo $blockActionInfo 78 */ 79 public function __construct( 80 $page, 81 $conds, 82 LinkBatchFactory $linkBatchFactory, 83 BlockRestrictionStore $blockRestrictionStore, 84 ILoadBalancer $loadBalancer, 85 SpecialPageFactory $specialPageFactory, 86 CommentStore $commentStore, 87 BlockUtils $blockUtils, 88 BlockActionInfo $blockActionInfo 89 ) { 90 $this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA ); 91 parent::__construct( $page->getContext(), $page->getLinkRenderer() ); 92 $this->conds = $conds; 93 $this->mDefaultDirection = IndexPager::DIR_DESCENDING; 94 $this->linkBatchFactory = $linkBatchFactory; 95 $this->blockRestrictionStore = $blockRestrictionStore; 96 $this->specialPageFactory = $specialPageFactory; 97 $this->commentStore = $commentStore; 98 $this->blockUtils = $blockUtils; 99 $this->blockActionInfo = $blockActionInfo; 100 } 101 102 protected function getFieldNames() { 103 static $headers = null; 104 105 if ( $headers === null ) { 106 $headers = [ 107 'ipb_timestamp' => 'blocklist-timestamp', 108 'ipb_target' => 'blocklist-target', 109 'ipb_expiry' => 'blocklist-expiry', 110 'ipb_by' => 'blocklist-by', 111 'ipb_params' => 'blocklist-params', 112 'ipb_reason' => 'blocklist-reason', 113 ]; 114 foreach ( $headers as $key => $val ) { 115 $headers[$key] = $this->msg( $val )->text(); 116 } 117 } 118 119 return $headers; 120 } 121 122 /** 123 * @param string $name 124 * @param string|null $value 125 * @return string 126 * @suppress PhanTypeArraySuspicious 127 */ 128 public function formatValue( $name, $value ) { 129 static $msg = null; 130 if ( $msg === null ) { 131 $keys = [ 132 'anononlyblock', 133 'createaccountblock', 134 'noautoblockblock', 135 'emailblock', 136 'blocklist-nousertalk', 137 'unblocklink', 138 'change-blocklink', 139 'blocklist-editing', 140 'blocklist-editing-sitewide', 141 ]; 142 143 foreach ( $keys as $key ) { 144 $msg[$key] = $this->msg( $key )->text(); 145 } 146 } 147 '@phan-var string[] $msg'; 148 149 /** @var stdClass $row */ 150 $row = $this->mCurrentRow; 151 152 $language = $this->getLanguage(); 153 154 $formatted = ''; 155 156 $linkRenderer = $this->getLinkRenderer(); 157 158 switch ( $name ) { 159 case 'ipb_timestamp': 160 $formatted = htmlspecialchars( $language->userTimeAndDate( $value, $this->getUser() ) ); 161 break; 162 163 case 'ipb_target': 164 if ( $row->ipb_auto ) { 165 $formatted = $this->msg( 'autoblockid', $row->ipb_id )->parse(); 166 } else { 167 list( $target, ) = $this->blockUtils->parseBlockTarget( $row->ipb_address ); 168 169 if ( is_string( $target ) ) { 170 if ( IPUtils::isValidRange( $target ) ) { 171 $target = User::newFromName( $target, false ); 172 } else { 173 $formatted = $target; 174 } 175 } 176 177 if ( $target instanceof UserIdentity ) { 178 $formatted = Linker::userLink( $target->getId(), $target->getName() ); 179 $formatted .= Linker::userToolLinks( 180 $target->getId(), 181 $target->getName(), 182 false, 183 Linker::TOOL_LINKS_NOBLOCK 184 ); 185 } 186 } 187 break; 188 189 case 'ipb_expiry': 190 $formatted = htmlspecialchars( $language->formatExpiry( 191 $value, 192 /* User preference timezone */true, 193 'infinity', 194 $this->getUser() 195 ) ); 196 if ( $this->getAuthority()->isAllowed( 'block' ) ) { 197 $links = []; 198 if ( $row->ipb_auto ) { 199 $links[] = $linkRenderer->makeKnownLink( 200 $this->specialPageFactory->getTitleForAlias( 'Unblock' ), 201 $msg['unblocklink'], 202 [], 203 [ 'wpTarget' => "#{$row->ipb_id}" ] 204 ); 205 } else { 206 $links[] = $linkRenderer->makeKnownLink( 207 $this->specialPageFactory->getTitleForAlias( 'Unblock/' . $row->ipb_address ), 208 $msg['unblocklink'] 209 ); 210 $links[] = $linkRenderer->makeKnownLink( 211 $this->specialPageFactory->getTitleForAlias( 'Block/' . $row->ipb_address ), 212 $msg['change-blocklink'] 213 ); 214 } 215 $formatted .= ' ' . Html::rawElement( 216 'span', 217 [ 'class' => 'mw-blocklist-actions' ], 218 $this->msg( 'parentheses' )->rawParams( 219 $language->pipeList( $links ) )->escaped() 220 ); 221 } 222 if ( $value !== 'infinity' ) { 223 $timestamp = new MWTimestamp( $value ); 224 $formatted .= '<br />' . $this->msg( 225 'ipb-blocklist-duration-left', 226 $language->formatDuration( 227 $timestamp->getTimestamp() - MWTimestamp::time(), 228 // reasonable output 229 [ 230 'minutes', 231 'hours', 232 'days', 233 'years', 234 ] 235 ) 236 )->escaped(); 237 } 238 break; 239 240 case 'ipb_by': 241 $formatted = Linker::userLink( $value, $row->ipb_by_text ); 242 $formatted .= Linker::userToolLinks( $value, $row->ipb_by_text ); 243 break; 244 245 case 'ipb_reason': 246 $value = $this->commentStore->getComment( 'ipb_reason', $row )->text; 247 $formatted = Linker::formatComment( $value ); 248 break; 249 250 case 'ipb_params': 251 $properties = []; 252 253 if ( $row->ipb_sitewide ) { 254 $properties[] = htmlspecialchars( $msg['blocklist-editing-sitewide'] ); 255 } 256 257 if ( !$row->ipb_sitewide && $this->restrictions ) { 258 $list = $this->getRestrictionListHTML( $row ); 259 if ( $list ) { 260 $properties[] = htmlspecialchars( $msg['blocklist-editing'] ) . $list; 261 } 262 } 263 264 if ( $row->ipb_anon_only ) { 265 $properties[] = htmlspecialchars( $msg['anononlyblock'] ); 266 } 267 if ( $row->ipb_create_account ) { 268 $properties[] = htmlspecialchars( $msg['createaccountblock'] ); 269 } 270 if ( $row->ipb_user && !$row->ipb_enable_autoblock ) { 271 $properties[] = htmlspecialchars( $msg['noautoblockblock'] ); 272 } 273 274 if ( $row->ipb_block_email ) { 275 $properties[] = htmlspecialchars( $msg['emailblock'] ); 276 } 277 278 if ( !$row->ipb_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 303 /** 304 * Get Restriction List HTML 305 * 306 * @param stdClass $row 307 * 308 * @return string 309 */ 310 private function getRestrictionListHTML( stdClass $row ) { 311 $items = []; 312 $linkRenderer = $this->getLinkRenderer(); 313 314 foreach ( $this->restrictions as $restriction ) { 315 if ( $restriction->getBlockId() !== (int)$row->ipb_id ) { 316 continue; 317 } 318 319 switch ( $restriction->getType() ) { 320 case PageRestriction::TYPE: 321 '@phan-var PageRestriction $restriction'; 322 if ( $restriction->getTitle() ) { 323 $items[$restriction->getType()][] = Html::rawElement( 324 'li', 325 [], 326 $linkRenderer->makeLink( $restriction->getTitle() ) 327 ); 328 } 329 break; 330 case NamespaceRestriction::TYPE: 331 $text = $restriction->getValue() === NS_MAIN 332 ? $this->msg( 'blanknamespace' )->text() 333 : $this->getLanguage()->getFormattedNsText( 334 $restriction->getValue() 335 ); 336 if ( $text ) { 337 $items[$restriction->getType()][] = Html::rawElement( 338 'li', 339 [], 340 $linkRenderer->makeLink( 341 $this->specialPageFactory->getTitleForAlias( 'Allpages' ), 342 $text, 343 [], 344 [ 345 'namespace' => $restriction->getValue() 346 ] 347 ) 348 ); 349 } 350 break; 351 case ActionRestriction::TYPE: 352 $actionName = $this->blockActionInfo->getActionFromId( $restriction->getValue() ); 353 $enablePartialActionBlocks = $this->getConfig()->get( 'EnablePartialActionBlocks' ); 354 if ( $actionName && $enablePartialActionBlocks ) { 355 $items[$restriction->getType()][] = Html::rawElement( 356 'li', 357 [], 358 $this->msg( 'ipb-action-' . 359 $this->blockActionInfo->getActionFromId( $restriction->getValue() ) )->escaped() 360 ); 361 } 362 break; 363 } 364 } 365 366 if ( empty( $items ) ) { 367 return ''; 368 } 369 370 $sets = []; 371 foreach ( $items as $key => $value ) { 372 $sets[] = Html::rawElement( 373 'li', 374 [], 375 $this->msg( 'blocklist-editing-' . $key ) . Html::rawElement( 376 'ul', 377 [], 378 implode( '', $value ) 379 ) 380 ); 381 } 382 383 return Html::rawElement( 384 'ul', 385 [], 386 implode( '', $sets ) 387 ); 388 } 389 390 public function getQueryInfo() { 391 $commentQuery = $this->commentStore->getJoin( 'ipb_reason' ); 392 393 $info = [ 394 'tables' => array_merge( 395 [ 'ipblocks', 'ipblocks_by_actor' => 'actor' ], 396 $commentQuery['tables'] 397 ), 398 'fields' => [ 399 'ipb_id', 400 'ipb_address', 401 'ipb_user', 402 'ipb_by' => 'ipblocks_by_actor.actor_user', 403 'ipb_by_text' => 'ipblocks_by_actor.actor_name', 404 'ipb_timestamp', 405 'ipb_auto', 406 'ipb_anon_only', 407 'ipb_create_account', 408 'ipb_enable_autoblock', 409 'ipb_expiry', 410 'ipb_range_start', 411 'ipb_range_end', 412 'ipb_deleted', 413 'ipb_block_email', 414 'ipb_allow_usertalk', 415 'ipb_sitewide', 416 ] + $commentQuery['fields'], 417 'conds' => $this->conds, 418 'join_conds' => [ 419 'ipblocks_by_actor' => [ 'JOIN', 'actor_id=ipb_by_actor' ] 420 ] + $commentQuery['joins'] 421 ]; 422 423 # Filter out any expired blocks 424 $db = $this->getDatabase(); 425 $info['conds'][] = 'ipb_expiry > ' . $db->addQuotes( $db->timestamp() ); 426 427 # Is the user allowed to see hidden blocks? 428 if ( !$this->getAuthority()->isAllowed( 'hideuser' ) ) { 429 $info['conds']['ipb_deleted'] = 0; 430 } 431 432 return $info; 433 } 434 435 /** 436 * Get total number of autoblocks at any given time 437 * 438 * @return int Total number of unexpired active autoblocks 439 */ 440 public function getTotalAutoblocks() { 441 $dbr = $this->getDatabase(); 442 return (int)$dbr->selectField( 'ipblocks', 'COUNT(*)', 443 [ 444 'ipb_auto' => '1', 445 'ipb_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ), 446 ], 447 __METHOD__ 448 ); 449 } 450 451 protected function getTableClass() { 452 return parent::getTableClass() . ' mw-blocklist'; 453 } 454 455 public function getIndexField() { 456 return [ [ 'ipb_timestamp', 'ipb_id' ] ]; 457 } 458 459 public function getDefaultSort() { 460 return ''; 461 } 462 463 protected function isFieldSortable( $name ) { 464 return false; 465 } 466 467 /** 468 * Do a LinkBatch query to minimise database load when generating all these links 469 * @param IResultWrapper $result 470 */ 471 public function preprocessResults( $result ) { 472 # Do a link batch query 473 $lb = $this->linkBatchFactory->newLinkBatch(); 474 $lb->setCaller( __METHOD__ ); 475 476 $partialBlocks = []; 477 foreach ( $result as $row ) { 478 $lb->add( NS_USER, $row->ipb_address ); 479 $lb->add( NS_USER_TALK, $row->ipb_address ); 480 481 if ( $row->ipb_by ?? null ) { 482 $lb->add( NS_USER, $row->ipb_by_text ); 483 $lb->add( NS_USER_TALK, $row->ipb_by_text ); 484 } 485 486 if ( !$row->ipb_sitewide ) { 487 $partialBlocks[] = $row->ipb_id; 488 } 489 } 490 491 if ( $partialBlocks ) { 492 // Mutations to the $row object are not persisted. The restrictions will 493 // need be stored in a separate store. 494 $this->restrictions = $this->blockRestrictionStore->loadByBlockId( $partialBlocks ); 495 496 foreach ( $this->restrictions as $restriction ) { 497 if ( $restriction->getType() === PageRestriction::TYPE ) { 498 '@phan-var PageRestriction $restriction'; 499 $title = $restriction->getTitle(); 500 if ( $title ) { 501 $lb->addObj( $title ); 502 } 503 } 504 } 505 } 506 507 $lb->execute(); 508 } 509 510} 511