SpecialPage.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  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. use MediaWiki\Auth\AuthManager;
  24. use MediaWiki\Linker\LinkRenderer;
  25. use MediaWiki\MediaWikiServices;
  26. use MediaWiki\Navigation\PrevNextNavigationRenderer;
  27. /**
  28. * Parent class for all special pages.
  29. *
  30. * Includes some static functions for handling the special page list deprecated
  31. * in favor of SpecialPageFactory.
  32. *
  33. * @ingroup SpecialPage
  34. */
  35. class SpecialPage implements MessageLocalizer {
  36. // The canonical name of this special page
  37. // Also used for the default <h1> heading, @see getDescription()
  38. protected $mName;
  39. // The local name of this special page
  40. private $mLocalName;
  41. // Minimum user level required to access this page, or "" for anyone.
  42. // Also used to categorise the pages in Special:Specialpages
  43. protected $mRestriction;
  44. // Listed in Special:Specialpages?
  45. private $mListed;
  46. // Whether or not this special page is being included from an article
  47. protected $mIncluding;
  48. // Whether the special page can be included in an article
  49. protected $mIncludable;
  50. /**
  51. * Current request context
  52. * @var IContextSource
  53. */
  54. protected $mContext;
  55. /**
  56. * @var \MediaWiki\Linker\LinkRenderer|null
  57. */
  58. private $linkRenderer;
  59. /**
  60. * Get a localised Title object for a specified special page name
  61. * If you don't need a full Title object, consider using TitleValue through
  62. * getTitleValueFor() below.
  63. *
  64. * @since 1.9
  65. * @since 1.21 $fragment parameter added
  66. *
  67. * @param string $name
  68. * @param string|bool $subpage Subpage string, or false to not use a subpage
  69. * @param string $fragment The link fragment (after the "#")
  70. * @return Title
  71. * @throws MWException
  72. */
  73. public static function getTitleFor( $name, $subpage = false, $fragment = '' ) {
  74. return Title::newFromTitleValue(
  75. self::getTitleValueFor( $name, $subpage, $fragment )
  76. );
  77. }
  78. /**
  79. * Get a localised TitleValue object for a specified special page name
  80. *
  81. * @since 1.28
  82. * @param string $name
  83. * @param string|bool $subpage Subpage string, or false to not use a subpage
  84. * @param string $fragment The link fragment (after the "#")
  85. * @return TitleValue
  86. */
  87. public static function getTitleValueFor( $name, $subpage = false, $fragment = '' ) {
  88. $name = MediaWikiServices::getInstance()->getSpecialPageFactory()->
  89. getLocalNameFor( $name, $subpage );
  90. return new TitleValue( NS_SPECIAL, $name, $fragment );
  91. }
  92. /**
  93. * Get a localised Title object for a page name with a possibly unvalidated subpage
  94. *
  95. * @param string $name
  96. * @param string|bool $subpage Subpage string, or false to not use a subpage
  97. * @return Title|null Title object or null if the page doesn't exist
  98. */
  99. public static function getSafeTitleFor( $name, $subpage = false ) {
  100. $name = MediaWikiServices::getInstance()->getSpecialPageFactory()->
  101. getLocalNameFor( $name, $subpage );
  102. if ( $name ) {
  103. return Title::makeTitleSafe( NS_SPECIAL, $name );
  104. } else {
  105. return null;
  106. }
  107. }
  108. /**
  109. * Default constructor for special pages
  110. * Derivative classes should call this from their constructor
  111. * Note that if the user does not have the required level, an error message will
  112. * be displayed by the default execute() method, without the global function ever
  113. * being called.
  114. *
  115. * If you override execute(), you can recover the default behavior with userCanExecute()
  116. * and displayRestrictionError()
  117. *
  118. * @param string $name Name of the special page, as seen in links and URLs
  119. * @param string $restriction User right required, e.g. "block" or "delete"
  120. * @param bool $listed Whether the page is listed in Special:Specialpages
  121. * @param callable|bool $function Unused
  122. * @param string $file Unused
  123. * @param bool $includable Whether the page can be included in normal pages
  124. */
  125. public function __construct(
  126. $name = '', $restriction = '', $listed = true,
  127. $function = false, $file = '', $includable = false
  128. ) {
  129. $this->mName = $name;
  130. $this->mRestriction = $restriction;
  131. $this->mListed = $listed;
  132. $this->mIncludable = $includable;
  133. }
  134. /**
  135. * Get the name of this Special Page.
  136. * @return string
  137. */
  138. function getName() {
  139. return $this->mName;
  140. }
  141. /**
  142. * Get the permission that a user must have to execute this page
  143. * @return string
  144. */
  145. function getRestriction() {
  146. return $this->mRestriction;
  147. }
  148. // @todo FIXME: Decide which syntax to use for this, and stick to it
  149. /**
  150. * Whether this special page is listed in Special:SpecialPages
  151. * @since 1.3 (r3583)
  152. * @return bool
  153. */
  154. function isListed() {
  155. return $this->mListed;
  156. }
  157. /**
  158. * Set whether this page is listed in Special:Specialpages, at run-time
  159. * @since 1.3
  160. * @param bool $listed
  161. * @return bool
  162. */
  163. function setListed( $listed ) {
  164. return wfSetVar( $this->mListed, $listed );
  165. }
  166. /**
  167. * Get or set whether this special page is listed in Special:SpecialPages
  168. * @since 1.6
  169. * @param bool|null $x
  170. * @return bool
  171. */
  172. function listed( $x = null ) {
  173. return wfSetVar( $this->mListed, $x );
  174. }
  175. /**
  176. * Whether it's allowed to transclude the special page via {{Special:Foo/params}}
  177. * @return bool
  178. */
  179. public function isIncludable() {
  180. return $this->mIncludable;
  181. }
  182. /**
  183. * How long to cache page when it is being included.
  184. *
  185. * @note If cache time is not 0, then the current user becomes an anon
  186. * if you want to do any per-user customizations, than this method
  187. * must be overriden to return 0.
  188. * @since 1.26
  189. * @return int Time in seconds, 0 to disable caching altogether,
  190. * false to use the parent page's cache settings
  191. */
  192. public function maxIncludeCacheTime() {
  193. return $this->getConfig()->get( 'MiserMode' ) ? $this->getCacheTTL() : 0;
  194. }
  195. /**
  196. * @return int Seconds that this page can be cached
  197. */
  198. protected function getCacheTTL() {
  199. return 60 * 60;
  200. }
  201. /**
  202. * Whether the special page is being evaluated via transclusion
  203. * @param bool|null $x
  204. * @return bool
  205. */
  206. function including( $x = null ) {
  207. return wfSetVar( $this->mIncluding, $x );
  208. }
  209. /**
  210. * Get the localised name of the special page
  211. * @return string
  212. */
  213. function getLocalName() {
  214. if ( !isset( $this->mLocalName ) ) {
  215. $this->mLocalName = MediaWikiServices::getInstance()->getSpecialPageFactory()->
  216. getLocalNameFor( $this->mName );
  217. }
  218. return $this->mLocalName;
  219. }
  220. /**
  221. * Is this page expensive (for some definition of expensive)?
  222. * Expensive pages are disabled or cached in miser mode. Originally used
  223. * (and still overridden) by QueryPage and subclasses, moved here so that
  224. * Special:SpecialPages can safely call it for all special pages.
  225. *
  226. * @return bool
  227. */
  228. public function isExpensive() {
  229. return false;
  230. }
  231. /**
  232. * Is this page cached?
  233. * Expensive pages are cached or disabled in miser mode.
  234. * Used by QueryPage and subclasses, moved here so that
  235. * Special:SpecialPages can safely call it for all special pages.
  236. *
  237. * @return bool
  238. * @since 1.21
  239. */
  240. public function isCached() {
  241. return false;
  242. }
  243. /**
  244. * Can be overridden by subclasses with more complicated permissions
  245. * schemes.
  246. *
  247. * @return bool Should the page be displayed with the restricted-access
  248. * pages?
  249. */
  250. public function isRestricted() {
  251. // DWIM: If anons can do something, then it is not restricted
  252. return $this->mRestriction != '' && !MediaWikiServices::getInstance()
  253. ->getPermissionManager()
  254. ->groupHasPermission( '*', $this->mRestriction );
  255. }
  256. /**
  257. * Checks if the given user (identified by an object) can execute this
  258. * special page (as defined by $mRestriction). Can be overridden by sub-
  259. * classes with more complicated permissions schemes.
  260. *
  261. * @param User $user The user to check
  262. * @return bool Does the user have permission to view the page?
  263. */
  264. public function userCanExecute( User $user ) {
  265. return MediaWikiServices::getInstance()
  266. ->getPermissionManager()
  267. ->userHasRight( $user, $this->mRestriction );
  268. }
  269. /**
  270. * Output an error message telling the user what access level they have to have
  271. * @throws PermissionsError
  272. */
  273. function displayRestrictionError() {
  274. throw new PermissionsError( $this->mRestriction );
  275. }
  276. /**
  277. * Checks if userCanExecute, and if not throws a PermissionsError
  278. *
  279. * @since 1.19
  280. * @return void
  281. * @throws PermissionsError
  282. */
  283. public function checkPermissions() {
  284. if ( !$this->userCanExecute( $this->getUser() ) ) {
  285. $this->displayRestrictionError();
  286. }
  287. }
  288. /**
  289. * If the wiki is currently in readonly mode, throws a ReadOnlyError
  290. *
  291. * @since 1.19
  292. * @return void
  293. * @throws ReadOnlyError
  294. */
  295. public function checkReadOnly() {
  296. if ( wfReadOnly() ) {
  297. throw new ReadOnlyError;
  298. }
  299. }
  300. /**
  301. * If the user is not logged in, throws UserNotLoggedIn error
  302. *
  303. * The user will be redirected to Special:Userlogin with the given message as an error on
  304. * the form.
  305. *
  306. * @since 1.23
  307. * @param string $reasonMsg [optional] Message key to be displayed on login page
  308. * @param string $titleMsg [optional] Passed on to UserNotLoggedIn constructor
  309. * @throws UserNotLoggedIn
  310. */
  311. public function requireLogin(
  312. $reasonMsg = 'exception-nologin-text', $titleMsg = 'exception-nologin'
  313. ) {
  314. if ( $this->getUser()->isAnon() ) {
  315. throw new UserNotLoggedIn( $reasonMsg, $titleMsg );
  316. }
  317. }
  318. /**
  319. * Tells if the special page does something security-sensitive and needs extra defense against
  320. * a stolen account (e.g. a reauthentication). What exactly that will mean is decided by the
  321. * authentication framework.
  322. * @return bool|string False or the argument for AuthManager::securitySensitiveOperationStatus().
  323. * Typically a special page needing elevated security would return its name here.
  324. */
  325. protected function getLoginSecurityLevel() {
  326. return false;
  327. }
  328. /**
  329. * Record preserved POST data after a reauthentication.
  330. *
  331. * This is called from checkLoginSecurityLevel() when returning from the
  332. * redirect for reauthentication, if the redirect had been served in
  333. * response to a POST request.
  334. *
  335. * The base SpecialPage implementation does nothing. If your subclass uses
  336. * getLoginSecurityLevel() or checkLoginSecurityLevel(), it should probably
  337. * implement this to do something with the data.
  338. *
  339. * @since 1.32
  340. * @param array $data
  341. */
  342. protected function setReauthPostData( array $data ) {
  343. }
  344. /**
  345. * Verifies that the user meets the security level, possibly reauthenticating them in the process.
  346. *
  347. * This should be used when the page does something security-sensitive and needs extra defense
  348. * against a stolen account (e.g. a reauthentication). The authentication framework will make
  349. * an extra effort to make sure the user account is not compromised. What that exactly means
  350. * will depend on the system and user settings; e.g. the user might be required to log in again
  351. * unless their last login happened recently, or they might be given a second-factor challenge.
  352. *
  353. * Calling this method will result in one if these actions:
  354. * - return true: all good.
  355. * - return false and set a redirect: caller should abort; the redirect will take the user
  356. * to the login page for reauthentication, and back.
  357. * - throw an exception if there is no way for the user to meet the requirements without using
  358. * a different access method (e.g. this functionality is only available from a specific IP).
  359. *
  360. * Note that this does not in any way check that the user is authorized to use this special page
  361. * (use checkPermissions() for that).
  362. *
  363. * @param string|null $level A security level. Can be an arbitrary string, defaults to the page
  364. * name.
  365. * @return bool False means a redirect to the reauthentication page has been set and processing
  366. * of the special page should be aborted.
  367. * @throws ErrorPageError If the security level cannot be met, even with reauthentication.
  368. */
  369. protected function checkLoginSecurityLevel( $level = null ) {
  370. $level = $level ?: $this->getName();
  371. $key = 'SpecialPage:reauth:' . $this->getName();
  372. $request = $this->getRequest();
  373. $securityStatus = AuthManager::singleton()->securitySensitiveOperationStatus( $level );
  374. if ( $securityStatus === AuthManager::SEC_OK ) {
  375. $uniqueId = $request->getVal( 'postUniqueId' );
  376. if ( $uniqueId ) {
  377. $key .= ':' . $uniqueId;
  378. $session = $request->getSession();
  379. $data = $session->getSecret( $key );
  380. if ( $data ) {
  381. $session->remove( $key );
  382. $this->setReauthPostData( $data );
  383. }
  384. }
  385. return true;
  386. } elseif ( $securityStatus === AuthManager::SEC_REAUTH ) {
  387. $title = self::getTitleFor( 'Userlogin' );
  388. $queryParams = $request->getQueryValues();
  389. if ( $request->wasPosted() ) {
  390. $data = array_diff_assoc( $request->getValues(), $request->getQueryValues() );
  391. if ( $data ) {
  392. // unique ID in case the same special page is open in multiple browser tabs
  393. $uniqueId = MWCryptRand::generateHex( 6 );
  394. $key .= ':' . $uniqueId;
  395. $queryParams['postUniqueId'] = $uniqueId;
  396. $session = $request->getSession();
  397. $session->persist(); // Just in case
  398. $session->setSecret( $key, $data );
  399. }
  400. }
  401. $query = [
  402. 'returnto' => $this->getFullTitle()->getPrefixedDBkey(),
  403. 'returntoquery' => wfArrayToCgi( array_diff_key( $queryParams, [ 'title' => true ] ) ),
  404. 'force' => $level,
  405. ];
  406. $url = $title->getFullURL( $query, false, PROTO_HTTPS );
  407. $this->getOutput()->redirect( $url );
  408. return false;
  409. }
  410. $titleMessage = wfMessage( 'specialpage-securitylevel-not-allowed-title' );
  411. $errorMessage = wfMessage( 'specialpage-securitylevel-not-allowed' );
  412. throw new ErrorPageError( $titleMessage, $errorMessage );
  413. }
  414. /**
  415. * Return an array of subpages beginning with $search that this special page will accept.
  416. *
  417. * For example, if a page supports subpages "foo", "bar" and "baz" (as in Special:PageName/foo,
  418. * etc.):
  419. *
  420. * - `prefixSearchSubpages( "ba" )` should return `[ "bar", "baz" ]`
  421. * - `prefixSearchSubpages( "f" )` should return `[ "foo" ]`
  422. * - `prefixSearchSubpages( "z" )` should return `[]`
  423. * - `prefixSearchSubpages( "" )` should return `[ foo", "bar", "baz" ]`
  424. *
  425. * @param string $search Prefix to search for
  426. * @param int $limit Maximum number of results to return (usually 10)
  427. * @param int $offset Number of results to skip (usually 0)
  428. * @return string[] Matching subpages
  429. */
  430. public function prefixSearchSubpages( $search, $limit, $offset ) {
  431. $subpages = $this->getSubpagesForPrefixSearch();
  432. if ( !$subpages ) {
  433. return [];
  434. }
  435. return self::prefixSearchArray( $search, $limit, $subpages, $offset );
  436. }
  437. /**
  438. * Return an array of subpages that this special page will accept for prefix
  439. * searches. If this method requires a query you might instead want to implement
  440. * prefixSearchSubpages() directly so you can support $limit and $offset. This
  441. * method is better for static-ish lists of things.
  442. *
  443. * @return string[] subpages to search from
  444. */
  445. protected function getSubpagesForPrefixSearch() {
  446. return [];
  447. }
  448. /**
  449. * Perform a regular substring search for prefixSearchSubpages
  450. * @param string $search Prefix to search for
  451. * @param int $limit Maximum number of results to return (usually 10)
  452. * @param int $offset Number of results to skip (usually 0)
  453. * @return string[] Matching subpages
  454. */
  455. protected function prefixSearchString( $search, $limit, $offset ) {
  456. $title = Title::newFromText( $search );
  457. if ( !$title || !$title->canExist() ) {
  458. // No prefix suggestion in special and media namespace
  459. return [];
  460. }
  461. $searchEngine = MediaWikiServices::getInstance()->newSearchEngine();
  462. $searchEngine->setLimitOffset( $limit, $offset );
  463. $searchEngine->setNamespaces( [] );
  464. $result = $searchEngine->defaultPrefixSearch( $search );
  465. return array_map( function ( Title $t ) {
  466. return $t->getPrefixedText();
  467. }, $result );
  468. }
  469. /**
  470. * Helper function for implementations of prefixSearchSubpages() that
  471. * filter the values in memory (as opposed to making a query).
  472. *
  473. * @since 1.24
  474. * @param string $search
  475. * @param int $limit
  476. * @param array $subpages
  477. * @param int $offset
  478. * @return string[]
  479. */
  480. protected static function prefixSearchArray( $search, $limit, array $subpages, $offset ) {
  481. $escaped = preg_quote( $search, '/' );
  482. return array_slice( preg_grep( "/^$escaped/i",
  483. array_slice( $subpages, $offset ) ), 0, $limit );
  484. }
  485. /**
  486. * Sets headers - this should be called from the execute() method of all derived classes!
  487. */
  488. function setHeaders() {
  489. $out = $this->getOutput();
  490. $out->setArticleRelated( false );
  491. $out->setRobotPolicy( $this->getRobotPolicy() );
  492. $out->setPageTitle( $this->getDescription() );
  493. if ( $this->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
  494. $out->addModuleStyles( [
  495. 'mediawiki.ui.input',
  496. 'mediawiki.ui.radio',
  497. 'mediawiki.ui.checkbox',
  498. ] );
  499. }
  500. }
  501. /**
  502. * Entry point.
  503. *
  504. * @since 1.20
  505. *
  506. * @param string|null $subPage
  507. */
  508. final public function run( $subPage ) {
  509. /**
  510. * Gets called before @see SpecialPage::execute.
  511. * Return false to prevent calling execute() (since 1.27+).
  512. *
  513. * @since 1.20
  514. *
  515. * @param SpecialPage $this
  516. * @param string|null $subPage
  517. */
  518. if ( !Hooks::run( 'SpecialPageBeforeExecute', [ $this, $subPage ] ) ) {
  519. return;
  520. }
  521. if ( $this->beforeExecute( $subPage ) === false ) {
  522. return;
  523. }
  524. $this->execute( $subPage );
  525. $this->afterExecute( $subPage );
  526. /**
  527. * Gets called after @see SpecialPage::execute.
  528. *
  529. * @since 1.20
  530. *
  531. * @param SpecialPage $this
  532. * @param string|null $subPage
  533. */
  534. Hooks::run( 'SpecialPageAfterExecute', [ $this, $subPage ] );
  535. }
  536. /**
  537. * Gets called before @see SpecialPage::execute.
  538. * Return false to prevent calling execute() (since 1.27+).
  539. *
  540. * @since 1.20
  541. *
  542. * @param string|null $subPage
  543. * @return bool|void
  544. */
  545. protected function beforeExecute( $subPage ) {
  546. // No-op
  547. }
  548. /**
  549. * Gets called after @see SpecialPage::execute.
  550. *
  551. * @since 1.20
  552. *
  553. * @param string|null $subPage
  554. */
  555. protected function afterExecute( $subPage ) {
  556. // No-op
  557. }
  558. /**
  559. * Default execute method
  560. * Checks user permissions
  561. *
  562. * This must be overridden by subclasses; it will be made abstract in a future version
  563. *
  564. * @param string|null $subPage
  565. */
  566. public function execute( $subPage ) {
  567. $this->setHeaders();
  568. $this->checkPermissions();
  569. $securityLevel = $this->getLoginSecurityLevel();
  570. if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) {
  571. return;
  572. }
  573. $this->outputHeader();
  574. }
  575. /**
  576. * Outputs a summary message on top of special pages
  577. * Per default the message key is the canonical name of the special page
  578. * May be overridden, i.e. by extensions to stick with the naming conventions
  579. * for message keys: 'extensionname-xxx'
  580. *
  581. * @param string $summaryMessageKey Message key of the summary
  582. */
  583. function outputHeader( $summaryMessageKey = '' ) {
  584. if ( $summaryMessageKey == '' ) {
  585. $msg = MediaWikiServices::getInstance()->getContentLanguage()->lc( $this->getName() ) .
  586. '-summary';
  587. } else {
  588. $msg = $summaryMessageKey;
  589. }
  590. if ( !$this->msg( $msg )->isDisabled() && !$this->including() ) {
  591. $this->getOutput()->wrapWikiMsg(
  592. "<div class='mw-specialpage-summary'>\n$1\n</div>", $msg );
  593. }
  594. }
  595. /**
  596. * Returns the name that goes in the \<h1\> in the special page itself, and
  597. * also the name that will be listed in Special:Specialpages
  598. *
  599. * Derived classes can override this, but usually it is easier to keep the
  600. * default behavior.
  601. *
  602. * @return string
  603. */
  604. function getDescription() {
  605. return $this->msg( strtolower( $this->mName ) )->text();
  606. }
  607. /**
  608. * Get a self-referential title object
  609. *
  610. * @param string|bool $subpage
  611. * @return Title
  612. * @since 1.23
  613. */
  614. function getPageTitle( $subpage = false ) {
  615. return self::getTitleFor( $this->mName, $subpage );
  616. }
  617. /**
  618. * Sets the context this SpecialPage is executed in
  619. *
  620. * @param IContextSource $context
  621. * @since 1.18
  622. */
  623. public function setContext( $context ) {
  624. $this->mContext = $context;
  625. }
  626. /**
  627. * Gets the context this SpecialPage is executed in
  628. *
  629. * @return IContextSource|RequestContext
  630. * @since 1.18
  631. */
  632. public function getContext() {
  633. if ( $this->mContext instanceof IContextSource ) {
  634. return $this->mContext;
  635. } else {
  636. wfDebug( __METHOD__ . " called and \$mContext is null. " .
  637. "Return RequestContext::getMain(); for sanity\n" );
  638. return RequestContext::getMain();
  639. }
  640. }
  641. /**
  642. * Get the WebRequest being used for this instance
  643. *
  644. * @return WebRequest
  645. * @since 1.18
  646. */
  647. public function getRequest() {
  648. return $this->getContext()->getRequest();
  649. }
  650. /**
  651. * Get the OutputPage being used for this instance
  652. *
  653. * @return OutputPage
  654. * @since 1.18
  655. */
  656. public function getOutput() {
  657. return $this->getContext()->getOutput();
  658. }
  659. /**
  660. * Shortcut to get the User executing this instance
  661. *
  662. * @return User
  663. * @since 1.18
  664. */
  665. public function getUser() {
  666. return $this->getContext()->getUser();
  667. }
  668. /**
  669. * Shortcut to get the skin being used for this instance
  670. *
  671. * @return Skin
  672. * @since 1.18
  673. */
  674. public function getSkin() {
  675. return $this->getContext()->getSkin();
  676. }
  677. /**
  678. * Shortcut to get user's language
  679. *
  680. * @return Language
  681. * @since 1.19
  682. */
  683. public function getLanguage() {
  684. return $this->getContext()->getLanguage();
  685. }
  686. /**
  687. * Shortcut to get main config object
  688. * @return Config
  689. * @since 1.24
  690. */
  691. public function getConfig() {
  692. return $this->getContext()->getConfig();
  693. }
  694. /**
  695. * Return the full title, including $par
  696. *
  697. * @return Title
  698. * @since 1.18
  699. */
  700. public function getFullTitle() {
  701. return $this->getContext()->getTitle();
  702. }
  703. /**
  704. * Return the robot policy. Derived classes that override this can change
  705. * the robot policy set by setHeaders() from the default 'noindex,nofollow'.
  706. *
  707. * @return string
  708. * @since 1.23
  709. */
  710. protected function getRobotPolicy() {
  711. return 'noindex,nofollow';
  712. }
  713. /**
  714. * Wrapper around wfMessage that sets the current context.
  715. *
  716. * @since 1.16
  717. * @param string|string[]|MessageSpecifier $key
  718. * @param mixed ...$params
  719. * @return Message
  720. * @see wfMessage
  721. */
  722. public function msg( $key, ...$params ) {
  723. $message = $this->getContext()->msg( $key, ...$params );
  724. // RequestContext passes context to wfMessage, and the language is set from
  725. // the context, but setting the language for Message class removes the
  726. // interface message status, which breaks for example usernameless gender
  727. // invocations. Restore the flag when not including special page in content.
  728. if ( $this->including() ) {
  729. $message->setInterfaceMessageFlag( false );
  730. }
  731. return $message;
  732. }
  733. /**
  734. * Adds RSS/atom links
  735. *
  736. * @param array $params
  737. */
  738. protected function addFeedLinks( $params ) {
  739. $feedTemplate = wfScript( 'api' );
  740. foreach ( $this->getConfig()->get( 'FeedClasses' ) as $format => $class ) {
  741. $theseParams = $params + [ 'feedformat' => $format ];
  742. $url = wfAppendQuery( $feedTemplate, $theseParams );
  743. $this->getOutput()->addFeedLink( $format, $url );
  744. }
  745. }
  746. /**
  747. * Adds help link with an icon via page indicators.
  748. * Link target can be overridden by a local message containing a wikilink:
  749. * the message key is: lowercase special page name + '-helppage'.
  750. * @param string $to Target MediaWiki.org page title or encoded URL.
  751. * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
  752. * @since 1.25
  753. */
  754. public function addHelpLink( $to, $overrideBaseUrl = false ) {
  755. if ( $this->including() ) {
  756. return;
  757. }
  758. $msg = $this->msg(
  759. MediaWikiServices::getInstance()->getContentLanguage()->lc( $this->getName() ) .
  760. '-helppage' );
  761. if ( !$msg->isDisabled() ) {
  762. $helpUrl = Skin::makeUrl( $msg->plain() );
  763. $this->getOutput()->addHelpLink( $helpUrl, true );
  764. } else {
  765. $this->getOutput()->addHelpLink( $to, $overrideBaseUrl );
  766. }
  767. }
  768. /**
  769. * Get the group that the special page belongs in on Special:SpecialPage
  770. * Use this method, instead of getGroupName to allow customization
  771. * of the group name from the wiki side
  772. *
  773. * @return string Group of this special page
  774. * @since 1.21
  775. */
  776. public function getFinalGroupName() {
  777. $name = $this->getName();
  778. // Allow overriding the group from the wiki side
  779. $msg = $this->msg( 'specialpages-specialpagegroup-' . strtolower( $name ) )->inContentLanguage();
  780. if ( !$msg->isBlank() ) {
  781. $group = $msg->text();
  782. } else {
  783. // Than use the group from this object
  784. $group = $this->getGroupName();
  785. }
  786. return $group;
  787. }
  788. /**
  789. * Indicates whether this special page may perform database writes
  790. *
  791. * @return bool
  792. * @since 1.27
  793. */
  794. public function doesWrites() {
  795. return false;
  796. }
  797. /**
  798. * Under which header this special page is listed in Special:SpecialPages
  799. * See messages 'specialpages-group-*' for valid names
  800. * This method defaults to group 'other'
  801. *
  802. * @return string
  803. * @since 1.21
  804. */
  805. protected function getGroupName() {
  806. return 'other';
  807. }
  808. /**
  809. * Call wfTransactionalTimeLimit() if this request was POSTed
  810. * @since 1.26
  811. */
  812. protected function useTransactionalTimeLimit() {
  813. if ( $this->getRequest()->wasPosted() ) {
  814. wfTransactionalTimeLimit();
  815. }
  816. }
  817. /**
  818. * @since 1.28
  819. * @return \MediaWiki\Linker\LinkRenderer
  820. */
  821. public function getLinkRenderer() {
  822. if ( $this->linkRenderer ) {
  823. return $this->linkRenderer;
  824. } else {
  825. return MediaWikiServices::getInstance()->getLinkRenderer();
  826. }
  827. }
  828. /**
  829. * @since 1.28
  830. * @param \MediaWiki\Linker\LinkRenderer $linkRenderer
  831. */
  832. public function setLinkRenderer( LinkRenderer $linkRenderer ) {
  833. $this->linkRenderer = $linkRenderer;
  834. }
  835. /**
  836. * Generate (prev x| next x) (20|50|100...) type links for paging
  837. *
  838. * @param int $offset
  839. * @param int $limit
  840. * @param array $query Optional URL query parameter string
  841. * @param bool $atend Optional param for specified if this is the last page
  842. * @param string|bool $subpage Optional param for specifying subpage
  843. * @return string
  844. */
  845. protected function buildPrevNextNavigation( $offset, $limit,
  846. array $query = [], $atend = false, $subpage = false
  847. ) {
  848. $title = $this->getPageTitle( $subpage );
  849. $prevNext = new PrevNextNavigationRenderer( $this );
  850. return $prevNext->buildPrevNextNavigation( $title, $offset, $limit, $query, $atend );
  851. }
  852. }