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