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