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