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