From 2338fe08c1928b9eaf0b4210bb3aee9cc2b7e4cf Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Mon, 6 Mar 2017 14:46:58 -0500 Subject: [PATCH] ApiPageSet: Follow RedirectSpecialArticle redirects For example Special:MyPage, Special:MyTalk, and Special:MyLanguage. Don't follow other redirect special pages like Special:MyContributions, though, because the following only really makes sense when the redirect is to an article. Bug: T145541 Change-Id: I8c8065552ed128017887e48285e359def8bd3cd3 --- includes/api/ApiPageSet.php | 137 ++++++++++++------ tests/phpunit/includes/api/ApiPageSetTest.php | 79 ++++++++++ 2 files changed, 168 insertions(+), 48 deletions(-) diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index 7d16af8f6b..71f6e0bf5d 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -70,6 +70,7 @@ class ApiPageSet extends ApiBase { private $mInterwikiTitles = []; /** @var Title[] */ private $mPendingRedirectIDs = []; + private $mPendingRedirectSpecialPages = []; // [dbkey] => [ Title $from, Title $to ] private $mResolvedRedirectTitles = []; private $mConvertedTitles = []; private $mGoodRevIDs = []; @@ -812,6 +813,8 @@ class ApiPageSet extends ApiBase { // Get validated and normalized title objects $linkBatch = $this->processTitlesArray( $titles ); if ( $linkBatch->isEmpty() ) { + // There might be special-page redirects + $this->resolvePendingRedirects(); return; } @@ -1032,7 +1035,7 @@ class ApiPageSet extends ApiBase { // Repeat until all redirects have been resolved // The infinite loop is prevented by keeping all known pages in $this->mAllPages - while ( $this->mPendingRedirectIDs ) { + while ( $this->mPendingRedirectIDs || $this->mPendingRedirectSpecialPages ) { // Resolve redirects by querying the pagelinks table, and repeat the process // Create a new linkBatch object for the next pass $linkBatch = $this->getRedirectTargets(); @@ -1066,56 +1069,76 @@ class ApiPageSet extends ApiBase { $titlesToResolve = []; $db = $this->getDB(); - $res = $db->select( - 'redirect', - [ - 'rd_from', - 'rd_namespace', - 'rd_fragment', - 'rd_interwiki', - 'rd_title' - ], [ 'rd_from' => array_keys( $this->mPendingRedirectIDs ) ], - __METHOD__ - ); - foreach ( $res as $row ) { - $rdfrom = intval( $row->rd_from ); - $from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText(); - $to = Title::makeTitle( - $row->rd_namespace, - $row->rd_title, - $row->rd_fragment, - $row->rd_interwiki - ); - $this->mResolvedRedirectTitles[$from] = $this->mPendingRedirectIDs[$rdfrom]; - unset( $this->mPendingRedirectIDs[$rdfrom] ); - if ( $to->isExternal() ) { - $this->mInterwikiTitles[$to->getPrefixedText()] = $to->getInterwiki(); - } elseif ( !isset( $this->mAllPages[$to->getNamespace()][$to->getDBkey()] ) ) { - $titlesToResolve[] = $to; + if ( $this->mPendingRedirectIDs ) { + $res = $db->select( + 'redirect', + [ + 'rd_from', + 'rd_namespace', + 'rd_fragment', + 'rd_interwiki', + 'rd_title' + ], [ 'rd_from' => array_keys( $this->mPendingRedirectIDs ) ], + __METHOD__ + ); + foreach ( $res as $row ) { + $rdfrom = intval( $row->rd_from ); + $from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText(); + $to = Title::makeTitle( + $row->rd_namespace, + $row->rd_title, + $row->rd_fragment, + $row->rd_interwiki + ); + $this->mResolvedRedirectTitles[$from] = $this->mPendingRedirectIDs[$rdfrom]; + unset( $this->mPendingRedirectIDs[$rdfrom] ); + if ( $to->isExternal() ) { + $this->mInterwikiTitles[$to->getPrefixedText()] = $to->getInterwiki(); + } elseif ( !isset( $this->mAllPages[$to->getNamespace()][$to->getDBkey()] ) ) { + $titlesToResolve[] = $to; + } + $this->mRedirectTitles[$from] = $to; } - $this->mRedirectTitles[$from] = $to; - } - if ( $this->mPendingRedirectIDs ) { - // We found pages that aren't in the redirect table - // Add them - foreach ( $this->mPendingRedirectIDs as $id => $title ) { - $page = WikiPage::factory( $title ); - $rt = $page->insertRedirect(); - if ( !$rt ) { - // What the hell. Let's just ignore this - continue; + if ( $this->mPendingRedirectIDs ) { + // We found pages that aren't in the redirect table + // Add them + foreach ( $this->mPendingRedirectIDs as $id => $title ) { + $page = WikiPage::factory( $title ); + $rt = $page->insertRedirect(); + if ( !$rt ) { + // What the hell. Let's just ignore this + continue; + } + if ( $rt->isExternal() ) { + $this->mInterwikiTitles[$rt->getPrefixedText()] = $rt->getInterwiki(); + } elseif ( !isset( $this->mAllPages[$rt->getNamespace()][$rt->getDBkey()] ) ) { + $titlesToResolve[] = $rt; + } + $from = $title->getPrefixedText(); + $this->mResolvedRedirectTitles[$from] = $title; + $this->mRedirectTitles[$from] = $rt; + unset( $this->mPendingRedirectIDs[$id] ); } - if ( $rt->isExternal() ) { - $this->mInterwikiTitles[$rt->getPrefixedText()] = $rt->getInterwiki(); - } elseif ( !isset( $this->mAllPages[$rt->getNamespace()][$rt->getDBkey()] ) ) { - $titlesToResolve[] = $rt; + } + } + + if ( $this->mPendingRedirectSpecialPages ) { + foreach ( $this->mPendingRedirectSpecialPages as $key => list( $from, $to ) ) { + $fromKey = $from->getPrefixedText(); + $this->mResolvedRedirectTitles[$fromKey] = $from; + $this->mRedirectTitles[$fromKey] = $to; + if ( $to->isExternal() ) { + $this->mInterwikiTitles[$to->getPrefixedText()] = $to->getInterwiki(); + } elseif ( !isset( $this->mAllPages[$to->getNamespace()][$to->getDBkey()] ) ) { + $titlesToResolve[] = $to; } - $from = $title->getPrefixedText(); - $this->mResolvedRedirectTitles[$from] = $title; - $this->mRedirectTitles[$from] = $rt; - unset( $this->mPendingRedirectIDs[$id] ); } + $this->mPendingRedirectSpecialPages = []; + + // Set private caching since we don't know what criteria the + // special pages used to decide on these redirects. + $this->mCacheMode = 'private'; } return $this->processTitlesArray( $titlesToResolve ); @@ -1196,8 +1219,26 @@ class ApiPageSet extends ApiBase { $dbkey = $titleObj->getDBkey(); if ( !isset( $this->mAllSpecials[$ns][$dbkey] ) ) { $this->mAllSpecials[$ns][$dbkey] = $this->mFakePageId; - $this->mSpecialTitles[$this->mFakePageId] = $titleObj; - $this->mFakePageId--; + $target = null; + if ( $ns === NS_SPECIAL && $this->mResolveRedirects ) { + $special = SpecialPageFactory::getPage( $dbkey ); + if ( $special instanceof RedirectSpecialArticle ) { + // Only RedirectSpecialArticle is intended to redirect to an article, other kinds of + // RedirectSpecialPage are probably applying weird URL parameters we don't want to handle. + $context = new DerivativeContext( $this ); + $context->setTitle( $titleObj ); + $context->setRequest( new FauxRequest ); + $special->setContext( $context ); + list( /* $alias */, $subpage ) = SpecialPageFactory::resolveAlias( $dbkey ); + $target = $special->getRedirect( $subpage ); + } + } + if ( $target ) { + $this->mPendingRedirectSpecialPages[$dbkey] = [ $titleObj, $target ]; + } else { + $this->mSpecialTitles[$this->mFakePageId] = $titleObj; + $this->mFakePageId--; + } } } else { // Regular page diff --git a/tests/phpunit/includes/api/ApiPageSetTest.php b/tests/phpunit/includes/api/ApiPageSetTest.php index ad1deee5ce..8a2146a15b 100644 --- a/tests/phpunit/includes/api/ApiPageSetTest.php +++ b/tests/phpunit/includes/api/ApiPageSetTest.php @@ -96,4 +96,83 @@ class ApiPageSetTest extends ApiTestCase { $pageSet->getNormalizedTitlesAsResult() ); } + + public function testSpecialRedirects() { + $id1 = self::editPage( 'UTApiPageSet', 'UTApiPageSet in the default language' ) + ->value['revision']->getTitle()->getArticleID(); + $id2 = self::editPage( 'UTApiPageSet/de', 'UTApiPageSet in German' ) + ->value['revision']->getTitle()->getArticleID(); + + $user = $this->getTestUser()->getUser(); + $userName = $user->getName(); + $userDbkey = str_replace( ' ', '_', $userName ); + $request = new FauxRequest( [ + 'titles' => join( '|', [ + 'Special:MyContributions', + 'Special:MyPage', + 'Special:MyTalk/subpage', + 'Special:MyLanguage/UTApiPageSet', + ] ), + ] ); + $context = new RequestContext(); + $context->setRequest( $request ); + $context->setUser( $user ); + + $main = new ApiMain( $context ); + $pageSet = new ApiPageSet( $main ); + $pageSet->execute(); + + $this->assertEquals( [ + ], $pageSet->getRedirectTitlesAsResult() ); + $this->assertEquals( [ + [ 'ns' => -1, 'title' => 'Special:MyContributions', 'special' => true ], + [ 'ns' => -1, 'title' => 'Special:MyPage', 'special' => true ], + [ 'ns' => -1, 'title' => 'Special:MyTalk/subpage', 'special' => true ], + [ 'ns' => -1, 'title' => 'Special:MyLanguage/UTApiPageSet', 'special' => true ], + ], $pageSet->getInvalidTitlesAndRevisions() ); + $this->assertEquals( [ + ], $pageSet->getAllTitlesByNamespace() ); + + $request->setVal( 'redirects', 1 ); + $main = new ApiMain( $context ); + $pageSet = new ApiPageSet( $main ); + $pageSet->execute(); + + $this->assertEquals( [ + [ 'from' => 'Special:MyPage', 'to' => "User:$userName" ], + [ 'from' => 'Special:MyTalk/subpage', 'to' => "User talk:$userName/subpage" ], + [ 'from' => 'Special:MyLanguage/UTApiPageSet', 'to' => 'UTApiPageSet' ], + ], $pageSet->getRedirectTitlesAsResult() ); + $this->assertEquals( [ + [ 'ns' => -1, 'title' => 'Special:MyContributions', 'special' => true ], + [ 'ns' => 2, 'title' => "User:$userName", 'missing' => true ], + [ 'ns' => 3, 'title' => "User talk:$userName/subpage", 'missing' => true ], + ], $pageSet->getInvalidTitlesAndRevisions() ); + $this->assertEquals( [ + 0 => [ 'UTApiPageSet' => $id1 ], + 2 => [ $userDbkey => -2 ], + 3 => [ "$userDbkey/subpage" => -3 ], + ], $pageSet->getAllTitlesByNamespace() ); + + $context->setLanguage( 'de' ); + $main = new ApiMain( $context ); + $pageSet = new ApiPageSet( $main ); + $pageSet->execute(); + + $this->assertEquals( [ + [ 'from' => 'Special:MyPage', 'to' => "User:$userName" ], + [ 'from' => 'Special:MyTalk/subpage', 'to' => "User talk:$userName/subpage" ], + [ 'from' => 'Special:MyLanguage/UTApiPageSet', 'to' => 'UTApiPageSet/de' ], + ], $pageSet->getRedirectTitlesAsResult() ); + $this->assertEquals( [ + [ 'ns' => -1, 'title' => 'Special:MyContributions', 'special' => true ], + [ 'ns' => 2, 'title' => "User:$userName", 'missing' => true ], + [ 'ns' => 3, 'title' => "User talk:$userName/subpage", 'missing' => true ], + ], $pageSet->getInvalidTitlesAndRevisions() ); + $this->assertEquals( [ + 0 => [ 'UTApiPageSet/de' => $id2 ], + 2 => [ $userDbkey => -2 ], + 3 => [ "$userDbkey/subpage" => -3 ], + ], $pageSet->getAllTitlesByNamespace() ); + } } -- 2.20.1