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 Profiler; 34use RequestContext; 35use SpecialPage; 36use Title; 37use User; 38use Wikimedia\ObjectFactory; 39 40/** 41 * Factory for handling the special page list and generating SpecialPage objects. 42 * 43 * To add a special page in an extension, add to $wgSpecialPages either 44 * an object instance or an array containing the name and constructor 45 * parameters. The latter is preferred for performance reasons. 46 * 47 * The object instantiated must be either an instance of SpecialPage or a 48 * sub-class thereof. It must have an execute() method, which sends the HTML 49 * for the special page to $wgOut. The parent class has an execute() method 50 * which distributes the call to the historical global functions. Additionally, 51 * execute() also checks if the user has the necessary access privileges 52 * and bails out if not. 53 * 54 * To add a core special page, use the similar static list in 55 * SpecialPageFactory::$list. To remove a core static special page at runtime, use 56 * a SpecialPage_initList hook. 57 * 58 * @note There are two classes called SpecialPageFactory. You should use this first one, in 59 * namespace MediaWiki\Special, which is a service. \SpecialPageFactory is a deprecated collection 60 * of static methods that forwards to the global service. 61 * 62 * @ingroup SpecialPage 63 * @since 1.17 64 */ 65class SpecialPageFactory { 66 /** 67 * List of special page names to the subclass of SpecialPage which handles them. 68 */ 69 private const CORE_LIST = [ 70 // Maintenance Reports 71 'BrokenRedirects' => \SpecialBrokenRedirects::class, 72 'Deadendpages' => \SpecialDeadendPages::class, 73 'DoubleRedirects' => \SpecialDoubleRedirects::class, 74 'Longpages' => \SpecialLongPages::class, 75 'Ancientpages' => \SpecialAncientPages::class, 76 'Lonelypages' => \SpecialLonelyPages::class, 77 'Fewestrevisions' => \SpecialFewestRevisions::class, 78 'Withoutinterwiki' => \SpecialWithoutInterwiki::class, 79 'Protectedpages' => \SpecialProtectedpages::class, 80 'Protectedtitles' => \SpecialProtectedtitles::class, 81 'Shortpages' => \SpecialShortPages::class, 82 'Uncategorizedcategories' => \SpecialUncategorizedCategories::class, 83 'Uncategorizedimages' => \SpecialUncategorizedImages::class, 84 'Uncategorizedpages' => \SpecialUncategorizedPages::class, 85 'Uncategorizedtemplates' => \SpecialUncategorizedTemplates::class, 86 'Unusedcategories' => \SpecialUnusedCategories::class, 87 'Unusedimages' => \SpecialUnusedImages::class, 88 'Unusedtemplates' => \SpecialUnusedTemplates::class, 89 'Unwatchedpages' => \SpecialUnwatchedPages::class, 90 'Wantedcategories' => \SpecialWantedCategories::class, 91 'Wantedfiles' => \WantedFilesPage::class, 92 'Wantedpages' => \WantedPagesPage::class, 93 'Wantedtemplates' => \SpecialWantedTemplates::class, 94 95 // List of pages 96 'Allpages' => \SpecialAllPages::class, 97 'Prefixindex' => \SpecialPrefixindex::class, 98 'Categories' => \SpecialCategories::class, 99 'Listredirects' => \SpecialListRedirects::class, 100 'PagesWithProp' => \SpecialPagesWithProp::class, 101 'TrackingCategories' => \SpecialTrackingCategories::class, 102 103 // Authentication 104 'Userlogin' => \SpecialUserLogin::class, 105 'Userlogout' => \SpecialUserLogout::class, 106 'CreateAccount' => \SpecialCreateAccount::class, 107 'LinkAccounts' => \SpecialLinkAccounts::class, 108 'UnlinkAccounts' => \SpecialUnlinkAccounts::class, 109 'ChangeCredentials' => \SpecialChangeCredentials::class, 110 'RemoveCredentials' => \SpecialRemoveCredentials::class, 111 112 // Users and rights 113 'Activeusers' => \SpecialActiveUsers::class, 114 'Block' => [ 115 'class' => \SpecialBlock::class, 116 'services' => [ 117 'PermissionManager' 118 ] 119 ], 120 'Unblock' => \SpecialUnblock::class, 121 'BlockList' => \SpecialBlockList::class, 122 'AutoblockList' => \SpecialAutoblockList::class, 123 'ChangePassword' => \SpecialChangePassword::class, 124 'BotPasswords' => \SpecialBotPasswords::class, 125 'PasswordReset' => \SpecialPasswordReset::class, 126 'DeletedContributions' => \SpecialDeletedContributions::class, 127 'Preferences' => \SpecialPreferences::class, 128 'ResetTokens' => \SpecialResetTokens::class, 129 'Contributions' => \SpecialContributions::class, 130 'Listgrouprights' => \SpecialListGroupRights::class, 131 'Listgrants' => \SpecialListGrants::class, 132 'Listusers' => \SpecialListUsers::class, 133 'Listadmins' => \SpecialListAdmins::class, 134 'Listbots' => \SpecialListBots::class, 135 'Userrights' => \UserrightsPage::class, 136 'EditWatchlist' => [ 137 'class' => \SpecialEditWatchlist::class, 138 'services' => [ 139 'WatchedItemStore' 140 ] 141 ], 142 'PasswordPolicies' => \SpecialPasswordPolicies::class, 143 144 // Recent changes and logs 145 'Newimages' => \SpecialNewFiles::class, 146 'Log' => \SpecialLog::class, 147 'Watchlist' => \SpecialWatchlist::class, 148 'Newpages' => \SpecialNewpages::class, 149 'Recentchanges' => \SpecialRecentChanges::class, 150 'Recentchangeslinked' => \SpecialRecentChangesLinked::class, 151 'Tags' => \SpecialTags::class, 152 153 // Media reports and uploads 154 'Listfiles' => \SpecialListFiles::class, 155 'Filepath' => \SpecialFilepath::class, 156 'MediaStatistics' => \SpecialMediaStatistics::class, 157 'MIMEsearch' => \SpecialMIMESearch::class, 158 'FileDuplicateSearch' => \SpecialFileDuplicateSearch::class, 159 'Upload' => \SpecialUpload::class, 160 'UploadStash' => \SpecialUploadStash::class, 161 'ListDuplicatedFiles' => \SpecialListDuplicatedFiles::class, 162 163 // Data and tools 164 'ApiSandbox' => \SpecialApiSandbox::class, 165 'Statistics' => \SpecialStatistics::class, 166 'Allmessages' => \SpecialAllMessages::class, 167 'Version' => \SpecialVersion::class, 168 'Lockdb' => \SpecialLockdb::class, 169 'Unlockdb' => \SpecialUnlockdb::class, 170 171 // Redirecting special pages 172 'LinkSearch' => \SpecialLinkSearch::class, 173 'Randompage' => \RandomPage::class, 174 'RandomInCategory' => \SpecialRandomInCategory::class, 175 'Randomredirect' => \SpecialRandomredirect::class, 176 'Randomrootpage' => \SpecialRandomrootpage::class, 177 'GoToInterwiki' => \SpecialGoToInterwiki::class, 178 179 // High use pages 180 'Mostlinkedcategories' => \SpecialMostLinkedCategories::class, 181 'Mostimages' => \MostimagesPage::class, 182 'Mostinterwikis' => \SpecialMostInterwikis::class, 183 'Mostlinked' => \SpecialMostLinked::class, 184 'Mostlinkedtemplates' => \SpecialMostLinkedTemplates::class, 185 'Mostcategories' => \SpecialMostCategories::class, 186 'Mostrevisions' => \SpecialMostRevisions::class, 187 188 // Page tools 189 'ComparePages' => \SpecialComparePages::class, 190 'Export' => \SpecialExport::class, 191 'Import' => \SpecialImport::class, 192 'Undelete' => \SpecialUndelete::class, 193 'Whatlinkshere' => \SpecialWhatLinksHere::class, 194 'MergeHistory' => \SpecialMergeHistory::class, 195 'ExpandTemplates' => \SpecialExpandTemplates::class, 196 'ChangeContentModel' => [ 197 'class' => \SpecialChangeContentModel::class, 198 'services' => [ 199 'ContentHandlerFactory', 200 ], 201 ], 202 203 // Other 204 'Booksources' => \SpecialBookSources::class, 205 206 // Unlisted / redirects 207 'ApiHelp' => \SpecialApiHelp::class, 208 'Blankpage' => \SpecialBlankpage::class, 209 'Diff' => \SpecialDiff::class, 210 'EditPage' => \SpecialEditPage::class, 211 'EditTags' => [ 212 'class' => \SpecialEditTags::class, 213 'services' => [ 214 'PermissionManager', 215 ], 216 ], 217 'Emailuser' => \SpecialEmailUser::class, 218 'Movepage' => \MovePageForm::class, 219 'Mycontributions' => \SpecialMycontributions::class, 220 'MyLanguage' => \SpecialMyLanguage::class, 221 'Mypage' => \SpecialMypage::class, 222 'Mytalk' => \SpecialMytalk::class, 223 'PageHistory' => \SpecialPageHistory::class, 224 'PageInfo' => \SpecialPageInfo::class, 225 'Purge' => \SpecialPurge::class, 226 'Myuploads' => \SpecialMyuploads::class, 227 'AllMyUploads' => \SpecialAllMyUploads::class, 228 'NewSection' => \SpecialNewSection::class, 229 'PermanentLink' => \SpecialPermanentLink::class, 230 'Redirect' => \SpecialRedirect::class, 231 'Revisiondelete' => [ 232 'class' => \SpecialRevisionDelete::class, 233 'services' => [ 234 'PermissionManager', 235 'RepoGroup', 236 ], 237 ], 238 'RunJobs' => \SpecialRunJobs::class, 239 'Specialpages' => \SpecialSpecialpages::class, 240 'PageData' => \SpecialPageData::class, 241 ]; 242 243 /** @var array Special page name => class name */ 244 private $list; 245 246 /** @var array */ 247 private $aliases; 248 249 /** @var ServiceOptions */ 250 private $options; 251 252 /** @var Language */ 253 private $contLang; 254 255 /** @var ObjectFactory */ 256 private $objectFactory; 257 258 /** @var HookContainer */ 259 private $hookContainer; 260 261 /** @var HookRunner */ 262 private $hookRunner; 263 264 /** 265 * @var array 266 * @since 1.35 267 */ 268 public const CONSTRUCTOR_OPTIONS = [ 269 'DisableInternalSearch', 270 'EmailAuthentication', 271 'EnableEmail', 272 'EnableJavaScriptTest', 273 'EnableSpecialMute', 274 'PageLanguageUseDB', 275 'SpecialPages', 276 ]; 277 278 /** 279 * @param ServiceOptions $options 280 * @param Language $contLang 281 * @param ObjectFactory $objectFactory 282 * @param HookContainer $hookContainer 283 */ 284 public function __construct( 285 ServiceOptions $options, 286 Language $contLang, 287 ObjectFactory $objectFactory, 288 HookContainer $hookContainer 289 ) { 290 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); 291 $this->options = $options; 292 $this->contLang = $contLang; 293 $this->objectFactory = $objectFactory; 294 $this->hookContainer = $hookContainer; 295 $this->hookRunner = new HookRunner( $hookContainer ); 296 } 297 298 /** 299 * Returns a list of canonical special page names. 300 * May be used to iterate over all registered special pages. 301 * 302 * @return string[] 303 */ 304 public function getNames() : array { 305 return array_keys( $this->getPageList() ); 306 } 307 308 /** 309 * Get the special page list as an array 310 * 311 * @return array 312 */ 313 private function getPageList() : array { 314 if ( !is_array( $this->list ) ) { 315 $this->list = self::CORE_LIST; 316 317 if ( !$this->options->get( 'DisableInternalSearch' ) ) { 318 $this->list['Search'] = \SpecialSearch::class; 319 } 320 321 if ( $this->options->get( 'EmailAuthentication' ) ) { 322 $this->list['Confirmemail'] = \SpecialConfirmEmail::class; 323 $this->list['Invalidateemail'] = \SpecialEmailInvalidate::class; 324 } 325 326 if ( $this->options->get( 'EnableEmail' ) ) { 327 $this->list['ChangeEmail'] = \SpecialChangeEmail::class; 328 } 329 330 if ( $this->options->get( 'EnableJavaScriptTest' ) ) { 331 $this->list['JavaScriptTest'] = \SpecialJavaScriptTest::class; 332 } 333 334 if ( $this->options->get( 'EnableSpecialMute' ) ) { 335 $this->list['Mute'] = \SpecialMute::class; 336 } 337 338 if ( $this->options->get( 'PageLanguageUseDB' ) ) { 339 $this->list['PageLanguage'] = \SpecialPageLanguage::class; 340 } 341 342 // Add extension special pages 343 $this->list = array_merge( $this->list, $this->options->get( 'SpecialPages' ) ); 344 345 // This hook can be used to disable unwanted core special pages 346 // or conditionally register special pages. 347 $this->hookRunner->onSpecialPage_initList( $this->list ); 348 } 349 350 return $this->list; 351 } 352 353 /** 354 * Initialise and return the list of special page aliases. Returns an array where 355 * the key is an alias, and the value is the canonical name of the special page. 356 * All registered special pages are guaranteed to map to themselves. 357 * @return array 358 */ 359 private function getAliasList() : array { 360 if ( $this->aliases === null ) { 361 $aliases = $this->contLang->getSpecialPageAliases(); 362 $pageList = $this->getPageList(); 363 364 $this->aliases = []; 365 $keepAlias = []; 366 367 // Force every canonical name to be an alias for itself. 368 foreach ( $pageList as $name => $stuff ) { 369 $caseFoldedAlias = $this->contLang->caseFold( $name ); 370 $this->aliases[$caseFoldedAlias] = $name; 371 $keepAlias[$caseFoldedAlias] = 'canonical'; 372 } 373 374 // Check for $aliases being an array since Language::getSpecialPageAliases can return null 375 if ( is_array( $aliases ) ) { 376 foreach ( $aliases as $realName => $aliasList ) { 377 $aliasList = array_values( $aliasList ); 378 foreach ( $aliasList as $i => $alias ) { 379 $caseFoldedAlias = $this->contLang->caseFold( $alias ); 380 381 if ( isset( $this->aliases[$caseFoldedAlias] ) && 382 $realName === $this->aliases[$caseFoldedAlias] 383 ) { 384 // Ignore same-realName conflicts 385 continue; 386 } 387 388 if ( !isset( $keepAlias[$caseFoldedAlias] ) ) { 389 $this->aliases[$caseFoldedAlias] = $realName; 390 if ( !$i ) { 391 $keepAlias[$caseFoldedAlias] = 'first'; 392 } 393 } elseif ( !$i ) { 394 wfWarn( "First alias '$alias' for $realName conflicts with " . 395 "{$keepAlias[$caseFoldedAlias]} alias for " . 396 $this->aliases[$caseFoldedAlias] 397 ); 398 } 399 } 400 } 401 } 402 } 403 404 return $this->aliases; 405 } 406 407 /** 408 * Given a special page name with a possible subpage, return an array 409 * where the first element is the special page name and the second is the 410 * subpage. 411 * 412 * @param string $alias 413 * @return array [ String, String|null ], or [ null, null ] if the page is invalid 414 */ 415 public function resolveAlias( $alias ) { 416 $bits = explode( '/', $alias, 2 ); 417 418 $caseFoldedAlias = $this->contLang->caseFold( $bits[0] ); 419 $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias ); 420 $aliases = $this->getAliasList(); 421 if ( !isset( $aliases[$caseFoldedAlias] ) ) { 422 return [ null, null ]; 423 } 424 $name = $aliases[$caseFoldedAlias]; 425 $par = $bits[1] ?? null; // T4087 426 427 return [ $name, $par ]; 428 } 429 430 /** 431 * Check if a given name exist as a special page or as a special page alias 432 * 433 * @param string $name Name of a special page 434 * @return bool True if a special page exists with this name 435 */ 436 public function exists( $name ) { 437 list( $title, /*...*/ ) = $this->resolveAlias( $name ); 438 439 $specialPageList = $this->getPageList(); 440 return isset( $specialPageList[$title] ); 441 } 442 443 /** 444 * Find the object with a given name and return it (or NULL) 445 * 446 * @param string $name Special page name, may be localised and/or an alias 447 * @return SpecialPage|null SpecialPage object or null if the page doesn't exist 448 */ 449 public function getPage( $name ) { 450 list( $realName, /*...*/ ) = $this->resolveAlias( $name ); 451 452 $specialPageList = $this->getPageList(); 453 454 if ( isset( $specialPageList[$realName] ) ) { 455 $rec = $specialPageList[$realName]; 456 457 if ( $rec instanceof SpecialPage ) { 458 wfDeprecatedMsg( 459 "A SpecialPage instance for $realName was found in " . 460 '$wgSpecialPages or came from a SpecialPage_initList hook handler, ' . 461 'this was deprecated in MediaWiki 1.34', 462 '1.34' 463 ); 464 465 $page = $rec; // XXX: we should deep clone here 466 } elseif ( is_array( $rec ) || is_string( $rec ) || is_callable( $rec ) ) { 467 $page = $this->objectFactory->createObject( 468 $rec, 469 [ 470 'allowClassName' => true, 471 'allowCallable' => true 472 ] 473 ); 474 } else { 475 $page = null; 476 } 477 478 if ( $page instanceof SpecialPage ) { 479 $page->setHookContainer( $this->hookContainer ); 480 return $page; 481 } 482 483 // It's not a classname, nor a callback, nor a legacy constructor array, 484 // nor a special page object. Give up. 485 wfLogWarning( "Cannot instantiate special page $realName: bad spec!" ); 486 } 487 488 return null; 489 } 490 491 /** 492 * Return categorised listable special pages which are available 493 * for the current user, and everyone. 494 * 495 * @param User $user User object to check permissions 496 * provided 497 * @return array ( string => Specialpage ) 498 */ 499 public function getUsablePages( User $user ) : array { 500 $pages = []; 501 foreach ( $this->getPageList() as $name => $rec ) { 502 $page = $this->getPage( $name ); 503 if ( $page ) { // not null 504 $page->setContext( RequestContext::getMain() ); 505 if ( $page->isListed() 506 && ( !$page->isRestricted() || $page->userCanExecute( $user ) ) 507 ) { 508 $pages[$name] = $page; 509 } 510 } 511 } 512 513 return $pages; 514 } 515 516 /** 517 * Return categorised listable special pages for all users 518 * 519 * @return array ( string => Specialpage ) 520 */ 521 public function getRegularPages() : array { 522 $pages = []; 523 foreach ( $this->getPageList() as $name => $rec ) { 524 $page = $this->getPage( $name ); 525 if ( $page && $page->isListed() && !$page->isRestricted() ) { 526 $pages[$name] = $page; 527 } 528 } 529 530 return $pages; 531 } 532 533 /** 534 * Return categorised listable special pages which are available 535 * for the current user, but not for everyone 536 * 537 * @param User $user User object to use 538 * @return array ( string => Specialpage ) 539 */ 540 public function getRestrictedPages( User $user ) : array { 541 $pages = []; 542 foreach ( $this->getPageList() as $name => $rec ) { 543 $page = $this->getPage( $name ); 544 if ( $page 545 && $page->isListed() 546 && $page->isRestricted() 547 && $page->userCanExecute( $user ) 548 ) { 549 $pages[$name] = $page; 550 } 551 } 552 553 return $pages; 554 } 555 556 /** 557 * Execute a special page path. 558 * The path may contain parameters, e.g. Special:Name/Params 559 * Extracts the special page name and call the execute method, passing the parameters 560 * 561 * Returns a title object if the page is redirected, false if there was no such special 562 * page, and true if it was successful. 563 * 564 * @param Title &$title 565 * @param IContextSource &$context 566 * @param bool $including Bool output is being captured for use in {{special:whatever}} 567 * @param LinkRenderer|null $linkRenderer (since 1.28) 568 * 569 * @return bool|Title 570 */ 571 public function executePath( Title &$title, IContextSource &$context, $including = false, 572 LinkRenderer $linkRenderer = null 573 ) { 574 // @todo FIXME: Redirects broken due to this call 575 $bits = explode( '/', $title->getDBkey(), 2 ); 576 $name = $bits[0]; 577 $par = $bits[1] ?? null; // T4087 578 579 $page = $this->getPage( $name ); 580 if ( !$page ) { 581 $context->getOutput()->setArticleRelated( false ); 582 $context->getOutput()->setRobotPolicy( 'noindex,nofollow' ); 583 584 global $wgSend404Code; 585 if ( $wgSend404Code ) { 586 $context->getOutput()->setStatusCode( 404 ); 587 } 588 589 $context->getOutput()->showErrorPage( 'nosuchspecialpage', 'nospecialpagetext' ); 590 591 return false; 592 } 593 594 if ( !$including ) { 595 // Narrow DB query expectations for this HTTP request 596 $trxLimits = $context->getConfig()->get( 'TrxProfilerLimits' ); 597 $trxProfiler = Profiler::instance()->getTransactionProfiler(); 598 if ( $context->getRequest()->wasPosted() && !$page->doesWrites() ) { 599 $trxProfiler->setExpectations( $trxLimits['POST-nonwrite'], __METHOD__ ); 600 $context->getRequest()->markAsSafeRequest(); 601 } 602 } 603 604 // Page exists, set the context 605 $page->setContext( $context ); 606 607 if ( !$including ) { 608 // Redirect to canonical alias for GET commands 609 // Not for POST, we'd lose the post data, so it's best to just distribute 610 // the request. Such POST requests are possible for old extensions that 611 // generate self-links without being aware that their default name has 612 // changed. 613 if ( $name != $page->getLocalName() && !$context->getRequest()->wasPosted() ) { 614 $query = $context->getRequest()->getQueryValues(); 615 unset( $query['title'] ); 616 $title = $page->getPageTitle( $par ); 617 $url = $title->getFullURL( $query ); 618 $context->getOutput()->redirect( $url ); 619 620 return $title; 621 } 622 623 // @phan-suppress-next-line PhanUndeclaredMethod 624 $context->setTitle( $page->getPageTitle( $par ) ); 625 } elseif ( !$page->isIncludable() ) { 626 return false; 627 } 628 629 $page->including( $including ); 630 if ( $linkRenderer ) { 631 $page->setLinkRenderer( $linkRenderer ); 632 } 633 634 // Execute special page 635 $page->run( $par ); 636 637 return true; 638 } 639 640 /** 641 * Just like executePath() but will override global variables and execute 642 * the page in "inclusion" mode. Returns true if the execution was 643 * successful or false if there was no such special page, or a title object 644 * if it was a redirect. 645 * 646 * Also saves the current $wgTitle, $wgOut, $wgRequest, $wgUser and $wgLang 647 * variables so that the special page will get the context it'd expect on a 648 * normal request, and then restores them to their previous values after. 649 * 650 * @param Title $title 651 * @param IContextSource $context 652 * @param LinkRenderer|null $linkRenderer (since 1.28) 653 * @return string HTML fragment 654 */ 655 public function capturePath( 656 Title $title, IContextSource $context, LinkRenderer $linkRenderer = null 657 ) { 658 global $wgTitle, $wgOut, $wgRequest, $wgUser, $wgLang; 659 $main = RequestContext::getMain(); 660 661 // Save current globals and main context 662 $glob = [ 663 'title' => $wgTitle, 664 'output' => $wgOut, 665 'request' => $wgRequest, 666 'user' => $wgUser, 667 'language' => $wgLang, 668 ]; 669 $ctx = [ 670 'title' => $main->getTitle(), 671 'output' => $main->getOutput(), 672 'request' => $main->getRequest(), 673 'user' => $main->getUser(), 674 'language' => $main->getLanguage(), 675 ]; 676 if ( $main->canUseWikiPage() ) { 677 $ctx['wikipage'] = $main->getWikiPage(); 678 } 679 680 // Override 681 $wgTitle = $title; 682 $wgOut = $context->getOutput(); 683 $wgRequest = $context->getRequest(); 684 $wgUser = $context->getUser(); 685 $wgLang = $context->getLanguage(); 686 $main->setTitle( $title ); 687 $main->setOutput( $context->getOutput() ); 688 $main->setRequest( $context->getRequest() ); 689 $main->setUser( $context->getUser() ); 690 $main->setLanguage( $context->getLanguage() ); 691 692 // The useful part 693 $ret = $this->executePath( $title, $context, true, $linkRenderer ); 694 695 // Restore old globals and context 696 $wgTitle = $glob['title']; 697 $wgOut = $glob['output']; 698 $wgRequest = $glob['request']; 699 $wgUser = $glob['user']; 700 $wgLang = $glob['language']; 701 $main->setTitle( $ctx['title'] ); 702 $main->setOutput( $ctx['output'] ); 703 $main->setRequest( $ctx['request'] ); 704 $main->setUser( $ctx['user'] ); 705 $main->setLanguage( $ctx['language'] ); 706 if ( isset( $ctx['wikipage'] ) ) { 707 $main->setWikiPage( $ctx['wikipage'] ); 708 } 709 710 return $ret; 711 } 712 713 /** 714 * Get the local name for a specified canonical name 715 * 716 * @param string $name 717 * @param string|bool $subpage 718 * @return string 719 */ 720 public function getLocalNameFor( $name, $subpage = false ) { 721 $aliases = $this->contLang->getSpecialPageAliases(); 722 $aliasList = $this->getAliasList(); 723 724 // Find the first alias that maps back to $name 725 if ( isset( $aliases[$name] ) ) { 726 $found = false; 727 foreach ( $aliases[$name] as $alias ) { 728 $caseFoldedAlias = $this->contLang->caseFold( $alias ); 729 $caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias ); 730 if ( isset( $aliasList[$caseFoldedAlias] ) && 731 $aliasList[$caseFoldedAlias] === $name 732 ) { 733 $name = $alias; 734 $found = true; 735 break; 736 } 737 } 738 if ( !$found ) { 739 wfWarn( "Did not find a usable alias for special page '$name'. " . 740 "It seems all defined aliases conflict?" ); 741 } 742 } else { 743 // Check if someone misspelled the correct casing 744 if ( is_array( $aliases ) ) { 745 foreach ( $aliases as $n => $values ) { 746 if ( strcasecmp( $name, $n ) === 0 ) { 747 wfWarn( "Found alias defined for $n when searching for " . 748 "special page aliases for $name. Case mismatch?" ); 749 return $this->getLocalNameFor( $n, $subpage ); 750 } 751 } 752 } 753 754 wfWarn( "Did not find alias for special page '$name'. " . 755 "Perhaps no aliases are defined for it?" ); 756 } 757 758 if ( $subpage !== false && $subpage !== null ) { 759 // Make sure it's in dbkey form 760 $subpage = str_replace( ' ', '_', $subpage ); 761 $name = "$name/$subpage"; 762 } 763 764 return $this->contLang->ucfirst( $name ); 765 } 766 767 /** 768 * Get a title for a given alias 769 * 770 * @param string $alias 771 * @return Title|null Title or null if there is no such alias 772 */ 773 public function getTitleForAlias( $alias ) { 774 list( $name, $subpage ) = $this->resolveAlias( $alias ); 775 if ( $name != null ) { 776 return SpecialPage::getTitleFor( $name, $subpage ); 777 } 778 779 return null; 780 } 781} 782 783/** @deprecated since 1.35, use MediaWiki\\SpecialPage\\SpecialPageFactory */ 784class_alias( SpecialPageFactory::class, 'MediaWiki\\Special\\SpecialPageFactory' ); 785