1<?php 2/** 3 * Factory for handling the special page list and generating SpecialPage objects. 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 * @defgroup SpecialPage SpecialPage 23 */ 24 25namespace MediaWiki\SpecialPage; 26 27use IContextSource; 28use Language; 29use MediaWiki\Config\ServiceOptions; 30use MediaWiki\HookContainer\HookContainer; 31use MediaWiki\HookContainer\HookRunner; 32use MediaWiki\Linker\LinkRenderer; 33use MediaWiki\Page\PageReference; 34use Profiler; 35use RequestContext; 36use SpecialPage; 37use Title; 38use TitleFactory; 39use User; 40use Wikimedia\ObjectFactory; 41 42/** 43 * Factory for handling the special page list and generating SpecialPage objects. 44 * 45 * To add a special page in an extension, add to $wgSpecialPages either 46 * an object instance or an array containing the name and constructor 47 * parameters. The latter is preferred for performance reasons. 48 * 49 * The object instantiated must be either an instance of SpecialPage or a 50 * sub-class thereof. It must have an execute() method, which sends the HTML 51 * for the special page to $wgOut. The parent class has an execute() method 52 * which distributes the call to the historical global functions. Additionally, 53 * execute() also checks if the user has the necessary access privileges 54 * and bails out if not. 55 * 56 * To add a core special page, use the similar static list in 57 * SpecialPageFactory::$list. To remove a core static special page at runtime, use 58 * a SpecialPage_initList hook. 59 * 60 * @ingroup SpecialPage 61 * @since 1.17 62 */ 63class SpecialPageFactory { 64 /** 65 * List of special page names to the subclass of SpecialPage which handles them. 66 */ 67 private const CORE_LIST = [ 68 // Maintenance Reports 69 'BrokenRedirects' => [ 70 'class' => \SpecialBrokenRedirects::class, 71 'services' => [ 72 'ContentHandlerFactory', 73 'DBLoadBalancer', 74 'LinkBatchFactory', 75 ] 76 ], 77 'Deadendpages' => [ 78 'class' => \SpecialDeadendPages::class, 79 'services' => [ 80 'NamespaceInfo', 81 'DBLoadBalancer', 82 'LinkBatchFactory', 83 'LanguageConverterFactory', 84 ] 85 ], 86 'DoubleRedirects' => [ 87 'class' => \SpecialDoubleRedirects::class, 88 'services' => [ 89 'ContentHandlerFactory', 90 'LinkBatchFactory', 91 'DBLoadBalancer', 92 ] 93 ], 94 'Longpages' => [ 95 'class' => \SpecialLongPages::class, 96 'services' => [ 97 // Same as for Shortpages 98 'NamespaceInfo', 99 'DBLoadBalancer', 100 'LinkBatchFactory', 101 ] 102 ], 103 'Ancientpages' => [ 104 'class' => \SpecialAncientPages::class, 105 'services' => [ 106 'NamespaceInfo', 107 'DBLoadBalancer', 108 'LinkBatchFactory', 109 'LanguageConverterFactory', 110 ] 111 ], 112 'Lonelypages' => [ 113 'class' => \SpecialLonelyPages::class, 114 'services' => [ 115 'NamespaceInfo', 116 'DBLoadBalancer', 117 'LinkBatchFactory', 118 'LanguageConverterFactory', 119 ] 120 ], 121 'Fewestrevisions' => [ 122 'class' => \SpecialFewestRevisions::class, 123 'services' => [ 124 // Same as for Mostrevisions 125 'NamespaceInfo', 126 'DBLoadBalancer', 127 'LinkBatchFactory', 128 'LanguageConverterFactory', 129 ] 130 ], 131 'Withoutinterwiki' => [ 132 'class' => \SpecialWithoutInterwiki::class, 133 'services' => [ 134 'NamespaceInfo', 135 'DBLoadBalancer', 136 'LinkBatchFactory', 137 'LanguageConverterFactory', 138 ] 139 ], 140 'Protectedpages' => [ 141 'class' => \SpecialProtectedpages::class, 142 'services' => [ 143 'LinkBatchFactory', 144 'DBLoadBalancer', 145 'CommentStore', 146 'UserCache', 147 ] 148 ], 149 'Protectedtitles' => [ 150 'class' => \SpecialProtectedtitles::class, 151 'services' => [ 152 'LinkBatchFactory', 153 'DBLoadBalancer', 154 ] 155 ], 156 'Shortpages' => [ 157 'class' => \SpecialShortPages::class, 158 'services' => [ 159 // Same as for Longpages 160 'NamespaceInfo', 161 'DBLoadBalancer', 162 'LinkBatchFactory', 163 ] 164 ], 165 'Uncategorizedcategories' => [ 166 'class' => \SpecialUncategorizedCategories::class, 167 'services' => [ 168 // Same as for SpecialUncategorizedPages and SpecialUncategorizedTemplates 169 'NamespaceInfo', 170 'DBLoadBalancer', 171 'LinkBatchFactory', 172 'LanguageConverterFactory', 173 ] 174 ], 175 'Uncategorizedimages' => [ 176 'class' => \SpecialUncategorizedImages::class, 177 'services' => [ 178 'DBLoadBalancer', 179 ] 180 ], 181 'Uncategorizedpages' => [ 182 'class' => \SpecialUncategorizedPages::class, 183 'services' => [ 184 // Same as for SpecialUncategorizedCategories and SpecialUncategorizedTemplates 185 'NamespaceInfo', 186 'DBLoadBalancer', 187 'LinkBatchFactory', 188 'LanguageConverterFactory', 189 ] 190 ], 191 'Uncategorizedtemplates' => [ 192 'class' => \SpecialUncategorizedTemplates::class, 193 'services' => [ 194 // Same as for SpecialUncategorizedCategories and SpecialUncategorizedPages 195 'NamespaceInfo', 196 'DBLoadBalancer', 197 'LinkBatchFactory', 198 'LanguageConverterFactory', 199 ] 200 ], 201 'Unusedcategories' => [ 202 'class' => \SpecialUnusedCategories::class, 203 'services' => [ 204 'DBLoadBalancer', 205 'LinkBatchFactory', 206 ] 207 ], 208 'Unusedimages' => [ 209 'class' => \SpecialUnusedImages::class, 210 'services' => [ 211 'DBLoadBalancer', 212 ] 213 ], 214 'Unusedtemplates' => [ 215 'class' => \SpecialUnusedTemplates::class, 216 'services' => [ 217 'DBLoadBalancer', 218 ] 219 ], 220 'Unwatchedpages' => [ 221 'class' => \SpecialUnwatchedPages::class, 222 'services' => [ 223 'LinkBatchFactory', 224 'DBLoadBalancer', 225 'LanguageConverterFactory', 226 ] 227 ], 228 'Wantedcategories' => [ 229 'class' => \SpecialWantedCategories::class, 230 'services' => [ 231 'DBLoadBalancer', 232 'LinkBatchFactory', 233 'LanguageConverterFactory', 234 ] 235 ], 236 'Wantedfiles' => [ 237 'class' => \WantedFilesPage::class, 238 'services' => [ 239 'RepoGroup', 240 'DBLoadBalancer', 241 'LinkBatchFactory', 242 ] 243 ], 244 'Wantedpages' => [ 245 'class' => \WantedPagesPage::class, 246 'services' => [ 247 'DBLoadBalancer', 248 'LinkBatchFactory', 249 ] 250 ], 251 'Wantedtemplates' => [ 252 'class' => \SpecialWantedTemplates::class, 253 'services' => [ 254 'DBLoadBalancer', 255 'LinkBatchFactory', 256 ] 257 ], 258 259 // List of pages 260 'Allpages' => [ 261 'class' => \SpecialAllPages::class, 262 'services' => [ 263 'DBLoadBalancer', 264 'SearchEngineFactory', 265 ] 266 ], 267 'Prefixindex' => [ 268 'class' => \SpecialPrefixindex::class, 269 'services' => [ 270 'DBLoadBalancer', 271 'LinkCache', 272 ] 273 ], 274 'Categories' => [ 275 'class' => \SpecialCategories::class, 276 'services' => [ 277 'LinkBatchFactory', 278 'DBLoadBalancer', 279 ] 280 ], 281 'Listredirects' => [ 282 'class' => \SpecialListRedirects::class, 283 'services' => [ 284 'LinkBatchFactory', 285 'DBLoadBalancer', 286 'WikiPageFactory', 287 ] 288 ], 289 'PagesWithProp' => [ 290 'class' => \SpecialPagesWithProp::class, 291 'services' => [ 292 'DBLoadBalancer', 293 ] 294 ], 295 'TrackingCategories' => [ 296 'class' => \SpecialTrackingCategories::class, 297 'services' => [ 298 'LinkBatchFactory', 299 ] 300 ], 301 302 // Authentication 303 'Userlogin' => [ 304 'class' => \SpecialUserLogin::class, 305 'services' => [ 306 'AuthManager', 307 ] 308 ], 309 'Userlogout' => [ 310 'class' => \SpecialUserLogout::class, 311 ], 312 'CreateAccount' => [ 313 'class' => \SpecialCreateAccount::class, 314 'services' => [ 315 'AuthManager', 316 ] 317 ], 318 'LinkAccounts' => [ 319 'class' => \SpecialLinkAccounts::class, 320 'services' => [ 321 'AuthManager', 322 ] 323 ], 324 'UnlinkAccounts' => [ 325 'class' => \SpecialUnlinkAccounts::class, 326 'services' => [ 327 'AuthManager', 328 ] 329 ], 330 'ChangeCredentials' => [ 331 'class' => \SpecialChangeCredentials::class, 332 'services' => [ 333 'AuthManager', 334 ] 335 ], 336 'RemoveCredentials' => [ 337 'class' => \SpecialRemoveCredentials::class, 338 'services' => [ 339 'AuthManager', 340 ] 341 ], 342 343 // Users and rights 344 'Activeusers' => [ 345 'class' => \SpecialActiveUsers::class, 346 'services' => [ 347 'LinkBatchFactory', 348 'DBLoadBalancer', 349 'UserGroupManager', 350 ] 351 ], 352 'Block' => [ 353 'class' => \SpecialBlock::class, 354 'services' => [ 355 'BlockUtils', 356 'BlockPermissionCheckerFactory', 357 'BlockUserFactory', 358 'UserNameUtils', 359 'UserNamePrefixSearch', 360 'BlockActionInfo', 361 'TitleFormatter', 362 'NamespaceInfo' 363 ] 364 ], 365 'Unblock' => [ 366 'class' => \SpecialUnblock::class, 367 'services' => [ 368 'UnblockUserFactory', 369 'BlockUtils', 370 'UserNameUtils', 371 'UserNamePrefixSearch', 372 ] 373 ], 374 'BlockList' => [ 375 'class' => \SpecialBlockList::class, 376 'services' => [ 377 'LinkBatchFactory', 378 'BlockRestrictionStore', 379 'DBLoadBalancer', 380 'CommentStore', 381 'BlockUtils', 382 'BlockActionInfo', 383 ], 384 ], 385 'AutoblockList' => [ 386 'class' => \SpecialAutoblockList::class, 387 'services' => [ 388 'LinkBatchFactory', 389 'BlockRestrictionStore', 390 'DBLoadBalancer', 391 'CommentStore', 392 'BlockUtils', 393 'BlockActionInfo', 394 ], 395 ], 396 'ChangePassword' => [ 397 'class' => \SpecialChangePassword::class, 398 ], 399 'BotPasswords' => [ 400 'class' => \SpecialBotPasswords::class, 401 'services' => [ 402 'PasswordFactory', 403 'AuthManager', 404 'CentralIdLookup', 405 ] 406 ], 407 'PasswordReset' => [ 408 'class' => \SpecialPasswordReset::class, 409 'services' => [ 410 'PasswordReset' 411 ] 412 ], 413 'DeletedContributions' => [ 414 'class' => \SpecialDeletedContributions::class, 415 'services' => [ 416 'PermissionManager', 417 'DBLoadBalancer', 418 'CommentStore', 419 'RevisionFactory', 420 'NamespaceInfo', 421 'UserNameUtils', 422 'UserNamePrefixSearch', 423 ] 424 ], 425 'Preferences' => [ 426 'class' => \SpecialPreferences::class, 427 'services' => [ 428 'PreferencesFactory', 429 'UserOptionsManager', 430 ] 431 ], 432 'ResetTokens' => [ 433 'class' => \SpecialResetTokens::class, 434 ], 435 'Contributions' => [ 436 'class' => \SpecialContributions::class, 437 'services' => [ 438 'LinkBatchFactory', 439 'PermissionManager', 440 'DBLoadBalancer', 441 'ActorMigration', 442 'RevisionStore', 443 'NamespaceInfo', 444 'UserNameUtils', 445 'UserNamePrefixSearch', 446 'UserOptionsLookup', 447 ] 448 ], 449 'Listgrouprights' => [ 450 'class' => \SpecialListGroupRights::class, 451 'services' => [ 452 'NamespaceInfo', 453 'UserGroupManager', 454 'LanguageConverterFactory', 455 ] 456 ], 457 'Listgrants' => [ 458 'class' => \SpecialListGrants::class, 459 ], 460 'Listusers' => [ 461 'class' => \SpecialListUsers::class, 462 'services' => [ 463 'LinkBatchFactory', 464 'DBLoadBalancer', 465 'UserGroupManager', 466 ] 467 ], 468 'Listadmins' => [ 469 'class' => \SpecialListAdmins::class, 470 ], 471 'Listbots' => [ 472 'class' => \SpecialListBots::class, 473 ], 474 'Userrights' => [ 475 'class' => \UserrightsPage::class, 476 'services' => [ 477 'UserGroupManagerFactory', 478 'UserNameUtils', 479 'UserNamePrefixSearch', 480 ] 481 ], 482 'EditWatchlist' => [ 483 'class' => \SpecialEditWatchlist::class, 484 'services' => [ 485 'WatchedItemStore', 486 'TitleParser', 487 'GenderCache', 488 'LinkBatchFactory', 489 'NamespaceInfo', 490 'WikiPageFactory', 491 'WatchlistManager', 492 ] 493 ], 494 'PasswordPolicies' => [ 495 'class' => \SpecialPasswordPolicies::class 496 ], 497 498 // Recent changes and logs 499 'Newimages' => [ 500 'class' => \SpecialNewFiles::class, 501 'services' => [ 502 'MimeAnalyzer', 503 'GroupPermissionsLookup', 504 'DBLoadBalancer', 505 'LinkBatchFactory', 506 ] 507 ], 508 'Log' => [ 509 'class' => \SpecialLog::class, 510 'services' => [ 511 'LinkBatchFactory', 512 'DBLoadBalancer', 513 'ActorNormalization', 514 ] 515 ], 516 'Watchlist' => [ 517 'class' => \SpecialWatchlist::class, 518 'services' => [ 519 'WatchedItemStore', 520 'WatchlistManager', 521 'DBLoadBalancer', 522 'UserOptionsLookup', 523 ] 524 ], 525 'Newpages' => [ 526 'class' => \SpecialNewpages::class, 527 'services' => [ 528 'LinkBatchFactory', 529 'CommentStore', 530 'ContentHandlerFactory', 531 'GroupPermissionsLookup', 532 'DBLoadBalancer', 533 'RevisionLookup', 534 'NamespaceInfo', 535 'UserOptionsLookup', 536 ] 537 ], 538 'Recentchanges' => [ 539 'class' => \SpecialRecentChanges::class, 540 'services' => [ 541 'WatchedItemStore', 542 'MessageCache', 543 'DBLoadBalancer', 544 'UserOptionsLookup', 545 ] 546 ], 547 'Recentchangeslinked' => [ 548 'class' => \SpecialRecentChangesLinked::class, 549 'services' => [ 550 'WatchedItemStore', 551 'MessageCache', 552 'DBLoadBalancer', 553 'UserOptionsLookup', 554 'SearchEngineFactory', 555 ] 556 ], 557 'Tags' => [ 558 'class' => \SpecialTags::class, 559 ], 560 561 // Media reports and uploads 562 'Listfiles' => [ 563 'class' => \SpecialListFiles::class, 564 'services' => [ 565 'RepoGroup', 566 'DBLoadBalancer', 567 'CommentStore', 568 'UserNameUtils', 569 'UserNamePrefixSearch', 570 'UserCache', 571 ] 572 ], 573 'Filepath' => [ 574 'class' => \SpecialFilepath::class, 575 ], 576 'MediaStatistics' => [ 577 'class' => \SpecialMediaStatistics::class, 578 'services' => [ 579 'MimeAnalyzer', 580 'DBLoadBalancer', 581 'LinkBatchFactory', 582 ] 583 ], 584 'MIMEsearch' => [ 585 'class' => \SpecialMIMESearch::class, 586 'services' => [ 587 'DBLoadBalancer', 588 'LinkBatchFactory', 589 'LanguageConverterFactory', 590 ] 591 ], 592 'FileDuplicateSearch' => [ 593 'class' => \SpecialFileDuplicateSearch::class, 594 'services' => [ 595 'LinkBatchFactory', 596 'RepoGroup', 597 'SearchEngineFactory', 598 'LanguageConverterFactory', 599 ] 600 ], 601 'Upload' => [ 602 'class' => \SpecialUpload::class, 603 'services' => [ 604 'RepoGroup', 605 'UserOptionsLookup', 606 'NamespaceInfo', 607 ] 608 ], 609 'UploadStash' => [ 610 'class' => \SpecialUploadStash::class, 611 'services' => [ 612 'RepoGroup', 613 'HttpRequestFactory', 614 ] 615 ], 616 'ListDuplicatedFiles' => [ 617 'class' => \SpecialListDuplicatedFiles::class, 618 'services' => [ 619 'DBLoadBalancer', 620 'LinkBatchFactory', 621 ] 622 ], 623 624 // Data and tools 625 'ApiSandbox' => [ 626 'class' => \SpecialApiSandbox::class, 627 ], 628 'Statistics' => [ 629 'class' => \SpecialStatistics::class 630 ], 631 'Allmessages' => [ 632 'class' => \SpecialAllMessages::class, 633 'services' => [ 634 'LocalisationCache', 635 'DBLoadBalancer', 636 ] 637 ], 638 'Version' => [ 639 'class' => \SpecialVersion::class, 640 'services' => [ 641 'Parser', 642 ] 643 ], 644 'Lockdb' => [ 645 'class' => \SpecialLockdb::class, 646 ], 647 'Unlockdb' => [ 648 'class' => \SpecialUnlockdb::class, 649 ], 650 651 // Redirecting special pages 652 'LinkSearch' => [ 653 'class' => \SpecialLinkSearch::class, 654 'services' => [ 655 'DBLoadBalancer', 656 'LinkBatchFactory', 657 ] 658 ], 659 'Randompage' => [ 660 'class' => \SpecialRandomPage::class, 661 'services' => [ 662 'DBLoadBalancer', 663 'NamespaceInfo', 664 ] 665 ], 666 'RandomInCategory' => [ 667 'class' => \SpecialRandomInCategory::class, 668 'services' => [ 669 'DBLoadBalancer', 670 ] 671 ], 672 'Randomredirect' => [ 673 'class' => \SpecialRandomRedirect::class, 674 'services' => [ 675 'DBLoadBalancer', 676 'NamespaceInfo', 677 ] 678 ], 679 'Randomrootpage' => [ 680 'class' => \SpecialRandomRootPage::class, 681 'services' => [ 682 'DBLoadBalancer', 683 'NamespaceInfo', 684 ] 685 ], 686 'GoToInterwiki' => [ 687 'class' => \SpecialGoToInterwiki::class, 688 ], 689 690 // High use pages 691 'Mostlinkedcategories' => [ 692 'class' => \SpecialMostLinkedCategories::class, 693 'services' => [ 694 'DBLoadBalancer', 695 'LinkBatchFactory', 696 'LanguageConverterFactory', 697 ] 698 ], 699 'Mostimages' => [ 700 'class' => \MostimagesPage::class, 701 'services' => [ 702 'DBLoadBalancer', 703 'LanguageConverterFactory', 704 ] 705 ], 706 'Mostinterwikis' => [ 707 'class' => \SpecialMostInterwikis::class, 708 'services' => [ 709 'NamespaceInfo', 710 'DBLoadBalancer', 711 'LinkBatchFactory', 712 ] 713 ], 714 'Mostlinked' => [ 715 'class' => \SpecialMostLinked::class, 716 'services' => [ 717 'DBLoadBalancer', 718 'LinkBatchFactory', 719 ] 720 ], 721 'Mostlinkedtemplates' => [ 722 'class' => \SpecialMostLinkedTemplates::class, 723 'services' => [ 724 'DBLoadBalancer', 725 'LinkBatchFactory', 726 ] 727 ], 728 'Mostcategories' => [ 729 'class' => \SpecialMostCategories::class, 730 'services' => [ 731 'NamespaceInfo', 732 'DBLoadBalancer', 733 'LinkBatchFactory', 734 ] 735 ], 736 'Mostrevisions' => [ 737 'class' => \SpecialMostRevisions::class, 738 'services' => [ 739 // Same as for Fewestrevisions 740 'NamespaceInfo', 741 'DBLoadBalancer', 742 'LinkBatchFactory', 743 'LanguageConverterFactory', 744 ] 745 ], 746 747 // Page tools 748 'ComparePages' => [ 749 'class' => \SpecialComparePages::class, 750 'services' => [ 751 'RevisionLookup', 752 'ContentHandlerFactory', 753 ] 754 ], 755 'Export' => [ 756 'class' => \SpecialExport::class, 757 'services' => [ 758 'DBLoadBalancer', 759 ] 760 ], 761 'Import' => [ 762 'class' => \SpecialImport::class, 763 'services' => [ 764 'PermissionManager', 765 'WikiImporterFactory', 766 ] 767 ], 768 'Undelete' => [ 769 'class' => \SpecialUndelete::class, 770 'services' => [ 771 'PermissionManager', 772 'RevisionStore', 773 'RevisionRenderer', 774 'ContentHandlerFactory', 775 'ChangeTagDefStore', 776 'LinkBatchFactory', 777 'RepoGroup', 778 'DBLoadBalancer', 779 'UserOptionsLookup', 780 'WikiPageFactory', 781 'SearchEngineFactory', 782 ], 783 ], 784 'Whatlinkshere' => [ 785 'class' => \SpecialWhatLinksHere::class, 786 'services' => [ 787 'DBLoadBalancer', 788 'LinkBatchFactory', 789 'ContentHandlerFactory', 790 'SearchEngineFactory', 791 'NamespaceInfo', 792 ] 793 ], 794 'MergeHistory' => [ 795 'class' => \SpecialMergeHistory::class, 796 'services' => [ 797 'MergeHistoryFactory', 798 'LinkBatchFactory', 799 'DBLoadBalancer', 800 'RevisionStore', 801 ] 802 ], 803 'ExpandTemplates' => [ 804 'class' => \SpecialExpandTemplates::class, 805 'services' => [ 806 'Parser', 807 'UserOptionsLookup', 808 'Tidy', 809 ], 810 ], 811 'ChangeContentModel' => [ 812 'class' => \SpecialChangeContentModel::class, 813 'services' => [ 814 'ContentHandlerFactory', 815 'ContentModelChangeFactory', 816 'SpamChecker', 817 'RevisionLookup', 818 'WikiPageFactory', 819 'SearchEngineFactory', 820 ], 821 ], 822 823 // Other 824 'Booksources' => [ 825 'class' => \SpecialBookSources::class, 826 'services' => [ 827 'RevisionLookup', 828 ] 829 ], 830 831 // Unlisted / redirects 832 'ApiHelp' => [ 833 'class' => \SpecialApiHelp::class, 834 ], 835 'Blankpage' => [ 836 'class' => \SpecialBlankpage::class, 837 ], 838 'Diff' => [ 839 'class' => \SpecialDiff::class, 840 ], 841 'EditPage' => [ 842 'class' => \SpecialEditPage::class, 843 ], 844 'EditTags' => [ 845 'class' => \SpecialEditTags::class, 846 'services' => [ 847 'PermissionManager', 848 ], 849 ], 850 'Emailuser' => [ 851 'class' => \SpecialEmailUser::class, 852 'services' => [ 853 'UserNameUtils', 854 'UserNamePrefixSearch', 855 'UserOptionsLookup', 856 ] 857 ], 858 'Movepage' => [ 859 'class' => \MovePageForm::class, 860 'services' => [ 861 'MovePageFactory', 862 'PermissionManager', 863 'UserOptionsLookup', 864 'DBLoadBalancer', 865 'ContentHandlerFactory', 866 'NamespaceInfo', 867 'LinkBatchFactory', 868 'RepoGroup', 869 'WikiPageFactory', 870 'SearchEngineFactory', 871 'WatchlistManager', 872 ] 873 ], 874 'Mycontributions' => [ 875 'class' => \SpecialMycontributions::class, 876 ], 877 'MyLanguage' => [ 878 'class' => \SpecialMyLanguage::class, 879 'services' => [ 880 'LanguageNameUtils', 881 'WikiPageFactory', 882 ] 883 ], 884 'Mypage' => [ 885 'class' => \SpecialMypage::class, 886 ], 887 'Mytalk' => [ 888 'class' => \SpecialMytalk::class, 889 ], 890 'PageHistory' => [ 891 'class' => \SpecialPageHistory::class, 892 ], 893 'PageInfo' => [ 894 'class' => \SpecialPageInfo::class, 895 ], 896 'Purge' => [ 897 'class' => \SpecialPurge::class, 898 ], 899 'Myuploads' => [ 900 'class' => \SpecialMyuploads::class, 901 ], 902 'AllMyUploads' => [ 903 'class' => \SpecialAllMyUploads::class, 904 ], 905 'NewSection' => [ 906 'class' => \SpecialNewSection::class, 907 ], 908 'PermanentLink' => [ 909 'class' => \SpecialPermanentLink::class, 910 ], 911 'Redirect' => [ 912 'class' => \SpecialRedirect::class, 913 'services' => [ 914 'RepoGroup', 915 'UserFactory', 916 ] 917 ], 918 'Revisiondelete' => [ 919 'class' => \SpecialRevisionDelete::class, 920 'services' => [ 921 'PermissionManager', 922 'RepoGroup', 923 ], 924 ], 925 'RunJobs' => [ 926 'class' => \SpecialRunJobs::class, 927 'services' => [ 928 'JobRunner', 929 'ReadOnlyMode', 930 ] 931 ], 932 'Specialpages' => [ 933 'class' => \SpecialSpecialpages::class, 934 ], 935 'PageData' => [ 936 'class' => \SpecialPageData::class, 937 ], 938 ]; 939 940 /** @var array Special page name => class name */ 941 private $list; 942 943 /** @var array */ 944 private $aliases; 945 946 /** @var ServiceOptions */ 947 private $options; 948 949 /** @var Language */ 950 private $contLang; 951 952 /** @var ObjectFactory */ 953 private $objectFactory; 954 955 /** @var HookContainer */ 956 private $hookContainer; 957 958 /** @var HookRunner */ 959 private $hookRunner; 960 961 /** 962 * @internal For use by ServiceWiring 963 */ 964 public const CONSTRUCTOR_OPTIONS = [ 965 'DisableInternalSearch', 966 'EmailAuthentication', 967 'EnableEmail', 968 'EnableJavaScriptTest', 969 'EnableSpecialMute', 970 'PageLanguageUseDB', 971 'SpecialPages', 972 ]; 973 974 /** 975 * @var TitleFactory 976 */ 977 private $titleFactory; 978 979 /** 980 * @param ServiceOptions $options 981 * @param Language $contLang 982 * @param ObjectFactory $objectFactory 983 * @param TitleFactory $titleFactory 984 * @param HookContainer $hookContainer 985 */ 986 public function __construct( 987 ServiceOptions $options, 988 Language $contLang, 989 ObjectFactory $objectFactory, 990 TitleFactory $titleFactory, 991 HookContainer $hookContainer 992 ) { 993 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); 994 $this->options = $options; 995 $this->contLang = $contLang; 996 $this->objectFactory = $objectFactory; 997 $this->titleFactory = $titleFactory; 998 $this->hookContainer = $hookContainer; 999 $this->hookRunner = new HookRunner( $hookContainer ); 1000 } 1001 1002 /** 1003 * Returns a list of canonical special page names. 1004 * May be used to iterate over all registered special pages. 1005 * 1006 * @return string[] 1007 */ 1008 public function getNames(): array { 1009 return array_keys( $this->getPageList() ); 1010 } 1011 1012 /** 1013 * Get the special page list as an array 1014 * 1015 * @return array 1016 */ 1017 private function getPageList(): array { 1018 if ( !is_array( $this->list ) ) { 1019 $this->list = self::CORE_LIST; 1020 1021 if ( !$this->options->get( 'DisableInternalSearch' ) ) { 1022 $this->list['Search'] = [ 1023 'class' => \SpecialSearch::class, 1024 'services' => [ 1025 'SearchEngineConfig', 1026 'SearchEngineFactory', 1027 'NamespaceInfo', 1028 'ContentHandlerFactory', 1029 'InterwikiLookup', 1030 'ReadOnlyMode', 1031 'UserOptionsManager', 1032 'LanguageConverterFactory' 1033 ] 1034 ]; 1035 } 1036 1037 if ( $this->options->get( 'EmailAuthentication' ) ) { 1038 $this->list['Confirmemail'] = [ 1039 'class' => \SpecialConfirmEmail::class, 1040 'services' => [ 1041 'UserFactory', 1042 ] 1043 ]; 1044 $this->list['Invalidateemail'] = [ 1045 'class' => \SpecialEmailInvalidate::class, 1046 'services' => [ 1047 'UserFactory', 1048 ] 1049 ]; 1050 } 1051 1052 if ( $this->options->get( 'EnableEmail' ) ) { 1053 $this->list['ChangeEmail'] = [ 1054 'class' => \SpecialChangeEmail::class, 1055 'services' => [ 1056 'AuthManager', 1057 ], 1058 ]; 1059 } 1060 1061 if ( $this->options->get( 'EnableJavaScriptTest' ) ) { 1062 $this->list['JavaScriptTest'] = [ 1063 'class' => \SpecialJavaScriptTest::class 1064 ]; 1065 } 1066 1067 if ( $this->options->get( 'EnableSpecialMute' ) ) { 1068 $this->list['Mute'] = [ 1069 'class' => \SpecialMute::class, 1070 'services' => [ 1071 'CentralIdLookup', 1072 'UserOptionsManager', 1073 'UserIdentityLookup', 1074 ] 1075 ]; 1076 } 1077 1078 if ( $this->options->get( 'PageLanguageUseDB' ) ) { 1079 $this->list['PageLanguage'] = [ 1080 'class' => \SpecialPageLanguage::class, 1081 'services' => [ 1082 'ContentHandlerFactory', 1083 'LanguageNameUtils', 1084 'DBLoadBalancer', 1085 'SearchEngineFactory', 1086 ] 1087 ]; 1088 } 1089 1090 // Add extension special pages 1091 $this->list = array_merge( $this->list, $this->options->get( 'SpecialPages' ) ); 1092 1093 // This hook can be used to disable unwanted core special pages 1094 // or conditionally register special pages. 1095 $this->hookRunner->onSpecialPage_initList( $this->list ); 1096 } 1097 1098 return $this->list; 1099 } 1100 1101 /** 1102 * Initialise and return the list of special page aliases. Returns an array where 1103 * the key is an alias, and the value is the canonical name of the special page. 1104 * All registered special pages are guaranteed to map to themselves. 1105 * @return array 1106 */ 1107 private function getAliasList(): array { 1108 if ( $this->aliases === null ) { 1109 $aliases = $this->contLang->getSpecialPageAliases(); 1110 $pageList = $this->getPageList(); 1111 1112 $this->aliases = []; 1113 $keepAlias = []; 1114 1115 // Force every canonical name to be an alias for itself. 1116 foreach ( $pageList as $name => $stuff ) { 1117 $caseFoldedAlias = $this->contLang->caseFold( $name ); 1118 $this->aliases[$caseFoldedAlias] = $name; 1119 $keepAlias[$caseFoldedAlias] = 'canonical'; 1120 } 1121 1122 // Check for $aliases being an array since Language::getSpecialPageAliases can return null 1123 if ( is_array( $aliases ) ) { 1124 foreach ( $aliases as $realName => $aliasList ) { 1125 $aliasList = array_values( $aliasList ); 1126 foreach ( $aliasList as $i => $alias ) { 1127 $caseFoldedAlias = $this->contLang->caseFold( $alias ); 1128 1129 if ( isset( $this->aliases[$caseFoldedAlias] ) && 1130 $realName === $this->aliases[$caseFoldedAlias] 1131 ) { 1132 // Ignore same-realName conflicts 1133 continue; 1134 } 1135 1136 if ( !isset( $keepAlias[$caseFoldedAlias] ) ) { 1137 $this->aliases[$caseFoldedAlias] = $realName; 1138 if ( !$i ) { 1139 $keepAlias[$caseFoldedAlias] = 'first'; 1140 } 1141 } elseif ( !$i ) { 1142 wfWarn( "First alias '$alias' for $realName conflicts with " . 1143 "{$keepAlias[$caseFoldedAlias]} alias for " . 1144 $this->aliases[$caseFoldedAlias] 1145 ); 1146 } 1147 } 1148 } 1149 } 1150 } 1151 1152 return $this->aliases; 1153 } 1154 1155 /** 1156 * Given a special page name with a possible subpage, return an array 1157 * where the first element is the special page name and the second is the 1158 * subpage. 1159 * 1160 * @param string $alias 1161 * @return array [ String, String|null ], or [ null, null ] if the page is invalid 1162 */ 1163 public function resolveAlias( $alias ) { 1164 $bits = explode( '/', $alias, 2 ); 1165 1166 $caseFoldedAlias = $this->contLang->caseFold( $bits[0] ); 1167 $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias ); 1168 $aliases = $this->getAliasList(); 1169 if ( !isset( $aliases[$caseFoldedAlias] ) ) { 1170 return [ null, null ]; 1171 } 1172 $name = $aliases[$caseFoldedAlias]; 1173 $par = $bits[1] ?? null; // T4087 1174 1175 return [ $name, $par ]; 1176 } 1177 1178 /** 1179 * Check if a given name exist as a special page or as a special page alias 1180 * 1181 * @param string $name Name of a special page 1182 * @return bool True if a special page exists with this name 1183 */ 1184 public function exists( $name ) { 1185 list( $title, /*...*/ ) = $this->resolveAlias( $name ); 1186 1187 $specialPageList = $this->getPageList(); 1188 return isset( $specialPageList[$title] ); 1189 } 1190 1191 /** 1192 * Find the object with a given name and return it (or NULL) 1193 * 1194 * @param string $name Special page name, may be localised and/or an alias 1195 * @return SpecialPage|null SpecialPage object or null if the page doesn't exist 1196 */ 1197 public function getPage( $name ) { 1198 list( $realName, /*...*/ ) = $this->resolveAlias( $name ); 1199 1200 $specialPageList = $this->getPageList(); 1201 1202 if ( isset( $specialPageList[$realName] ) ) { 1203 $rec = $specialPageList[$realName]; 1204 1205 if ( $rec instanceof SpecialPage ) { 1206 wfDeprecatedMsg( 1207 "A SpecialPage instance for $realName was found in " . 1208 '$wgSpecialPages or came from a SpecialPage_initList hook handler, ' . 1209 'this was deprecated in MediaWiki 1.34', 1210 '1.34' 1211 ); 1212 1213 $page = $rec; // XXX: we should deep clone here 1214 } elseif ( is_array( $rec ) || is_string( $rec ) || is_callable( $rec ) ) { 1215 $page = $this->objectFactory->createObject( 1216 $rec, 1217 [ 1218 'allowClassName' => true, 1219 'allowCallable' => true 1220 ] 1221 ); 1222 } else { 1223 $page = null; 1224 } 1225 1226 if ( $page instanceof SpecialPage ) { 1227 $page->setHookContainer( $this->hookContainer ); 1228 $page->setContentLanguage( $this->contLang ); 1229 $page->setSpecialPageFactory( $this ); 1230 return $page; 1231 } 1232 1233 // It's not a classname, nor a callback, nor a legacy constructor array, 1234 // nor a special page object. Give up. 1235 wfLogWarning( "Cannot instantiate special page $realName: bad spec!" ); 1236 } 1237 1238 return null; 1239 } 1240 1241 /** 1242 * Get listed special pages available to the current user. 1243 * 1244 * This includes both unrestricted pages, and restricted pages 1245 * that the current user has the required permissions for. 1246 * 1247 * @param User $user User object to check permissions provided 1248 * @return SpecialPage[] 1249 */ 1250 public function getUsablePages( User $user ): array { 1251 $pages = []; 1252 foreach ( $this->getPageList() as $name => $rec ) { 1253 $page = $this->getPage( $name ); 1254 if ( $page ) { // not null 1255 $page->setContext( RequestContext::getMain() ); 1256 if ( $page->isListed() 1257 && ( !$page->isRestricted() || $page->userCanExecute( $user ) ) 1258 ) { 1259 $pages[$name] = $page; 1260 } 1261 } 1262 } 1263 1264 return $pages; 1265 } 1266 1267 /** 1268 * Get listed special pages available to everyone by default. 1269 * 1270 * @return SpecialPage[] 1271 */ 1272 public function getRegularPages(): array { 1273 $pages = []; 1274 foreach ( $this->getPageList() as $name => $rec ) { 1275 $page = $this->getPage( $name ); 1276 if ( $page && $page->isListed() && !$page->isRestricted() ) { 1277 $pages[$name] = $page; 1278 } 1279 } 1280 1281 return $pages; 1282 } 1283 1284 /** 1285 * Execute a special page path. 1286 * The path may contain parameters, e.g. Special:Name/Params 1287 * Extracts the special page name and call the execute method, passing the parameters 1288 * 1289 * Returns a title object if the page is redirected, false if there was no such special 1290 * page, and true if it was successful. 1291 * 1292 * @param PageReference|string $path 1293 * @param IContextSource $context 1294 * @param bool $including Bool output is being captured for use in {{special:whatever}} 1295 * @param LinkRenderer|null $linkRenderer (since 1.28) 1296 * 1297 * @return bool|Title 1298 */ 1299 public function executePath( $path, IContextSource $context, $including = false, 1300 LinkRenderer $linkRenderer = null 1301 ) { 1302 if ( $path instanceof PageReference ) { 1303 $path = $path->getDBkey(); 1304 } 1305 1306 $bits = explode( '/', $path, 2 ); 1307 $name = $bits[0]; 1308 $par = $bits[1] ?? null; // T4087 1309 1310 $page = $this->getPage( $name ); 1311 if ( !$page ) { 1312 $context->getOutput()->setArticleRelated( false ); 1313 $context->getOutput()->setRobotPolicy( 'noindex,nofollow' ); 1314 1315 global $wgSend404Code; 1316 if ( $wgSend404Code ) { 1317 $context->getOutput()->setStatusCode( 404 ); 1318 } 1319 1320 $context->getOutput()->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' ); 1321 1322 return false; 1323 } 1324 1325 if ( !$including ) { 1326 // Narrow DB query expectations for this HTTP request 1327 $trxLimits = $context->getConfig()->get( 'TrxProfilerLimits' ); 1328 $trxProfiler = Profiler::instance()->getTransactionProfiler(); 1329 if ( $context->getRequest()->wasPosted() && !$page->doesWrites() ) { 1330 $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ ); 1331 $context->getRequest()->markAsSafeRequest(); 1332 } 1333 } 1334 1335 // Page exists, set the context 1336 $page->setContext( $context ); 1337 1338 if ( !$including ) { 1339 // Redirect to canonical alias for GET commands 1340 // Not for POST, we'd lose the post data, so it's best to just distribute 1341 // the request. Such POST requests are possible for old extensions that 1342 // generate self-links without being aware that their default name has 1343 // changed. 1344 if ( $name != $page->getLocalName() && !$context->getRequest()->wasPosted() ) { 1345 $query = $context->getRequest()->getQueryValues(); 1346 unset( $query['title'] ); 1347 $title = $page->getPageTitle( $par ); 1348 $url = $title->getFullURL( $query ); 1349 $context->getOutput()->redirect( $url ); 1350 1351 return $title; 1352 } 1353 1354 // @phan-suppress-next-line PhanUndeclaredMethod 1355 $context->setTitle( $page->getPageTitle( $par ) ); 1356 } elseif ( !$page->isIncludable() ) { 1357 return false; 1358 } 1359 1360 $page->including( $including ); 1361 if ( $linkRenderer ) { 1362 $page->setLinkRenderer( $linkRenderer ); 1363 } 1364 1365 // Execute special page 1366 $page->run( $par ); 1367 1368 return true; 1369 } 1370 1371 /** 1372 * Just like executePath() but will override global variables and execute 1373 * the page in "inclusion" mode. Returns true if the execution was 1374 * successful or false if there was no such special page, or a title object 1375 * if it was a redirect. 1376 * 1377 * Also saves the current $wgTitle, $wgOut, $wgRequest, $wgUser and $wgLang 1378 * variables so that the special page will get the context it'd expect on a 1379 * normal request, and then restores them to their previous values after. 1380 * 1381 * @param PageReference $page 1382 * @param IContextSource $context 1383 * @param LinkRenderer|null $linkRenderer (since 1.28) 1384 * @return string HTML fragment 1385 */ 1386 public function capturePath( 1387 PageReference $page, IContextSource $context, LinkRenderer $linkRenderer = null 1388 ) { 1389 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser 1390 global $wgTitle, $wgOut, $wgRequest, $wgUser, $wgLang; 1391 $main = RequestContext::getMain(); 1392 1393 // Save current globals and main context 1394 $glob = [ 1395 'title' => $wgTitle, 1396 'output' => $wgOut, 1397 'request' => $wgRequest, 1398 'user' => $wgUser, 1399 'language' => $wgLang, 1400 ]; 1401 $ctx = [ 1402 'title' => $main->getTitle(), 1403 'output' => $main->getOutput(), 1404 'request' => $main->getRequest(), 1405 'user' => $main->getUser(), 1406 'language' => $main->getLanguage(), 1407 ]; 1408 if ( $main->canUseWikiPage() ) { 1409 $ctx['wikipage'] = $main->getWikiPage(); 1410 } 1411 1412 // just needed for $wgTitle and RequestContext::setTitle 1413 $title = $this->titleFactory->castFromPageReference( $page ); 1414 1415 // Override 1416 $wgTitle = $title; 1417 $wgOut = $context->getOutput(); 1418 $wgRequest = $context->getRequest(); 1419 $wgUser = $context->getUser(); 1420 $wgLang = $context->getLanguage(); 1421 $main->setTitle( $title ); 1422 $main->setOutput( $context->getOutput() ); 1423 $main->setRequest( $context->getRequest() ); 1424 $main->setUser( $context->getUser() ); 1425 $main->setLanguage( $context->getLanguage() ); 1426 1427 // The useful part 1428 $ret = $this->executePath( $page, $context, true, $linkRenderer ); 1429 1430 // Restore old globals and context 1431 $wgTitle = $glob['title']; 1432 $wgOut = $glob['output']; 1433 $wgRequest = $glob['request']; 1434 $wgUser = $glob['user']; 1435 $wgLang = $glob['language']; 1436 $main->setTitle( $ctx['title'] ); 1437 $main->setOutput( $ctx['output'] ); 1438 $main->setRequest( $ctx['request'] ); 1439 $main->setUser( $ctx['user'] ); 1440 $main->setLanguage( $ctx['language'] ); 1441 if ( isset( $ctx['wikipage'] ) ) { 1442 $main->setWikiPage( $ctx['wikipage'] ); 1443 } 1444 1445 return $ret; 1446 } 1447 1448 /** 1449 * Get the local name for a specified canonical name 1450 * 1451 * @param string $name 1452 * @param string|bool $subpage 1453 * @return string 1454 */ 1455 public function getLocalNameFor( $name, $subpage = false ) { 1456 $aliases = $this->contLang->getSpecialPageAliases(); 1457 $aliasList = $this->getAliasList(); 1458 1459 // Find the first alias that maps back to $name 1460 if ( isset( $aliases[$name] ) ) { 1461 $found = false; 1462 foreach ( $aliases[$name] as $alias ) { 1463 $caseFoldedAlias = $this->contLang->caseFold( $alias ); 1464 $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias ); 1465 if ( isset( $aliasList[$caseFoldedAlias] ) && 1466 $aliasList[$caseFoldedAlias] === $name 1467 ) { 1468 $name = $alias; 1469 $found = true; 1470 break; 1471 } 1472 } 1473 if ( !$found ) { 1474 wfWarn( "Did not find a usable alias for special page '$name'. " . 1475 "It seems all defined aliases conflict?" ); 1476 } 1477 } else { 1478 // Check if someone misspelled the correct casing 1479 if ( is_array( $aliases ) ) { 1480 foreach ( $aliases as $n => $values ) { 1481 if ( strcasecmp( $name, $n ) === 0 ) { 1482 wfWarn( "Found alias defined for $n when searching for " . 1483 "special page aliases for $name. Case mismatch?" ); 1484 return $this->getLocalNameFor( $n, $subpage ); 1485 } 1486 } 1487 } 1488 1489 wfWarn( "Did not find alias for special page '$name'. " . 1490 "Perhaps no aliases are defined for it?" ); 1491 } 1492 1493 if ( $subpage !== false && $subpage !== null ) { 1494 // Make sure it's in dbkey form 1495 $subpage = str_replace( ' ', '_', $subpage ); 1496 $name = "$name/$subpage"; 1497 } 1498 1499 return $this->contLang->ucfirst( $name ); 1500 } 1501 1502 /** 1503 * Get a title for a given alias 1504 * 1505 * @param string $alias 1506 * @return Title|null Title or null if there is no such alias 1507 */ 1508 public function getTitleForAlias( $alias ) { 1509 list( $name, $subpage ) = $this->resolveAlias( $alias ); 1510 if ( $name != null ) { 1511 return SpecialPage::getTitleFor( $name, $subpage ); 1512 } 1513 1514 return null; 1515 } 1516} 1517 1518/** @deprecated since 1.35, use MediaWiki\\SpecialPage\\SpecialPageFactory */ 1519class_alias( SpecialPageFactory::class, 'MediaWiki\\Special\\SpecialPageFactory' ); 1520