1<?php 2/** 3 * Parent class for all special pages. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup SpecialPage 22 */ 23 24use MediaWiki\Auth\AuthManager; 25use MediaWiki\HookContainer\HookContainer; 26use MediaWiki\HookContainer\HookRunner; 27use MediaWiki\Linker\LinkRenderer; 28use MediaWiki\MediaWikiServices; 29use MediaWiki\Navigation\PrevNextNavigationRenderer; 30use MediaWiki\Permissions\Authority; 31use MediaWiki\SpecialPage\SpecialPageFactory; 32 33/** 34 * Parent class for all special pages. 35 * 36 * Includes some static functions for handling the special page list deprecated 37 * in favor of SpecialPageFactory. 38 * 39 * @stable to extend 40 * 41 * @ingroup SpecialPage 42 */ 43class SpecialPage implements MessageLocalizer { 44 /** 45 * @var string The canonical name of this special page 46 * Also used for the default <h1> heading, @see getDescription() 47 */ 48 protected $mName; 49 50 /** @var string The local name of this special page */ 51 private $mLocalName; 52 53 /** 54 * @var string Minimum user level required to access this page, or "" for anyone. 55 * Also used to categorise the pages in Special:Specialpages 56 */ 57 protected $mRestriction; 58 59 /** @var bool Listed in Special:Specialpages? */ 60 private $mListed; 61 62 /** @var bool Whether or not this special page is being included from an article */ 63 protected $mIncluding; 64 65 /** @var bool Whether the special page can be included in an article */ 66 protected $mIncludable; 67 68 /** 69 * Current request context 70 * @var IContextSource 71 */ 72 protected $mContext; 73 74 /** @var Language|null */ 75 private $contentLanguage; 76 77 /** 78 * @var LinkRenderer|null 79 */ 80 private $linkRenderer = null; 81 82 /** @var HookContainer|null */ 83 private $hookContainer; 84 /** @var HookRunner|null */ 85 private $hookRunner; 86 87 /** @var AuthManager|null */ 88 private $authManager = null; 89 90 /** @var SpecialPageFactory */ 91 private $specialPageFactory; 92 93 /** 94 * Get a localised Title object for a specified special page name 95 * If you don't need a full Title object, consider using TitleValue through 96 * getTitleValueFor() below. 97 * 98 * @since 1.9 99 * @since 1.21 $fragment parameter added 100 * 101 * @param string $name 102 * @param string|bool $subpage Subpage string, or false to not use a subpage 103 * @param string $fragment The link fragment (after the "#") 104 * @return Title 105 * @throws MWException 106 */ 107 public static function getTitleFor( $name, $subpage = false, $fragment = '' ) { 108 return Title::newFromLinkTarget( 109 self::getTitleValueFor( $name, $subpage, $fragment ) 110 ); 111 } 112 113 /** 114 * Get a localised TitleValue object for a specified special page name 115 * 116 * @since 1.28 117 * @param string $name 118 * @param string|bool $subpage Subpage string, or false to not use a subpage 119 * @param string $fragment The link fragment (after the "#") 120 * @return TitleValue 121 */ 122 public static function getTitleValueFor( $name, $subpage = false, $fragment = '' ) { 123 $name = MediaWikiServices::getInstance()->getSpecialPageFactory()-> 124 getLocalNameFor( $name, $subpage ); 125 126 return new TitleValue( NS_SPECIAL, $name, $fragment ); 127 } 128 129 /** 130 * Get a localised Title object for a page name with a possibly unvalidated subpage 131 * 132 * @param string $name 133 * @param string|bool $subpage Subpage string, or false to not use a subpage 134 * @return Title|null Title object or null if the page doesn't exist 135 */ 136 public static function getSafeTitleFor( $name, $subpage = false ) { 137 $name = MediaWikiServices::getInstance()->getSpecialPageFactory()-> 138 getLocalNameFor( $name, $subpage ); 139 if ( $name ) { 140 return Title::makeTitleSafe( NS_SPECIAL, $name ); 141 } else { 142 return null; 143 } 144 } 145 146 /** 147 * Default constructor for special pages 148 * Derivative classes should call this from their constructor 149 * Note that if the user does not have the required level, an error message will 150 * be displayed by the default execute() method, without the global function ever 151 * being called. 152 * 153 * If you override execute(), you can recover the default behavior with userCanExecute() 154 * and displayRestrictionError() 155 * 156 * @stable to call 157 * 158 * @param string $name Name of the special page, as seen in links and URLs 159 * @param string $restriction User right required, e.g. "block" or "delete" 160 * @param bool $listed Whether the page is listed in Special:Specialpages 161 * @param callable|bool $function Unused 162 * @param string $file Unused 163 * @param bool $includable Whether the page can be included in normal pages 164 */ 165 public function __construct( 166 $name = '', $restriction = '', $listed = true, 167 $function = false, $file = '', $includable = false 168 ) { 169 $this->mName = $name; 170 $this->mRestriction = $restriction; 171 $this->mListed = $listed; 172 $this->mIncludable = $includable; 173 } 174 175 /** 176 * Get the name of this Special Page. 177 * @return string 178 */ 179 public function getName() { 180 return $this->mName; 181 } 182 183 /** 184 * Get the permission that a user must have to execute this page 185 * @return string 186 */ 187 public function getRestriction() { 188 return $this->mRestriction; 189 } 190 191 // @todo FIXME: Decide which syntax to use for this, and stick to it 192 193 /** 194 * Whether this special page is listed in Special:SpecialPages 195 * @stable to override 196 * @since 1.3 (r3583) 197 * @return bool 198 */ 199 public function isListed() { 200 return $this->mListed; 201 } 202 203 /** 204 * Set whether this page is listed in Special:Specialpages, at run-time 205 * @since 1.3 206 * @deprecated since 1.35 207 * @param bool $listed Set via subclassing UnlistedSpecialPage, get via 208 * isListed() 209 * @return bool 210 */ 211 public function setListed( $listed ) { 212 wfDeprecated( __METHOD__, '1.35' ); 213 return wfSetVar( $this->mListed, $listed ); 214 } 215 216 /** 217 * Get or set whether this special page is listed in Special:SpecialPages 218 * @since 1.6 219 * @deprecated since 1.35 Set via subclassing UnlistedSpecialPage, get via 220 * isListed() 221 * @param bool|null $x 222 * @return bool 223 */ 224 public function listed( $x = null ) { 225 wfDeprecated( __METHOD__, '1.35' ); 226 return wfSetVar( $this->mListed, $x ); 227 } 228 229 /** 230 * Whether it's allowed to transclude the special page via {{Special:Foo/params}} 231 * @stable to override 232 * @return bool 233 */ 234 public function isIncludable() { 235 return $this->mIncludable; 236 } 237 238 /** 239 * How long to cache page when it is being included. 240 * 241 * @note If cache time is not 0, then the current user becomes an anon 242 * if you want to do any per-user customizations, than this method 243 * must be overriden to return 0. 244 * @since 1.26 245 * @stable to override 246 * @return int Time in seconds, 0 to disable caching altogether, 247 * false to use the parent page's cache settings 248 */ 249 public function maxIncludeCacheTime() { 250 return $this->getConfig()->get( 'MiserMode' ) ? $this->getCacheTTL() : 0; 251 } 252 253 /** 254 * @stable to override 255 * @return int Seconds that this page can be cached 256 */ 257 protected function getCacheTTL() { 258 return 60 * 60; 259 } 260 261 /** 262 * Whether the special page is being evaluated via transclusion 263 * @param bool|null $x 264 * @return bool 265 */ 266 public function including( $x = null ) { 267 return wfSetVar( $this->mIncluding, $x ); 268 } 269 270 /** 271 * Get the localised name of the special page 272 * @stable to override 273 * @return string 274 */ 275 public function getLocalName() { 276 if ( !isset( $this->mLocalName ) ) { 277 $this->mLocalName = $this->getSpecialPageFactory()->getLocalNameFor( $this->mName ); 278 } 279 280 return $this->mLocalName; 281 } 282 283 /** 284 * Is this page expensive (for some definition of expensive)? 285 * Expensive pages are disabled or cached in miser mode. Originally used 286 * (and still overridden) by QueryPage and subclasses, moved here so that 287 * Special:SpecialPages can safely call it for all special pages. 288 * 289 * @stable to override 290 * @return bool 291 */ 292 public function isExpensive() { 293 return false; 294 } 295 296 /** 297 * Is this page cached? 298 * Expensive pages are cached or disabled in miser mode. 299 * Used by QueryPage and subclasses, moved here so that 300 * Special:SpecialPages can safely call it for all special pages. 301 * 302 * @stable to override 303 * @return bool 304 * @since 1.21 305 */ 306 public function isCached() { 307 return false; 308 } 309 310 /** 311 * Can be overridden by subclasses with more complicated permissions 312 * schemes. 313 * 314 * @stable to override 315 * @return bool Should the page be displayed with the restricted-access 316 * pages? 317 */ 318 public function isRestricted() { 319 // DWIM: If anons can do something, then it is not restricted 320 return $this->mRestriction != '' && !MediaWikiServices::getInstance() 321 ->getGroupPermissionsLookup() 322 ->groupHasPermission( '*', $this->mRestriction ); 323 } 324 325 /** 326 * Checks if the given user (identified by an object) can execute this 327 * special page (as defined by $mRestriction). Can be overridden by sub- 328 * classes with more complicated permissions schemes. 329 * 330 * @stable to override 331 * @param User $user The user to check 332 * @return bool Does the user have permission to view the page? 333 */ 334 public function userCanExecute( User $user ) { 335 return MediaWikiServices::getInstance() 336 ->getPermissionManager() 337 ->userHasRight( $user, $this->mRestriction ); 338 } 339 340 /** 341 * Output an error message telling the user what access level they have to have 342 * @stable to override 343 * @throws PermissionsError 344 * @return never 345 */ 346 protected function displayRestrictionError() { 347 throw new PermissionsError( $this->mRestriction ); 348 } 349 350 /** 351 * Checks if userCanExecute, and if not throws a PermissionsError 352 * 353 * @stable to override 354 * @since 1.19 355 * @return void|never 356 * @throws PermissionsError 357 */ 358 public function checkPermissions() { 359 if ( !$this->userCanExecute( $this->getUser() ) ) { 360 $this->displayRestrictionError(); 361 } 362 } 363 364 /** 365 * If the wiki is currently in readonly mode, throws a ReadOnlyError 366 * 367 * @since 1.19 368 * @return void|never 369 * @throws ReadOnlyError 370 */ 371 public function checkReadOnly() { 372 if ( wfReadOnly() ) { 373 throw new ReadOnlyError; 374 } 375 } 376 377 /** 378 * If the user is not logged in, throws UserNotLoggedIn error 379 * 380 * The user will be redirected to Special:Userlogin with the given message as an error on 381 * the form. 382 * 383 * @since 1.23 384 * @param string $reasonMsg [optional] Message key to be displayed on login page 385 * @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor 386 * @throws UserNotLoggedIn 387 */ 388 public function requireLogin( 389 $reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin' 390 ) { 391 if ( $this->getUser()->isAnon() ) { 392 throw new UserNotLoggedIn( $reasonMsg, $titleMsg ); 393 } 394 } 395 396 /** 397 * Tells if the special page does something security-sensitive and needs extra defense against 398 * a stolen account (e.g. a reauthentication). What exactly that will mean is decided by the 399 * authentication framework. 400 * @stable to override 401 * @return bool|string False or the argument for AuthManager::securitySensitiveOperationStatus(). 402 * Typically a special page needing elevated security would return its name here. 403 */ 404 protected function getLoginSecurityLevel() { 405 return false; 406 } 407 408 /** 409 * Record preserved POST data after a reauthentication. 410 * 411 * This is called from checkLoginSecurityLevel() when returning from the 412 * redirect for reauthentication, if the redirect had been served in 413 * response to a POST request. 414 * 415 * The base SpecialPage implementation does nothing. If your subclass uses 416 * getLoginSecurityLevel() or checkLoginSecurityLevel(), it should probably 417 * implement this to do something with the data. 418 * 419 * @note Call self::setAuthManager from special page constructor when overriding 420 * 421 * @stable to override 422 * @since 1.32 423 * @param array $data 424 */ 425 protected function setReauthPostData( array $data ) { 426 } 427 428 /** 429 * Verifies that the user meets the security level, possibly reauthenticating them in the process. 430 * 431 * This should be used when the page does something security-sensitive and needs extra defense 432 * against a stolen account (e.g. a reauthentication). The authentication framework will make 433 * an extra effort to make sure the user account is not compromised. What that exactly means 434 * will depend on the system and user settings; e.g. the user might be required to log in again 435 * unless their last login happened recently, or they might be given a second-factor challenge. 436 * 437 * Calling this method will result in one if these actions: 438 * - return true: all good. 439 * - return false and set a redirect: caller should abort; the redirect will take the user 440 * to the login page for reauthentication, and back. 441 * - throw an exception if there is no way for the user to meet the requirements without using 442 * a different access method (e.g. this functionality is only available from a specific IP). 443 * 444 * Note that this does not in any way check that the user is authorized to use this special page 445 * (use checkPermissions() for that). 446 * 447 * @param string|null $level A security level. Can be an arbitrary string, defaults to the page 448 * name. 449 * @return bool False means a redirect to the reauthentication page has been set and processing 450 * of the special page should be aborted. 451 * @throws ErrorPageError If the security level cannot be met, even with reauthentication. 452 */ 453 protected function checkLoginSecurityLevel( $level = null ) { 454 $level = $level ?: $this->getName(); 455 $key = 'SpecialPage:reauth:' . $this->getName(); 456 $request = $this->getRequest(); 457 458 $securityStatus = $this->getAuthManager()->securitySensitiveOperationStatus( $level ); 459 if ( $securityStatus === AuthManager::SEC_OK ) { 460 $uniqueId = $request->getVal( 'postUniqueId' ); 461 if ( $uniqueId ) { 462 $key .= ':' . $uniqueId; 463 $session = $request->getSession(); 464 $data = $session->getSecret( $key ); 465 if ( $data ) { 466 $session->remove( $key ); 467 $this->setReauthPostData( $data ); 468 } 469 } 470 return true; 471 } elseif ( $securityStatus === AuthManager::SEC_REAUTH ) { 472 $title = self::getTitleFor( 'Userlogin' ); 473 $queryParams = $request->getQueryValues(); 474 475 if ( $request->wasPosted() ) { 476 $data = array_diff_assoc( $request->getValues(), $request->getQueryValues() ); 477 if ( $data ) { 478 // unique ID in case the same special page is open in multiple browser tabs 479 $uniqueId = MWCryptRand::generateHex( 6 ); 480 $key .= ':' . $uniqueId; 481 $queryParams['postUniqueId'] = $uniqueId; 482 $session = $request->getSession(); 483 $session->persist(); // Just in case 484 $session->setSecret( $key, $data ); 485 } 486 } 487 488 $query = [ 489 'returnto' => $this->getFullTitle()->getPrefixedDBkey(), 490 'returntoquery' => wfArrayToCgi( array_diff_key( $queryParams, [ 'title' => true ] ) ), 491 'force' => $level, 492 ]; 493 $url = $title->getFullURL( $query, false, PROTO_HTTPS ); 494 495 $this->getOutput()->redirect( $url ); 496 return false; 497 } 498 499 $titleMessage = wfMessage( 'specialpage-securitylevel-not-allowed-title' ); 500 $errorMessage = wfMessage( 'specialpage-securitylevel-not-allowed' ); 501 throw new ErrorPageError( $titleMessage, $errorMessage ); 502 } 503 504 /** 505 * Set the injected AuthManager from the special page constructor 506 * 507 * @since 1.36 508 * @param AuthManager $authManager 509 */ 510 final protected function setAuthManager( AuthManager $authManager ) { 511 $this->authManager = $authManager; 512 } 513 514 /** 515 * @note Call self::setAuthManager from special page constructor when using 516 * 517 * @since 1.36 518 * @return AuthManager 519 */ 520 final protected function getAuthManager(): AuthManager { 521 if ( $this->authManager === null ) { 522 // Fallback if not provided 523 // TODO Change to wfWarn in a future release 524 $this->authManager = MediaWikiServices::getInstance()->getAuthManager(); 525 } 526 return $this->authManager; 527 } 528 529 /** 530 * Return an array of subpages beginning with $search that this special page will accept. 531 * 532 * For example, if a page supports subpages "foo", "bar" and "baz" (as in Special:PageName/foo, 533 * etc.): 534 * 535 * - `prefixSearchSubpages( "ba" )` should return `[ "bar", "baz" ]` 536 * - `prefixSearchSubpages( "f" )` should return `[ "foo" ]` 537 * - `prefixSearchSubpages( "z" )` should return `[]` 538 * - `prefixSearchSubpages( "" )` should return `[ foo", "bar", "baz" ]` 539 * 540 * @stable to override 541 * @param string $search Prefix to search for 542 * @param int $limit Maximum number of results to return (usually 10) 543 * @param int $offset Number of results to skip (usually 0) 544 * @return string[] Matching subpages 545 */ 546 public function prefixSearchSubpages( $search, $limit, $offset ) { 547 $subpages = $this->getSubpagesForPrefixSearch(); 548 if ( !$subpages ) { 549 return []; 550 } 551 552 return self::prefixSearchArray( $search, $limit, $subpages, $offset ); 553 } 554 555 /** 556 * Return an array of subpages that this special page will accept for prefix 557 * searches. If this method requires a query you might instead want to implement 558 * prefixSearchSubpages() directly so you can support $limit and $offset. This 559 * method is better for static-ish lists of things. 560 * 561 * @stable to override 562 * @return string[] subpages to search from 563 */ 564 protected function getSubpagesForPrefixSearch() { 565 return []; 566 } 567 568 /** 569 * Perform a regular substring search for prefixSearchSubpages 570 * @since 1.36 Added $searchEngineFactory parameter 571 * @param string $search Prefix to search for 572 * @param int $limit Maximum number of results to return (usually 10) 573 * @param int $offset Number of results to skip (usually 0) 574 * @param SearchEngineFactory|null $searchEngineFactory Provide the service 575 * @return string[] Matching subpages 576 */ 577 protected function prefixSearchString( $search, $limit, $offset, SearchEngineFactory $searchEngineFactory = null ) { 578 $title = Title::newFromText( $search ); 579 if ( !$title || !$title->canExist() ) { 580 // No prefix suggestion in special and media namespace 581 return []; 582 } 583 584 $searchEngine = $searchEngineFactory 585 ? $searchEngineFactory->create() 586 // Fallback if not provided 587 // TODO Change to wfWarn in a future release 588 : MediaWikiServices::getInstance()->newSearchEngine(); 589 $searchEngine->setLimitOffset( $limit, $offset ); 590 $searchEngine->setNamespaces( [] ); 591 $result = $searchEngine->defaultPrefixSearch( $search ); 592 return array_map( static function ( Title $t ) { 593 return $t->getPrefixedText(); 594 }, $result ); 595 } 596 597 /** 598 * Helper function for implementations of prefixSearchSubpages() that 599 * filter the values in memory (as opposed to making a query). 600 * 601 * @since 1.24 602 * @param string $search 603 * @param int $limit 604 * @param array $subpages 605 * @param int $offset 606 * @return string[] 607 */ 608 protected static function prefixSearchArray( $search, $limit, array $subpages, $offset ) { 609 $escaped = preg_quote( $search, '/' ); 610 return array_slice( preg_grep( "/^$escaped/i", 611 array_slice( $subpages, $offset ) ), 0, $limit ); 612 } 613 614 /** 615 * Sets headers - this should be called from the execute() method of all derived classes! 616 * @stable to override 617 */ 618 protected function setHeaders() { 619 $out = $this->getOutput(); 620 $out->setArticleRelated( false ); 621 $out->setRobotPolicy( $this->getRobotPolicy() ); 622 $out->setPageTitle( $this->getDescription() ); 623 if ( $this->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) { 624 $out->addModuleStyles( [ 625 'mediawiki.ui.input', 626 'mediawiki.ui.radio', 627 'mediawiki.ui.checkbox', 628 ] ); 629 } 630 } 631 632 /** 633 * Entry point. 634 * 635 * @since 1.20 636 * 637 * @param string|null $subPage 638 */ 639 final public function run( $subPage ) { 640 if ( !$this->getHookRunner()->onSpecialPageBeforeExecute( $this, $subPage ) ) { 641 return; 642 } 643 644 if ( $this->beforeExecute( $subPage ) === false ) { 645 return; 646 } 647 $this->execute( $subPage ); 648 $this->afterExecute( $subPage ); 649 650 $this->getHookRunner()->onSpecialPageAfterExecute( $this, $subPage ); 651 } 652 653 /** 654 * Gets called before @see SpecialPage::execute. 655 * Return false to prevent calling execute() (since 1.27+). 656 * 657 * @stable to override 658 * @since 1.20 659 * 660 * @param string|null $subPage 661 * @return bool|void 662 */ 663 protected function beforeExecute( $subPage ) { 664 // No-op 665 } 666 667 /** 668 * Gets called after @see SpecialPage::execute. 669 * 670 * @stable to override 671 * @since 1.20 672 * 673 * @param string|null $subPage 674 */ 675 protected function afterExecute( $subPage ) { 676 // No-op 677 } 678 679 /** 680 * Default execute method 681 * Checks user permissions 682 * 683 * This must be overridden by subclasses; it will be made abstract in a future version 684 * 685 * @stable to override 686 * 687 * @param string|null $subPage 688 */ 689 public function execute( $subPage ) { 690 $this->setHeaders(); 691 $this->checkPermissions(); 692 $securityLevel = $this->getLoginSecurityLevel(); 693 if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) { 694 return; 695 } 696 $this->outputHeader(); 697 } 698 699 /** 700 * Outputs a summary message on top of special pages 701 * Per default the message key is the canonical name of the special page 702 * May be overridden, i.e. by extensions to stick with the naming conventions 703 * for message keys: 'extensionname-xxx' 704 * 705 * @stable to override 706 * 707 * @param string $summaryMessageKey Message key of the summary 708 */ 709 protected function outputHeader( $summaryMessageKey = '' ) { 710 if ( $summaryMessageKey == '' ) { 711 $msg = $this->getContentLanguage()->lc( $this->getName() ) . 712 '-summary'; 713 } else { 714 $msg = $summaryMessageKey; 715 } 716 if ( !$this->msg( $msg )->isDisabled() && !$this->including() ) { 717 $this->getOutput()->wrapWikiMsg( 718 "<div class='mw-specialpage-summary'>\n$1\n</div>", $msg ); 719 } 720 } 721 722 /** 723 * Returns the name that goes in the \<h1\> in the special page itself, and 724 * also the name that will be listed in Special:Specialpages 725 * 726 * Derived classes can override this, but usually it is easier to keep the 727 * default behavior. 728 * 729 * @stable to override 730 * 731 * @return string 732 */ 733 public function getDescription() { 734 return $this->msg( strtolower( $this->mName ) )->text(); 735 } 736 737 /** 738 * Get a self-referential title object 739 * 740 * @param string|bool $subpage 741 * @return Title 742 * @since 1.23 743 */ 744 public function getPageTitle( $subpage = false ) { 745 return self::getTitleFor( $this->mName, $subpage ); 746 } 747 748 /** 749 * Sets the context this SpecialPage is executed in 750 * 751 * @param IContextSource $context 752 * @since 1.18 753 */ 754 public function setContext( $context ) { 755 $this->mContext = $context; 756 } 757 758 /** 759 * Gets the context this SpecialPage is executed in 760 * 761 * @return IContextSource|RequestContext 762 * @since 1.18 763 */ 764 public function getContext() { 765 if ( !( $this->mContext instanceof IContextSource ) ) { 766 wfDebug( __METHOD__ . " called and \$mContext is null. " . 767 "Using RequestContext::getMain(); for sanity" ); 768 769 $this->mContext = RequestContext::getMain(); 770 } 771 return $this->mContext; 772 } 773 774 /** 775 * Get the WebRequest being used for this instance 776 * 777 * @return WebRequest 778 * @since 1.18 779 */ 780 public function getRequest() { 781 return $this->getContext()->getRequest(); 782 } 783 784 /** 785 * Get the OutputPage being used for this instance 786 * 787 * @return OutputPage 788 * @since 1.18 789 */ 790 public function getOutput() { 791 return $this->getContext()->getOutput(); 792 } 793 794 /** 795 * Shortcut to get the User executing this instance 796 * 797 * @return User 798 * @since 1.18 799 */ 800 public function getUser() { 801 return $this->getContext()->getUser(); 802 } 803 804 /** 805 * Shortcut to get the Authority executing this instance 806 * 807 * @return Authority 808 * @since 1.36 809 */ 810 public function getAuthority(): Authority { 811 return $this->getContext()->getAuthority(); 812 } 813 814 /** 815 * Shortcut to get the skin being used for this instance 816 * 817 * @return Skin 818 * @since 1.18 819 */ 820 public function getSkin() { 821 return $this->getContext()->getSkin(); 822 } 823 824 /** 825 * Shortcut to get user's language 826 * 827 * @return Language 828 * @since 1.19 829 */ 830 public function getLanguage() { 831 return $this->getContext()->getLanguage(); 832 } 833 834 /** 835 * Shortcut to get content language 836 * 837 * @return Language 838 * @since 1.36 839 */ 840 final public function getContentLanguage(): Language { 841 if ( $this->contentLanguage === null ) { 842 // Fallback if not provided 843 // TODO Change to wfWarn in a future release 844 $this->contentLanguage = MediaWikiServices::getInstance()->getContentLanguage(); 845 } 846 return $this->contentLanguage; 847 } 848 849 /** 850 * Set content language 851 * 852 * @internal For factory only 853 * @param Language $contentLanguage 854 * @since 1.36 855 */ 856 final public function setContentLanguage( Language $contentLanguage ) { 857 $this->contentLanguage = $contentLanguage; 858 } 859 860 /** 861 * Shortcut to get language's converter 862 * 863 * @deprecated 1.36 Inject LanguageConverterFactory and store a ILanguageConverter instance 864 * @return ILanguageConverter 865 * @since 1.35 866 */ 867 protected function getLanguageConverter(): ILanguageConverter { 868 wfDeprecated( __METHOD__, '1.36' ); 869 return MediaWikiServices::getInstance()->getLanguageConverterFactory() 870 ->getLanguageConverter(); 871 } 872 873 /** 874 * Shortcut to get main config object 875 * @return Config 876 * @since 1.24 877 */ 878 public function getConfig() { 879 return $this->getContext()->getConfig(); 880 } 881 882 /** 883 * Return the full title, including $par 884 * 885 * @return Title 886 * @since 1.18 887 */ 888 public function getFullTitle() { 889 return $this->getContext()->getTitle(); 890 } 891 892 /** 893 * Return the robot policy. Derived classes that override this can change 894 * the robot policy set by setHeaders() from the default 'noindex,nofollow'. 895 * 896 * @return string 897 * @since 1.23 898 */ 899 protected function getRobotPolicy() { 900 return 'noindex,nofollow'; 901 } 902 903 /** 904 * Wrapper around wfMessage that sets the current context. 905 * 906 * @since 1.16 907 * @param string|string[]|MessageSpecifier $key 908 * @param mixed ...$params 909 * @return Message 910 * @see wfMessage 911 */ 912 public function msg( $key, ...$params ) { 913 $message = $this->getContext()->msg( $key, ...$params ); 914 // RequestContext passes context to wfMessage, and the language is set from 915 // the context, but setting the language for Message class removes the 916 // interface message status, which breaks for example usernameless gender 917 // invocations. Restore the flag when not including special page in content. 918 if ( $this->including() ) { 919 $message->setInterfaceMessageFlag( false ); 920 } 921 922 return $message; 923 } 924 925 /** 926 * Adds RSS/atom links 927 * 928 * @param array $params 929 */ 930 protected function addFeedLinks( $params ) { 931 $feedTemplate = wfScript( 'api' ); 932 933 foreach ( $this->getConfig()->get( 'FeedClasses' ) as $format => $class ) { 934 $theseParams = $params + [ 'feedformat' => $format ]; 935 $url = wfAppendQuery( $feedTemplate, $theseParams ); 936 $this->getOutput()->addFeedLink( $format, $url ); 937 } 938 } 939 940 /** 941 * Adds help link with an icon via page indicators. 942 * Link target can be overridden by a local message containing a wikilink: 943 * the message key is: lowercase special page name + '-helppage'. 944 * @param string $to Target MediaWiki.org page title or encoded URL. 945 * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o. 946 * @since 1.25 947 */ 948 public function addHelpLink( $to, $overrideBaseUrl = false ) { 949 if ( $this->including() ) { 950 return; 951 } 952 953 $msg = $this->msg( $this->getContentLanguage()->lc( $this->getName() ) . '-helppage' ); 954 955 if ( !$msg->isDisabled() ) { 956 $title = Title::newFromText( $msg->plain() ); 957 if ( $title instanceof Title ) { 958 $this->getOutput()->addHelpLink( $title->getLocalURL(), true ); 959 } 960 } else { 961 $this->getOutput()->addHelpLink( $to, $overrideBaseUrl ); 962 } 963 } 964 965 /** 966 * Get the group that the special page belongs in on Special:SpecialPage 967 * Use this method, instead of getGroupName to allow customization 968 * of the group name from the wiki side 969 * 970 * @return string Group of this special page 971 * @since 1.21 972 */ 973 public function getFinalGroupName() { 974 $name = $this->getName(); 975 976 // Allow overriding the group from the wiki side 977 $msg = $this->msg( 'specialpages-specialpagegroup-' . strtolower( $name ) )->inContentLanguage(); 978 if ( !$msg->isBlank() ) { 979 $group = $msg->text(); 980 } else { 981 // Than use the group from this object 982 $group = $this->getGroupName(); 983 } 984 985 return $group; 986 } 987 988 /** 989 * Indicates whether this special page may perform database writes 990 * 991 * @stable to override 992 * 993 * @return bool 994 * @since 1.27 995 */ 996 public function doesWrites() { 997 return false; 998 } 999 1000 /** 1001 * Under which header this special page is listed in Special:SpecialPages 1002 * See messages 'specialpages-group-*' for valid names 1003 * This method defaults to group 'other' 1004 * 1005 * @stable to override 1006 * 1007 * @return string 1008 * @since 1.21 1009 */ 1010 protected function getGroupName() { 1011 return 'other'; 1012 } 1013 1014 /** 1015 * Call wfTransactionalTimeLimit() if this request was POSTed 1016 * @since 1.26 1017 */ 1018 protected function useTransactionalTimeLimit() { 1019 if ( $this->getRequest()->wasPosted() ) { 1020 wfTransactionalTimeLimit(); 1021 } 1022 } 1023 1024 /** 1025 * @since 1.28 1026 * @return LinkRenderer 1027 */ 1028 public function getLinkRenderer(): LinkRenderer { 1029 if ( $this->linkRenderer === null ) { 1030 // TODO Inject the service 1031 $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRendererFactory() 1032 ->create(); 1033 } 1034 return $this->linkRenderer; 1035 } 1036 1037 /** 1038 * @since 1.28 1039 * @param LinkRenderer $linkRenderer 1040 */ 1041 public function setLinkRenderer( LinkRenderer $linkRenderer ) { 1042 $this->linkRenderer = $linkRenderer; 1043 } 1044 1045 /** 1046 * Generate (prev x| next x) (20|50|100...) type links for paging 1047 * 1048 * @param int $offset 1049 * @param int $limit 1050 * @param array $query Optional URL query parameter string 1051 * @param bool $atend Optional param for specified if this is the last page 1052 * @param string|bool $subpage Optional param for specifying subpage 1053 * @return string 1054 */ 1055 protected function buildPrevNextNavigation( 1056 $offset, 1057 $limit, 1058 array $query = [], 1059 $atend = false, 1060 $subpage = false 1061 ) { 1062 $title = $this->getPageTitle( $subpage ); 1063 $prevNext = new PrevNextNavigationRenderer( $this ); 1064 1065 return $prevNext->buildPrevNextNavigation( $title, $offset, $limit, $query, $atend ); 1066 } 1067 1068 /** 1069 * @since 1.35 1070 * @internal 1071 * @param HookContainer $hookContainer 1072 */ 1073 public function setHookContainer( HookContainer $hookContainer ) { 1074 $this->hookContainer = $hookContainer; 1075 $this->hookRunner = new HookRunner( $hookContainer ); 1076 } 1077 1078 /** 1079 * @since 1.35 1080 * @return HookContainer 1081 */ 1082 protected function getHookContainer() { 1083 if ( !$this->hookContainer ) { 1084 $this->hookContainer = MediaWikiServices::getInstance()->getHookContainer(); 1085 } 1086 return $this->hookContainer; 1087 } 1088 1089 /** 1090 * @internal This is for use by core only. Hook interfaces may be removed 1091 * without notice. 1092 * @since 1.35 1093 * @return HookRunner 1094 */ 1095 protected function getHookRunner() { 1096 if ( !$this->hookRunner ) { 1097 $this->hookRunner = new HookRunner( $this->getHookContainer() ); 1098 } 1099 return $this->hookRunner; 1100 } 1101 1102 /** 1103 * @internal For factory only 1104 * @since 1.36 1105 * @param SpecialPageFactory $specialPageFactory 1106 */ 1107 final public function setSpecialPageFactory( SpecialPageFactory $specialPageFactory ) { 1108 $this->specialPageFactory = $specialPageFactory; 1109 } 1110 1111 /** 1112 * @since 1.36 1113 * @return SpecialPageFactory 1114 */ 1115 final protected function getSpecialPageFactory(): SpecialPageFactory { 1116 if ( !$this->specialPageFactory ) { 1117 // Fallback if not provided 1118 // TODO Change to wfWarn in a future release 1119 $this->specialPageFactory = MediaWikiServices::getInstance()->getSpecialPageFactory(); 1120 } 1121 return $this->specialPageFactory; 1122 } 1123} 1124