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