3 * Parent class for all special pages.
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.
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.
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
21 * @ingroup SpecialPage
24 namespace MediaWiki\SpecialPage
;
27 use MediaWiki\Auth\AuthManager
;
28 use MediaWiki\Config\Config
;
29 use MediaWiki\Context\IContextSource
;
30 use MediaWiki\Context\RequestContext
;
31 use MediaWiki\HookContainer\HookContainer
;
32 use MediaWiki\HookContainer\HookRunner
;
33 use MediaWiki\Language\Language
;
34 use MediaWiki\Language\RawMessage
;
35 use MediaWiki\Linker\LinkRenderer
;
36 use MediaWiki\MainConfigNames
;
37 use MediaWiki\MediaWikiServices
;
38 use MediaWiki\Message\Message
;
39 use MediaWiki\Navigation\PagerNavigationBuilder
;
40 use MediaWiki\Output\OutputPage
;
41 use MediaWiki\Permissions\Authority
;
42 use MediaWiki\Permissions\PermissionStatus
;
43 use MediaWiki\Request\WebRequest
;
44 use MediaWiki\Title\Title
;
45 use MediaWiki\Title\TitleValue
;
46 use MediaWiki\User\User
;
51 use SearchEngineFactory
;
54 use Wikimedia\Message\MessageParam
;
55 use Wikimedia\Message\MessageSpecifier
;
58 * Parent class for all special pages.
60 * Includes some static functions for handling the special page list deprecated
61 * in favor of SpecialPageFactory.
65 * @ingroup SpecialPage
67 class SpecialPage
implements MessageLocalizer
{
69 * @var string The canonical name of this special page
70 * Also used as the message key for the default <h1> heading,
71 * @see getDescription()
75 /** @var string The local name of this special page */
79 * @var string Minimum user level required to access this page, or "" for anyone.
80 * Also used to categorise the pages in Special:Specialpages
82 protected $mRestriction;
84 /** @var bool Listed in Special:Specialpages? */
87 /** @var bool Whether or not this special page is being included from an article */
88 protected $mIncluding;
90 /** @var bool Whether the special page can be included in an article */
91 protected $mIncludable;
94 * Current request context
99 /** @var Language|null */
100 private $contentLanguage;
103 * @var LinkRenderer|null
105 private $linkRenderer = null;
107 /** @var HookContainer|null */
108 private $hookContainer;
109 /** @var HookRunner|null */
112 /** @var AuthManager|null */
113 private $authManager = null;
115 /** @var SpecialPageFactory */
116 private $specialPageFactory;
119 * Get the users preferred search page.
121 * It will fall back to Special:Search if the preference points to a page
122 * that doesn't exist or is not defined.
125 * @param User $user Search page can be customized by user preference.
128 public static function newSearchPage( User
$user ) {
129 // Try user preference first
130 $userOptionsManager = MediaWikiServices
::getInstance()->getUserOptionsManager();
131 $title = $userOptionsManager->getOption( $user, 'search-special-page' );
133 $page = self
::getTitleFor( $title );
134 $factory = MediaWikiServices
::getInstance()->getSpecialPageFactory();
135 if ( $factory->exists( $page->getText() ) ) {
139 return self
::getTitleFor( 'Search' );
143 * Get a localised Title object for a specified special page name
144 * If you don't need a full Title object, consider using TitleValue through
145 * getTitleValueFor() below.
148 * @since 1.21 $fragment parameter added
150 * @param string $name
151 * @param string|false|null $subpage Subpage string, or false/null to not use a subpage
152 * @param string $fragment The link fragment (after the "#")
155 public static function getTitleFor( $name, $subpage = false, $fragment = '' ) {
156 return Title
::newFromLinkTarget(
157 self
::getTitleValueFor( $name, $subpage, $fragment )
162 * Get a localised TitleValue object for a specified special page name
165 * @param string $name
166 * @param string|false|null $subpage Subpage string, or false/null to not use a subpage
167 * @param string $fragment The link fragment (after the "#")
170 public static function getTitleValueFor( $name, $subpage = false, $fragment = '' ) {
171 $name = MediaWikiServices
::getInstance()->getSpecialPageFactory()->
172 getLocalNameFor( $name, $subpage );
174 return new TitleValue( NS_SPECIAL
, $name, $fragment );
178 * Get a localised Title object for a page name with a possibly unvalidated subpage
180 * @param string $name
181 * @param string|false $subpage Subpage string, or false to not use a subpage
182 * @return Title|null Title object or null if the page doesn't exist
184 public static function getSafeTitleFor( $name, $subpage = false ) {
185 $name = MediaWikiServices
::getInstance()->getSpecialPageFactory()->
186 getLocalNameFor( $name, $subpage );
188 return Title
::makeTitleSafe( NS_SPECIAL
, $name );
195 * Default constructor for special pages
196 * Derivative classes should call this from their constructor
197 * Note that if the user does not have the required level, an error message will
198 * be displayed by the default execute() method, without the global function ever
201 * If you override execute(), you can recover the default behavior with userCanExecute()
202 * and displayRestrictionError()
206 * @param string $name Name of the special page, as seen in links and URLs
207 * @param string $restriction User right required, e.g. "block" or "delete"
208 * @param bool $listed Whether the page is listed in Special:Specialpages
209 * @param callable|bool $function Unused
210 * @param string $file Unused
211 * @param bool $includable Whether the page can be included in normal pages
213 public function __construct(
214 $name = '', $restriction = '', $listed = true,
215 $function = false, $file = '', $includable = false
217 $this->mName
= $name;
218 $this->mRestriction
= $restriction;
219 $this->mListed
= $listed;
220 $this->mIncludable
= $includable;
224 * Get the canonical, unlocalized name of this special page without namespace.
227 public function getName() {
232 * Get the permission that a user must have to execute this page
235 public function getRestriction() {
236 return $this->mRestriction
;
239 // @todo FIXME: Decide which syntax to use for this, and stick to it
242 * Whether this special page is listed in Special:SpecialPages
243 * @stable to override
247 public function isListed() {
248 return $this->mListed
;
252 * Whether it's allowed to transclude the special page via {{Special:Foo/params}}
253 * @stable to override
256 public function isIncludable() {
257 return $this->mIncludable
;
261 * How long to cache page when it is being included.
263 * @note If cache time is not 0, then the current user becomes an anon
264 * if you want to do any per-user customizations, than this method
265 * must be overridden to return 0.
267 * @stable to override
268 * @return int Time in seconds, 0 to disable caching altogether,
269 * false to use the parent page's cache settings
271 public function maxIncludeCacheTime() {
272 return $this->getConfig()->get( MainConfigNames
::MiserMode
) ?
$this->getCacheTTL() : 0;
276 * @stable to override
277 * @return int Seconds that this page can be cached
279 protected function getCacheTTL() {
284 * Whether the special page is being evaluated via transclusion
285 * @param bool|null $x
288 public function including( $x = null ) {
289 return wfSetVar( $this->mIncluding
, $x );
293 * Get the localised name of the special page
294 * @stable to override
297 public function getLocalName() {
298 if ( $this->mLocalName
=== null ) {
299 $this->mLocalName
= $this->getSpecialPageFactory()->getLocalNameFor( $this->mName
);
302 return $this->mLocalName
;
306 * Is this page expensive (for some definition of expensive)?
307 * Expensive pages are disabled or cached in miser mode. Originally used
308 * (and still overridden) by QueryPage and subclasses, moved here so that
309 * Special:SpecialPages can safely call it for all special pages.
311 * @stable to override
314 public function isExpensive() {
319 * Is this page cached?
320 * Expensive pages are cached or disabled in miser mode.
321 * Used by QueryPage and subclasses, moved here so that
322 * Special:SpecialPages can safely call it for all special pages.
324 * @stable to override
328 public function isCached() {
333 * Can be overridden by subclasses with more complicated permissions
336 * @stable to override
337 * @return bool Should the page be displayed with the restricted-access
340 public function isRestricted() {
341 // DWIM: If anons can do something, then it is not restricted
342 return $this->mRestriction
!= '' && !MediaWikiServices
::getInstance()
343 ->getGroupPermissionsLookup()
344 ->groupHasPermission( '*', $this->mRestriction
);
348 * Checks if the given user (identified by an object) can execute this
349 * special page (as defined by $mRestriction). Can be overridden by sub-
350 * classes with more complicated permissions schemes.
352 * @stable to override
353 * @param User $user The user to check
354 * @return bool Does the user have permission to view the page?
356 public function userCanExecute( User
$user ) {
357 return MediaWikiServices
::getInstance()
358 ->getPermissionManager()
359 ->userHasRight( $user, $this->mRestriction
);
363 * Utility function for authorizing an action to be performed by the special
364 * page. User blocks and rate limits are enforced implicitly.
366 * @see Authority::authorizeAction.
368 * @param ?string $action If not given, the action returned by
369 * getRestriction() will be used.
371 * @return PermissionStatus
373 protected function authorizeAction( ?
string $action = null ): PermissionStatus
{
374 $action ??
= $this->getRestriction();
377 return PermissionStatus
::newGood();
380 $status = PermissionStatus
::newEmpty();
381 $this->getAuthority()->authorizeAction( $action, $status );
386 * Output an error message telling the user what access level they have to have
387 * @stable to override
388 * @throws PermissionsError
391 protected function displayRestrictionError() {
392 throw new PermissionsError( $this->mRestriction
);
396 * Checks if userCanExecute, and if not throws a PermissionsError
398 * @stable to override
401 * @throws PermissionsError
403 public function checkPermissions() {
404 if ( !$this->userCanExecute( $this->getUser() ) ) {
405 $this->displayRestrictionError();
410 * If the wiki is currently in readonly mode, throws a ReadOnlyError
414 * @throws ReadOnlyError
416 public function checkReadOnly() {
417 // Can not inject the ReadOnlyMode as it would break the installer since
418 // it instantiates SpecialPageFactory before the DB (via ParserFactory for message parsing)
419 if ( MediaWikiServices
::getInstance()->getReadOnlyMode()->isReadOnly() ) {
420 throw new ReadOnlyError
;
425 * If the user is not logged in, throws UserNotLoggedIn error
427 * The user will be redirected to Special:Userlogin with the given message as an error on
431 * @param string $reasonMsg [optional] Message key to be displayed on login page
432 * @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor
433 * @throws UserNotLoggedIn
435 public function requireLogin(
436 $reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin'
438 if ( $this->getUser()->isAnon() ) {
439 throw new UserNotLoggedIn( $reasonMsg, $titleMsg );
444 * If the user is not logged in or is a temporary user, throws UserNotLoggedIn
447 * @param string $reasonMsg [optional] Message key to be displayed on login page
448 * @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor. Default 'exception-nologin'
449 * which is used when $titleMsg is null.
450 * @param bool $alwaysRedirectToLoginPage [optional] Should the redirect always go to Special:UserLogin?
451 * If false (the default), the redirect will be to Special:CreateAccount when the user is logged in to
452 * a temporary account.
453 * @throws UserNotLoggedIn
455 public function requireNamedUser(
456 $reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin', bool $alwaysRedirectToLoginPage = false
458 if ( !$this->getUser()->isNamed() ) {
459 throw new UserNotLoggedIn( $reasonMsg, $titleMsg, [], $alwaysRedirectToLoginPage );
464 * Tells if the special page does something security-sensitive and needs extra defense against
465 * a stolen account (e.g. a reauthentication). What exactly that will mean is decided by the
466 * authentication framework.
467 * @stable to override
468 * @return string|false False or the argument for AuthManager::securitySensitiveOperationStatus().
469 * Typically a special page needing elevated security would return its name here.
471 protected function getLoginSecurityLevel() {
476 * Record preserved POST data after a reauthentication.
478 * This is called from checkLoginSecurityLevel() when returning from the
479 * redirect for reauthentication, if the redirect had been served in
480 * response to a POST request.
482 * The base SpecialPage implementation does nothing. If your subclass uses
483 * getLoginSecurityLevel() or checkLoginSecurityLevel(), it should probably
484 * implement this to do something with the data.
486 * @note Call self::setAuthManager from special page constructor when overriding
488 * @stable to override
492 protected function setReauthPostData( array $data ) {
496 * Verifies that the user meets the security level, possibly reauthenticating them in the process.
498 * This should be used when the page does something security-sensitive and needs extra defense
499 * against a stolen account (e.g. a reauthentication). The authentication framework will make
500 * an extra effort to make sure the user account is not compromised. What that exactly means
501 * will depend on the system and user settings; e.g. the user might be required to log in again
502 * unless their last login happened recently, or they might be given a second-factor challenge.
504 * Calling this method will result in one if these actions:
505 * - return true: all good.
506 * - return false and set a redirect: caller should abort; the redirect will take the user
507 * to the login page for reauthentication, and back.
508 * - throw an exception if there is no way for the user to meet the requirements without using
509 * a different access method (e.g. this functionality is only available from a specific IP).
511 * Note that this does not in any way check that the user is authorized to use this special page
512 * (use checkPermissions() for that).
514 * @param string|null $level A security level. Can be an arbitrary string, defaults to the page
516 * @return bool False means a redirect to the reauthentication page has been set and processing
517 * of the special page should be aborted.
518 * @throws ErrorPageError If the security level cannot be met, even with reauthentication.
520 protected function checkLoginSecurityLevel( $level = null ) {
521 $level = $level ?
: $this->getName();
522 $key = 'SpecialPage:reauth:' . $this->getName();
523 $request = $this->getRequest();
525 $securityStatus = $this->getAuthManager()->securitySensitiveOperationStatus( $level );
526 if ( $securityStatus === AuthManager
::SEC_OK
) {
527 $uniqueId = $request->getVal( 'postUniqueId' );
529 $key .= ':' . $uniqueId;
530 $session = $request->getSession();
531 $data = $session->getSecret( $key );
533 $session->remove( $key );
534 $this->setReauthPostData( $data );
538 } elseif ( $securityStatus === AuthManager
::SEC_REAUTH
) {
539 $title = self
::getTitleFor( 'Userlogin' );
540 $queryParams = $request->getQueryValues();
542 if ( $request->wasPosted() ) {
543 $data = array_diff_assoc( $request->getValues(), $request->getQueryValues() );
545 // unique ID in case the same special page is open in multiple browser tabs
546 $uniqueId = MWCryptRand
::generateHex( 6 );
547 $key .= ':' . $uniqueId;
548 $queryParams['postUniqueId'] = $uniqueId;
549 $session = $request->getSession();
550 $session->persist(); // Just in case
551 $session->setSecret( $key, $data );
556 'returnto' => $this->getFullTitle()->getPrefixedDBkey(),
557 'returntoquery' => wfArrayToCgi( array_diff_key( $queryParams, [ 'title' => true ] ) ),
560 $url = $title->getFullURL( $query, false, PROTO_HTTPS
);
562 $this->getOutput()->redirect( $url );
566 $titleMessage = wfMessage( 'specialpage-securitylevel-not-allowed-title' );
567 $errorMessage = wfMessage( 'specialpage-securitylevel-not-allowed' );
568 throw new ErrorPageError( $titleMessage, $errorMessage );
572 * Set the injected AuthManager from the special page constructor
575 * @param AuthManager $authManager
577 final protected function setAuthManager( AuthManager
$authManager ) {
578 $this->authManager
= $authManager;
582 * @note Call self::setAuthManager from special page constructor when using
585 * @return AuthManager
587 final protected function getAuthManager(): AuthManager
{
588 if ( $this->authManager
=== null ) {
589 // Fallback if not provided
590 // TODO Change to wfWarn in a future release
591 $this->authManager
= MediaWikiServices
::getInstance()->getAuthManager();
593 return $this->authManager
;
597 * Return an array of subpages beginning with $search that this special page will accept.
599 * For example, if a page supports subpages "foo", "bar" and "baz" (as in Special:PageName/foo,
602 * - `prefixSearchSubpages( "ba" )` should return `[ "bar", "baz" ]`
603 * - `prefixSearchSubpages( "f" )` should return `[ "foo" ]`
604 * - `prefixSearchSubpages( "z" )` should return `[]`
605 * - `prefixSearchSubpages( "" )` should return `[ foo", "bar", "baz" ]`
607 * @stable to override
608 * @param string $search Prefix to search for
609 * @param int $limit Maximum number of results to return (usually 10)
610 * @param int $offset Number of results to skip (usually 0)
611 * @return string[] Matching subpages
613 public function prefixSearchSubpages( $search, $limit, $offset ) {
614 $subpages = $this->getSubpagesForPrefixSearch();
619 return self
::prefixSearchArray( $search, $limit, $subpages, $offset );
623 * Return an array of subpages that this special page will accept for prefix
624 * searches. If this method requires a query you might instead want to implement
625 * prefixSearchSubpages() directly so you can support $limit and $offset. This
626 * method is better for static-ish lists of things.
628 * @stable to override
629 * @return string[] subpages to search from
631 protected function getSubpagesForPrefixSearch() {
636 * Return an array of strings representing page titles that are discoverable to end users via UI.
639 * @stable to call or override
640 * @return string[] strings representing page titles that can be rendered by skins if required.
642 public function getAssociatedNavigationLinks() {
647 * Perform a regular substring search for prefixSearchSubpages
648 * @since 1.36 Added $searchEngineFactory parameter
649 * @param string $search Prefix to search for
650 * @param int $limit Maximum number of results to return (usually 10)
651 * @param int $offset Number of results to skip (usually 0)
652 * @param SearchEngineFactory|null $searchEngineFactory Provide the service
653 * @return string[] Matching subpages
655 protected function prefixSearchString(
659 ?SearchEngineFactory
$searchEngineFactory = null
661 $title = Title
::newFromText( $search );
662 if ( !$title ||
!$title->canExist() ) {
663 // No prefix suggestion in special and media namespace
667 $searchEngine = $searchEngineFactory
668 ?
$searchEngineFactory->create()
669 // Fallback if not provided
670 // TODO Change to wfWarn in a future release
671 : MediaWikiServices
::getInstance()->newSearchEngine();
672 $searchEngine->setLimitOffset( $limit, $offset );
673 $searchEngine->setNamespaces( [] );
674 $result = $searchEngine->defaultPrefixSearch( $search );
675 return array_map( static function ( Title
$t ) {
676 return $t->getPrefixedText();
681 * Helper function for implementations of prefixSearchSubpages() that
682 * filter the values in memory (as opposed to making a query).
685 * @param string $search
687 * @param array $subpages
691 protected static function prefixSearchArray( $search, $limit, array $subpages, $offset ) {
692 $escaped = preg_quote( $search, '/' );
693 return array_slice( preg_grep( "/^$escaped/i",
694 array_slice( $subpages, $offset ) ), 0, $limit );
698 * Sets headers - this should be called from the execute() method of all derived classes!
699 * @stable to override
701 protected function setHeaders() {
702 $out = $this->getOutput();
703 $out->setArticleRelated( false );
704 $out->setRobotPolicy( $this->getRobotPolicy() );
705 $title = $this->getDescription();
707 if ( is_string( $title ) ) {
708 wfDeprecated( "string return from {$this->getName()}::getDescription()", '1.41' );
709 $title = ( new RawMessage( '$1' ) )->rawParams( $title );
711 $out->setPageTitleMsg( $title );
719 * @param string|null $subPage
721 final public function run( $subPage ) {
722 if ( !$this->getHookRunner()->onSpecialPageBeforeExecute( $this, $subPage ) ) {
726 if ( $this->beforeExecute( $subPage ) === false ) {
729 $this->execute( $subPage );
730 $this->afterExecute( $subPage );
732 $this->getHookRunner()->onSpecialPageAfterExecute( $this, $subPage );
736 * Gets called before @see SpecialPage::execute.
737 * Return false to prevent calling execute() (since 1.27+).
739 * @stable to override
742 * @param string|null $subPage
745 protected function beforeExecute( $subPage ) {
750 * Gets called after @see SpecialPage::execute.
752 * @stable to override
755 * @param string|null $subPage
757 protected function afterExecute( $subPage ) {
762 * Default execute method
763 * Checks user permissions
765 * This must be overridden by subclasses; it will be made abstract in a future version
767 * @stable to override
769 * @param string|null $subPage
771 public function execute( $subPage ) {
773 $this->checkPermissions();
774 $securityLevel = $this->getLoginSecurityLevel();
775 if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) {
778 $this->outputHeader();
782 * Outputs a summary message on top of special pages
783 * By default the message key is the canonical name of the special page
784 * May be overridden, i.e. by extensions to stick with the naming conventions
785 * for message keys: 'extensionname-xxx'
787 * @stable to override
789 * @param string $summaryMessageKey Message key of the summary
791 protected function outputHeader( $summaryMessageKey = '' ) {
792 if ( $summaryMessageKey == '' ) {
793 $msg = strtolower( $this->getName() ) . '-summary';
795 $msg = $summaryMessageKey;
797 if ( !$this->msg( $msg )->isDisabled() && !$this->including() ) {
798 $this->getOutput()->wrapWikiMsg(
799 "<div class='mw-specialpage-summary'>\n$1\n</div>", $msg );
804 * Returns the name that goes in the \<h1\> in the special page itself, and
805 * also the name that will be listed in Special:Specialpages
807 * Derived classes can override this, but usually it is easier to keep the
810 * Returning a string from this method has been deprecated since 1.41.
812 * @stable to override
814 * @return string|Message
816 public function getDescription() {
817 return $this->msg( strtolower( $this->mName
) );
821 * Similar to getDescription, but takes into account subpages and designed for display
825 * @stable to override if special page has complex parameter handling. Use default message keys
828 * @param string $path (optional)
831 public function getShortDescription( string $path = '' ): string {
832 $lowerPath = strtolower( str_replace( '/', '-', $path ) );
833 $shortKey = 'special-tab-' . $lowerPath;
834 $shortKey .= '-short';
835 $msgShort = $this->msg( $shortKey );
836 return $msgShort->text();
840 * Get a self-referential title object
842 * @param string|false|null $subpage
846 public function getPageTitle( $subpage = false ) {
847 return self
::getTitleFor( $this->mName
, $subpage );
851 * Sets the context this SpecialPage is executed in
853 * @param IContextSource $context
856 public function setContext( $context ) {
857 $this->mContext
= $context;
861 * Gets the context this SpecialPage is executed in
863 * @return IContextSource|RequestContext
866 public function getContext() {
867 if ( !( $this->mContext
instanceof IContextSource
) ) {
868 wfDebug( __METHOD__
. " called and \$mContext is null. " .
869 "Using RequestContext::getMain()" );
871 $this->mContext
= RequestContext
::getMain();
873 return $this->mContext
;
877 * Get the WebRequest being used for this instance
882 public function getRequest() {
883 return $this->getContext()->getRequest();
887 * Get the OutputPage being used for this instance
892 public function getOutput() {
893 return $this->getContext()->getOutput();
897 * Shortcut to get the User executing this instance
902 public function getUser() {
903 return $this->getContext()->getUser();
907 * Shortcut to get the Authority executing this instance
912 public function getAuthority(): Authority
{
913 return $this->getContext()->getAuthority();
917 * Shortcut to get the skin being used for this instance
922 public function getSkin() {
923 return $this->getContext()->getSkin();
927 * Shortcut to get user's language
932 public function getLanguage() {
933 return $this->getContext()->getLanguage();
937 * Shortcut to get content language
942 final public function getContentLanguage(): Language
{
943 if ( $this->contentLanguage
=== null ) {
944 // Fallback if not provided
945 // TODO Change to wfWarn in a future release
946 $this->contentLanguage
= MediaWikiServices
::getInstance()->getContentLanguage();
948 return $this->contentLanguage
;
952 * Set content language
954 * @internal For factory only
955 * @param Language $contentLanguage
958 final public function setContentLanguage( Language
$contentLanguage ) {
959 $this->contentLanguage
= $contentLanguage;
963 * Shortcut to get main config object
967 public function getConfig() {
968 return $this->getContext()->getConfig();
972 * Return the full title, including $par
977 public function getFullTitle() {
978 return $this->getContext()->getTitle();
982 * Return the robot policy. Derived classes that override this can change
983 * the robot policy set by setHeaders() from the default 'noindex,nofollow'.
988 protected function getRobotPolicy() {
989 return 'noindex,nofollow';
993 * Wrapper around wfMessage that sets the current context.
996 * @param string|string[]|MessageSpecifier $key
997 * @phpcs:ignore Generic.Files.LineLength
998 * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$params
999 * See Message::params()
1003 public function msg( $key, ...$params ) {
1004 $message = $this->getContext()->msg( $key, ...$params );
1005 // RequestContext passes context to wfMessage, and the language is set from
1006 // the context, but setting the language for Message class removes the
1007 // interface message status, which breaks for example usernameless gender
1008 // invocations. Restore the flag when not including special page in content.
1009 if ( $this->including() ) {
1010 $message->setInterfaceMessageFlag( false );
1017 * Adds RSS/atom links
1019 * @param array $params
1021 protected function addFeedLinks( $params ) {
1022 $feedTemplate = wfScript( 'api' );
1024 foreach ( $this->getConfig()->get( MainConfigNames
::FeedClasses
) as $format => $class ) {
1025 $theseParams = $params +
[ 'feedformat' => $format ];
1026 $url = wfAppendQuery( $feedTemplate, $theseParams );
1027 $this->getOutput()->addFeedLink( $format, $url );
1032 * Adds help link with an icon via page indicators.
1033 * Link target can be overridden by a local message containing a wikilink:
1034 * the message key is: lowercase special page name + '-helppage'.
1035 * @param string $to Target MediaWiki.org page title or encoded URL.
1036 * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
1039 public function addHelpLink( $to, $overrideBaseUrl = false ) {
1040 if ( $this->including() ) {
1044 $msg = $this->msg( strtolower( $this->getName() ) . '-helppage' );
1046 if ( !$msg->isDisabled() ) {
1047 $title = Title
::newFromText( $msg->plain() );
1048 if ( $title instanceof Title
) {
1049 $this->getOutput()->addHelpLink( $title->getLocalURL(), true );
1052 $this->getOutput()->addHelpLink( $to, $overrideBaseUrl );
1057 * Get the group that the special page belongs in on Special:SpecialPage
1058 * Use this method, instead of getGroupName to allow customization
1059 * of the group name from the wiki side
1061 * @return string Group of this special page
1064 public function getFinalGroupName() {
1065 $name = $this->getName();
1067 // Allow overriding the group from the wiki side
1068 $msg = $this->msg( 'specialpages-specialpagegroup-' . strtolower( $name ) )->inContentLanguage();
1069 if ( !$msg->isBlank() ) {
1070 $group = $msg->text();
1072 // Than use the group from this object
1073 $group = $this->getGroupName();
1080 * Indicates whether POST requests to this special page require write access to the wiki.
1082 * Subclasses must override this method to return true if any of the operations that
1083 * they perform on POST requests are not "safe" per RFC 7231 section 4.2.1. A subclass's
1084 * operation is "safe" if it is essentially read-only, i.e. the client does not request
1085 * nor expect any state change that would be observable in the responses to future requests.
1087 * Implementations of this method must always return the same value, regardless of the
1088 * parameters passed to the constructor or system state.
1090 * When handling GET/HEAD requests, subclasses should only perform "safe" operations.
1091 * Note that some subclasses might only perform "safe" operations even for POST requests,
1092 * particularly in the case where large input parameters are required.
1094 * @stable to override
1099 public function doesWrites() {
1104 * Under which header this special page is listed in Special:SpecialPages
1105 * See messages 'specialpages-group-*' for valid names
1106 * This method defaults to group 'other'
1108 * @stable to override
1113 protected function getGroupName() {
1118 * Call wfTransactionalTimeLimit() if this request was POSTed
1121 protected function useTransactionalTimeLimit() {
1122 if ( $this->getRequest()->wasPosted() ) {
1123 wfTransactionalTimeLimit();
1129 * @return LinkRenderer
1131 public function getLinkRenderer(): LinkRenderer
{
1132 if ( $this->linkRenderer
=== null ) {
1133 // TODO Inject the service
1134 $this->linkRenderer
= MediaWikiServices
::getInstance()->getLinkRendererFactory()
1137 return $this->linkRenderer
;
1142 * @param LinkRenderer $linkRenderer
1144 public function setLinkRenderer( LinkRenderer
$linkRenderer ) {
1145 $this->linkRenderer
= $linkRenderer;
1149 * Generate (prev x| next x) (20|50|100...) type links for paging
1151 * @param int $offset
1153 * @param array $query Optional URL query parameter string
1154 * @param bool $atend Optional param for specified if this is the last page
1155 * @param string|false $subpage Optional param for specifying subpage
1158 protected function buildPrevNextNavigation(
1165 $navBuilder = new PagerNavigationBuilder( $this );
1167 ->setPage( $this->getPageTitle( $subpage ) )
1168 ->setLinkQuery( [ 'limit' => $limit, 'offset' => $offset ] +
$query )
1169 ->setLimitLinkQueryParam( 'limit' )
1170 ->setCurrentLimit( $limit )
1171 ->setPrevTooltipMsg( 'prevn-title' )
1172 ->setNextTooltipMsg( 'nextn-title' )
1173 ->setLimitTooltipMsg( 'shown-title' );
1175 if ( $offset > 0 ) {
1176 $navBuilder->setPrevLinkQuery( [ 'offset' => (string)max( $offset - $limit, 0 ) ] );
1179 $navBuilder->setNextLinkQuery( [ 'offset' => (string)( $offset +
$limit ) ] );
1182 return $navBuilder->getHtml();
1188 * @param HookContainer $hookContainer
1190 public function setHookContainer( HookContainer
$hookContainer ) {
1191 $this->hookContainer
= $hookContainer;
1192 $this->hookRunner
= new HookRunner( $hookContainer );
1197 * @return HookContainer
1199 protected function getHookContainer() {
1200 if ( !$this->hookContainer
) {
1201 $this->hookContainer
= MediaWikiServices
::getInstance()->getHookContainer();
1203 return $this->hookContainer
;
1207 * @internal This is for use by core only. Hook interfaces may be removed
1210 * @return HookRunner
1212 protected function getHookRunner() {
1213 if ( !$this->hookRunner
) {
1214 $this->hookRunner
= new HookRunner( $this->getHookContainer() );
1216 return $this->hookRunner
;
1220 * @internal For factory only
1222 * @param SpecialPageFactory $specialPageFactory
1224 final public function setSpecialPageFactory( SpecialPageFactory
$specialPageFactory ) {
1225 $this->specialPageFactory
= $specialPageFactory;
1230 * @return SpecialPageFactory
1232 final protected function getSpecialPageFactory(): SpecialPageFactory
{
1233 if ( !$this->specialPageFactory
) {
1234 // Fallback if not provided
1235 // TODO Change to wfWarn in a future release
1236 $this->specialPageFactory
= MediaWikiServices
::getInstance()->getSpecialPageFactory();
1238 return $this->specialPageFactory
;
1242 /** @deprecated class alias since 1.41 */
1243 class_alias( SpecialPage
::class, 'SpecialPage' );