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