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        if ( is_string( $title ) ) { // T343849
697            wfDeprecated( 'string return from SpecialPage::getDescription()', '1.41' );
698            $title = ( new RawMessage( '$1' ) )->rawParams( $title );
699        }
700        $out->setPageTitleMsg( $title );
701    }
702
703    /**
704     * Entry point.
705     *
706     * @since 1.20
707     *
708     * @param string|null $subPage
709     */
710    final public function run( $subPage ) {
711        if ( !$this->getHookRunner()->onSpecialPageBeforeExecute( $this, $subPage ) ) {
712            return;
713        }
714
715        if ( $this->beforeExecute( $subPage ) === false ) {
716            return;
717        }
718        $this->execute( $subPage );
719        $this->afterExecute( $subPage );
720
721        $this->getHookRunner()->onSpecialPageAfterExecute( $this, $subPage );
722    }
723
724    /**
725     * Gets called before @see SpecialPage::execute.
726     * Return false to prevent calling execute() (since 1.27+).
727     *
728     * @stable to override
729     * @since 1.20
730     *
731     * @param string|null $subPage
732     * @return bool|void
733     */
734    protected function beforeExecute( $subPage ) {
735        // No-op
736    }
737
738    /**
739     * Gets called after @see SpecialPage::execute.
740     *
741     * @stable to override
742     * @since 1.20
743     *
744     * @param string|null $subPage
745     */
746    protected function afterExecute( $subPage ) {
747        // No-op
748    }
749
750    /**
751     * Default execute method
752     * Checks user permissions
753     *
754     * This must be overridden by subclasses; it will be made abstract in a future version
755     *
756     * @stable to override
757     *
758     * @param string|null $subPage
759     */
760    public function execute( $subPage ) {
761        $this->setHeaders();
762        $this->checkPermissions();
763        $securityLevel = $this->getLoginSecurityLevel();
764        if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) {
765            return;
766        }
767        $this->outputHeader();
768    }
769
770    /**
771     * Outputs a summary message on top of special pages
772     * Per default the message key is the canonical name of the special page
773     * May be overridden, i.e. by extensions to stick with the naming conventions
774     * for message keys: 'extensionname-xxx'
775     *
776     * @stable to override
777     *
778     * @param string $summaryMessageKey Message key of the summary
779     */
780    protected function outputHeader( $summaryMessageKey = '' ) {
781        if ( $summaryMessageKey == '' ) {
782            $msg = strtolower( $this->getName() ) . '-summary';
783        } else {
784            $msg = $summaryMessageKey;
785        }
786        if ( !$this->msg( $msg )->isDisabled() && !$this->including() ) {
787            $this->getOutput()->wrapWikiMsg(
788                "<div class='mw-specialpage-summary'>\n$1\n</div>", $msg );
789        }
790    }
791
792    /**
793     * Returns the name that goes in the \<h1\> in the special page itself, and
794     * also the name that will be listed in Special:Specialpages
795     *
796     * Derived classes can override this, but usually it is easier to keep the
797     * default behavior.
798     *
799     * Returning a string from this method has been deprecated since 1.41.
800     *
801     * @stable to override
802     *
803     * @return string|Message
804     */
805    public function getDescription() {
806        return $this->msg( strtolower( $this->mName ) );
807    }
808
809    /**
810     * Similar to getDescription but takes into account sub pages and designed for display
811     * in tabs.
812     *
813     * @since 1.39
814     * @stable to override if special page has complex parameter handling. Use default message keys
815     * where possible.
816     *
817     * @param string $path (optional)
818     * @return string
819     */
820    public function getShortDescription( string $path = '' ): string {
821        $lowerPath = strtolower( str_replace( '/', '-', $path ) );
822        $shortKey = 'special-tab-' . $lowerPath;
823        $shortKey .= '-short';
824        $msgShort = $this->msg( $shortKey );
825        return $msgShort->text();
826    }
827
828    /**
829     * Get a self-referential title object
830     *
831     * @param string|false|null $subpage
832     * @return Title
833     * @since 1.23
834     */
835    public function getPageTitle( $subpage = false ) {
836        return self::getTitleFor( $this->mName, $subpage );
837    }
838
839    /**
840     * Sets the context this SpecialPage is executed in
841     *
842     * @param IContextSource $context
843     * @since 1.18
844     */
845    public function setContext( $context ) {
846        $this->mContext = $context;
847    }
848
849    /**
850     * Gets the context this SpecialPage is executed in
851     *
852     * @return IContextSource|RequestContext
853     * @since 1.18
854     */
855    public function getContext() {
856        if ( !( $this->mContext instanceof IContextSource ) ) {
857            wfDebug( __METHOD__ . " called and \$mContext is null. " .
858                "Using RequestContext::getMain()" );
859
860            $this->mContext = RequestContext::getMain();
861        }
862        return $this->mContext;
863    }
864
865    /**
866     * Get the WebRequest being used for this instance
867     *
868     * @return WebRequest
869     * @since 1.18
870     */
871    public function getRequest() {
872        return $this->getContext()->getRequest();
873    }
874
875    /**
876     * Get the OutputPage being used for this instance
877     *
878     * @return OutputPage
879     * @since 1.18
880     */
881    public function getOutput() {
882        return $this->getContext()->getOutput();
883    }
884
885    /**
886     * Shortcut to get the User executing this instance
887     *
888     * @return User
889     * @since 1.18
890     */
891    public function getUser() {
892        return $this->getContext()->getUser();
893    }
894
895    /**
896     * Shortcut to get the Authority executing this instance
897     *
898     * @return Authority
899     * @since 1.36
900     */
901    public function getAuthority(): Authority {
902        return $this->getContext()->getAuthority();
903    }
904
905    /**
906     * Shortcut to get the skin being used for this instance
907     *
908     * @return Skin
909     * @since 1.18
910     */
911    public function getSkin() {
912        return $this->getContext()->getSkin();
913    }
914
915    /**
916     * Shortcut to get user's language
917     *
918     * @return Language
919     * @since 1.19
920     */
921    public function getLanguage() {
922        return $this->getContext()->getLanguage();
923    }
924
925    /**
926     * Shortcut to get content language
927     *
928     * @return Language
929     * @since 1.36
930     */
931    final public function getContentLanguage(): Language {
932        if ( $this->contentLanguage === null ) {
933            // Fallback if not provided
934            // TODO Change to wfWarn in a future release
935            $this->contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
936        }
937        return $this->contentLanguage;
938    }
939
940    /**
941     * Set content language
942     *
943     * @internal For factory only
944     * @param Language $contentLanguage
945     * @since 1.36
946     */
947    final public function setContentLanguage( Language $contentLanguage ) {
948        $this->contentLanguage = $contentLanguage;
949    }
950
951    /**
952     * Shortcut to get main config object
953     * @return Config
954     * @since 1.24
955     */
956    public function getConfig() {
957        return $this->getContext()->getConfig();
958    }
959
960    /**
961     * Return the full title, including $par
962     *
963     * @return Title
964     * @since 1.18
965     */
966    public function getFullTitle() {
967        return $this->getContext()->getTitle();
968    }
969
970    /**
971     * Return the robot policy. Derived classes that override this can change
972     * the robot policy set by setHeaders() from the default 'noindex,nofollow'.
973     *
974     * @return string
975     * @since 1.23
976     */
977    protected function getRobotPolicy() {
978        return 'noindex,nofollow';
979    }
980
981    /**
982     * Wrapper around wfMessage that sets the current context.
983     *
984     * @since 1.16
985     * @param string|string[]|MessageSpecifier $key
986     * @param mixed ...$params
987     * @return Message
988     * @see wfMessage
989     */
990    public function msg( $key, ...$params ) {
991        $message = $this->getContext()->msg( $key, ...$params );
992        // RequestContext passes context to wfMessage, and the language is set from
993        // the context, but setting the language for Message class removes the
994        // interface message status, which breaks for example usernameless gender
995        // invocations. Restore the flag when not including special page in content.
996        if ( $this->including() ) {
997            $message->setInterfaceMessageFlag( false );
998        }
999
1000        return $message;
1001    }
1002
1003    /**
1004     * Adds RSS/atom links
1005     *
1006     * @param array $params
1007     */
1008    protected function addFeedLinks( $params ) {
1009        $feedTemplate = wfScript( 'api' );
1010
1011        foreach ( $this->getConfig()->get( MainConfigNames::FeedClasses ) as $format => $class ) {
1012            $theseParams = $params + [ 'feedformat' => $format ];
1013            $url = wfAppendQuery( $feedTemplate, $theseParams );
1014            $this->getOutput()->addFeedLink( $format, $url );
1015        }
1016    }
1017
1018    /**
1019     * Adds help link with an icon via page indicators.
1020     * Link target can be overridden by a local message containing a wikilink:
1021     * the message key is: lowercase special page name + '-helppage'.
1022     * @param string $to Target MediaWiki.org page title or encoded URL.
1023     * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
1024     * @since 1.25
1025     */
1026    public function addHelpLink( $to, $overrideBaseUrl = false ) {
1027        if ( $this->including() ) {
1028            return;
1029        }
1030
1031        $msg = $this->msg( strtolower( $this->getName() ) . '-helppage' );
1032
1033        if ( !$msg->isDisabled() ) {
1034            $title = Title::newFromText( $msg->plain() );
1035            if ( $title instanceof Title ) {
1036                $this->getOutput()->addHelpLink( $title->getLocalURL(), true );
1037            }
1038        } else {
1039            $this->getOutput()->addHelpLink( $to, $overrideBaseUrl );
1040        }
1041    }
1042
1043    /**
1044     * Get the group that the special page belongs in on Special:SpecialPage
1045     * Use this method, instead of getGroupName to allow customization
1046     * of the group name from the wiki side
1047     *
1048     * @return string Group of this special page
1049     * @since 1.21
1050     */
1051    public function getFinalGroupName() {
1052        $name = $this->getName();
1053
1054        // Allow overriding the group from the wiki side
1055        $msg = $this->msg( 'specialpages-specialpagegroup-' . strtolower( $name ) )->inContentLanguage();
1056        if ( !$msg->isBlank() ) {
1057            $group = $msg->text();
1058        } else {
1059            // Than use the group from this object
1060            $group = $this->getGroupName();
1061        }
1062
1063        return $group;
1064    }
1065
1066    /**
1067     * Indicates whether this special page may perform database writes
1068     *
1069     * @stable to override
1070     *
1071     * @return bool
1072     * @since 1.27
1073     */
1074    public function doesWrites() {
1075        return false;
1076    }
1077
1078    /**
1079     * Under which header this special page is listed in Special:SpecialPages
1080     * See messages 'specialpages-group-*' for valid names
1081     * This method defaults to group 'other'
1082     *
1083     * @stable to override
1084     *
1085     * @return string
1086     * @since 1.21
1087     */
1088    protected function getGroupName() {
1089        return 'other';
1090    }
1091
1092    /**
1093     * Call wfTransactionalTimeLimit() if this request was POSTed
1094     * @since 1.26
1095     */
1096    protected function useTransactionalTimeLimit() {
1097        if ( $this->getRequest()->wasPosted() ) {
1098            wfTransactionalTimeLimit();
1099        }
1100    }
1101
1102    /**
1103     * @since 1.28
1104     * @return LinkRenderer
1105     */
1106    public function getLinkRenderer(): LinkRenderer {
1107        if ( $this->linkRenderer === null ) {
1108            // TODO Inject the service
1109            $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRendererFactory()
1110                ->create();
1111        }
1112        return $this->linkRenderer;
1113    }
1114
1115    /**
1116     * @since 1.28
1117     * @param LinkRenderer $linkRenderer
1118     */
1119    public function setLinkRenderer( LinkRenderer $linkRenderer ) {
1120        $this->linkRenderer = $linkRenderer;
1121    }
1122
1123    /**
1124     * Generate (prev x| next x) (20|50|100...) type links for paging
1125     *
1126     * @param int $offset
1127     * @param int $limit
1128     * @param array $query Optional URL query parameter string
1129     * @param bool $atend Optional param for specified if this is the last page
1130     * @param string|false $subpage Optional param for specifying subpage
1131     * @return string
1132     */
1133    protected function buildPrevNextNavigation(
1134        $offset,
1135        $limit,
1136        array $query = [],
1137        $atend = false,
1138        $subpage = false
1139    ) {
1140        $navBuilder = new PagerNavigationBuilder( $this );
1141        $navBuilder
1142            ->setPage( $this->getPageTitle( $subpage ) )
1143            ->setLinkQuery( [ 'limit' => $limit, 'offset' => $offset ] + $query )
1144            ->setLimitLinkQueryParam( 'limit' )
1145            ->setCurrentLimit( $limit )
1146            ->setPrevTooltipMsg( 'prevn-title' )
1147            ->setNextTooltipMsg( 'nextn-title' )
1148            ->setLimitTooltipMsg( 'shown-title' );
1149
1150        if ( $offset > 0 ) {
1151            $navBuilder->setPrevLinkQuery( [ 'offset' => (string)max( $offset - $limit, 0 ) ] );
1152        }
1153        if ( !$atend ) {
1154            $navBuilder->setNextLinkQuery( [ 'offset' => (string)( $offset + $limit ) ] );
1155        }
1156
1157        return $navBuilder->getHtml();
1158    }
1159
1160    /**
1161     * @since 1.35
1162     * @internal
1163     * @param HookContainer $hookContainer
1164     */
1165    public function setHookContainer( HookContainer $hookContainer ) {
1166        $this->hookContainer = $hookContainer;
1167        $this->hookRunner = new HookRunner( $hookContainer );
1168    }
1169
1170    /**
1171     * @since 1.35
1172     * @return HookContainer
1173     */
1174    protected function getHookContainer() {
1175        if ( !$this->hookContainer ) {
1176            $this->hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1177        }
1178        return $this->hookContainer;
1179    }
1180
1181    /**
1182     * @internal This is for use by core only. Hook interfaces may be removed
1183     *   without notice.
1184     * @since 1.35
1185     * @return HookRunner
1186     */
1187    protected function getHookRunner() {
1188        if ( !$this->hookRunner ) {
1189            $this->hookRunner = new HookRunner( $this->getHookContainer() );
1190        }
1191        return $this->hookRunner;
1192    }
1193
1194    /**
1195     * @internal For factory only
1196     * @since 1.36
1197     * @param SpecialPageFactory $specialPageFactory
1198     */
1199    final public function setSpecialPageFactory( SpecialPageFactory $specialPageFactory ) {
1200        $this->specialPageFactory = $specialPageFactory;
1201    }
1202
1203    /**
1204     * @since 1.36
1205     * @return SpecialPageFactory
1206     */
1207    final protected function getSpecialPageFactory(): SpecialPageFactory {
1208        if ( !$this->specialPageFactory ) {
1209            // Fallback if not provided
1210            // TODO Change to wfWarn in a future release
1211            $this->specialPageFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
1212        }
1213        return $this->specialPageFactory;
1214    }
1215}
1216
1217/** @deprecated class alias since 1.41 */
1218class_alias( SpecialPage::class, 'SpecialPage' );