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