From 0ce8a2ac126d7a8d9f50201a18f2e8b5a65825a6 Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Mon, 30 Dec 2013 16:57:27 -0500 Subject: [PATCH] API: Add prop=redirects and list=allredirects While redirects can be sort-of queried using list=backlinks with blfilterredir=redirects, we can get more accurate results with a module dedicated to this purpose. We can also get the fragment of the redirect without having to load the content of the redirect page and parse it. I'm a bit surprised I was able to put together a query for this that will work as a prop module. Or did I overlook something? And then we may as well add the corresponding list=allredirects, to work like alllinks, allfileusages, and alltransclusions. Bug: 57057 Change-Id: I81082aa9e4e3a3b2c66cc4f9970a97eed83a6a4f --- RELEASE-NOTES-1.23 | 2 + includes/AutoLoader.php | 1 + includes/api/ApiQuery.php | 2 + includes/api/ApiQueryAllLinks.php | 69 ++++++-- includes/api/ApiQueryRedirects.php | 267 +++++++++++++++++++++++++++++ 5 files changed, 324 insertions(+), 17 deletions(-) create mode 100644 includes/api/ApiQueryRedirects.php diff --git a/RELEASE-NOTES-1.23 b/RELEASE-NOTES-1.23 index 2350b8e48a..adf17b503e 100644 --- a/RELEASE-NOTES-1.23 +++ b/RELEASE-NOTES-1.23 @@ -187,6 +187,8 @@ production. * (bug 58627) Provide language names on action=parse&prop=langlinks. * Deprecated llurl= in favour of llprop=url for action=query&prop=langlinks. * Added llprop=langname and llprop=autonym for action=query&prop=langlinks. +* prop=redirects is added, to return redirects to the pages in the query. +* list=allredirects is added, to list all redirects pointing to a namespace. === Languages updated in 1.23 === diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 54635e9c69..9a6d90b2d2 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -355,6 +355,7 @@ $wgAutoloadLocalClasses = array( 'ApiQueryRandom' => 'includes/api/ApiQueryRandom.php', 'ApiQueryRecentChanges' => 'includes/api/ApiQueryRecentChanges.php', 'ApiQueryFileRepoInfo' => 'includes/api/ApiQueryFileRepoInfo.php', + 'ApiQueryRedirects' => 'includes/api/ApiQueryRedirects.php', 'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php', 'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php', 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index c054bc1e90..49ab59177a 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -54,6 +54,7 @@ class ApiQuery extends ApiBase { 'iwlinks' => 'ApiQueryIWLinks', 'langlinks' => 'ApiQueryLangLinks', 'pageprops' => 'ApiQueryPageProps', + 'redirects' => 'ApiQueryRedirects', 'revisions' => 'ApiQueryRevisions', 'stashimageinfo' => 'ApiQueryStashImageInfo', 'templates' => 'ApiQueryLinks', @@ -69,6 +70,7 @@ class ApiQuery extends ApiBase { 'allimages' => 'ApiQueryAllImages', 'alllinks' => 'ApiQueryAllLinks', 'allpages' => 'ApiQueryAllPages', + 'allredirects' => 'ApiQueryAllLinks', 'alltransclusions' => 'ApiQueryAllLinks', 'allusers' => 'ApiQueryAllUsers', 'backlinks' => 'ApiQueryBacklinks', diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index bccc25fbfb..7b5123d5f2 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -31,15 +31,21 @@ */ class ApiQueryAllLinks extends ApiQueryGeneratorBase { + private $table, $tablePrefix, $indexTag, + $description, $descriptionWhat, $descriptionTargets, $descriptionLinking; + private $fieldTitle = 'title'; + private $dfltNamespace = NS_MAIN; + private $hasNamespace = true; + private $useIndex = null; + private $props = array(), $propHelp = array(); + public function __construct( $query, $moduleName ) { switch ( $moduleName ) { case 'alllinks': $prefix = 'al'; $this->table = 'pagelinks'; $this->tablePrefix = 'pl_'; - $this->fieldTitle = 'title'; - $this->dfltNamespace = NS_MAIN; - $this->hasNamespace = true; + $this->useIndex = 'pl_namespace'; $this->indexTag = 'l'; $this->description = 'Enumerate all links that point to a given namespace'; $this->descriptionWhat = 'link'; @@ -50,9 +56,8 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $prefix = 'at'; $this->table = 'templatelinks'; $this->tablePrefix = 'tl_'; - $this->fieldTitle = 'title'; $this->dfltNamespace = NS_TEMPLATE; - $this->hasNamespace = true; + $this->useIndex = 'tl_namespace'; $this->indexTag = 't'; $this->description = 'List all transclusions (pages embedded using {{x}}), including non-existing'; @@ -73,6 +78,24 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $this->descriptionTargets = 'file titles'; $this->descriptionLinking = 'using'; break; + case 'allredirects': + $prefix = 'ar'; + $this->table = 'redirect'; + $this->tablePrefix = 'rd_'; + $this->indexTag = 'r'; + $this->description = 'List all redirects to a namespace'; + $this->descriptionWhat = 'redirect'; + $this->descriptionTargets = 'target pages'; + $this->descriptionLinking = 'redirecting'; + $this->props = array( + 'fragment' => 'rd_fragment', + 'interwiki' => 'rd_interwiki', + ); + $this->propHelp = array( + ' fragment - Adds the fragment from the redirect, if any', + ' interwiki - Adds the interwiki prefix from the redirect, if any', + ); + break; default: ApiBase::dieDebug( __METHOD__, 'Unknown module name' ); } @@ -112,10 +135,11 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { } if ( $params['unique'] ) { - if ( $fld_ids ) { + $matches = array_intersect_key( $prop, $this->props + array( 'ids' => 1 ) ); + if ( $matches ) { + $p = $this->getModulePrefix(); $this->dieUsage( - "{$this->getModuleName()} cannot return corresponding page " . - "ids in unique {$this->descriptionWhat}s mode", + "Cannot use {$p}prop=" . join( '|', array_keys( $matches ) ) . " with {$p}unique", 'params' ); } @@ -161,9 +185,12 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $this->addFields( array( 'pl_title' => $pfx . $fieldTitle ) ); $this->addFieldsIf( array( 'pl_from' => $pfx . 'from' ), !$params['unique'] ); + foreach ( $this->props as $name => $field ) { + $this->addFieldsIf( $field, isset( $prop[$name] ) ); + } - if ( $this->hasNamespace ) { - $this->addOption( 'USE INDEX', $pfx . 'namespace' ); + if ( $this->useIndex ) { + $this->addOption( 'USE INDEX', $this->useIndex ); } $limit = $params['limit']; $this->addOption( 'LIMIT', $limit + 1 ); @@ -203,6 +230,11 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $title = Title::makeTitle( $namespace, $row->pl_title ); ApiQueryBase::addTitleInfo( $vals, $title ); } + foreach ( $this->props as $name => $field ) { + if ( isset( $prop[$name] ) && $row->$field !== null && $row->$field !== '' ) { + $vals[$name] = $row->$field; + } + } $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { if ( $params['unique'] ) { @@ -238,10 +270,9 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { 'prop' => array( ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_DFLT => 'title', - ApiBase::PARAM_TYPE => array( - 'ids', - 'title' - ) + ApiBase::PARAM_TYPE => array_merge( + array( 'ids', 'title' ), array_keys( $this->props ) + ), ), 'namespace' => array( ApiBase::PARAM_DFLT => $this->dfltNamespace, @@ -279,19 +310,23 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { 'to' => "The title of the $what to stop enumerating at", 'prefix' => "Search for all $targets that begin with this value", 'unique' => array( - "Only show distinct $targets. Cannot be used with {$p}prop=ids.", + "Only show distinct $targets. Cannot be used with {$p}prop=" . + join( '|', array_keys( array( 'ids' => 1 ) + $this->props ) ) . '.', 'When used as a generator, yields target pages instead of source pages.', ), 'prop' => array( 'What pieces of information to include', - " ids - Adds the pageid of the $linking page (Cannot be used with {$p}unique)", - " title - Adds the title of the $what", + " ids - Adds the pageid of the $linking page (Cannot be used with {$p}unique)", + " title - Adds the title of the $what", ), 'namespace' => 'The namespace to enumerate', 'limit' => 'How many total items to return', 'continue' => 'When more results are available, use this to continue', 'dir' => 'The direction in which to list', ); + foreach ( $this->propHelp as $help ) { + $paramDescription['prop'][] = "$help (Cannot be used with {$p}unique)"; + } if ( !$this->hasNamespace ) { unset( $paramDescription['namespace'] ); } diff --git a/includes/api/ApiQueryRedirects.php b/includes/api/ApiQueryRedirects.php new file mode 100644 index 0000000000..c046109d3a --- /dev/null +++ b/includes/api/ApiQueryRedirects.php @@ -0,0 +1,267 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.23 + */ + +/** + * This query lists redirects to the given pages. + * + * @ingroup API + */ +class ApiQueryRedirects extends ApiQueryGeneratorBase { + + public function __construct( $query, $moduleName ) { + parent::__construct( $query, $moduleName, 'rd' ); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator( $resultPageSet ) { + $this->run( $resultPageSet ); + } + + /** + * @param $resultPageSet ApiPageSet + */ + private function run( ApiPageSet $resultPageSet = null ) { + $db = $this->getDB(); + $params = $this->extractRequestParams(); + $emptyString = $db->addQuotes( '' ); + + $pageSet = $this->getPageSet(); + $titles = $pageSet->getGoodTitles() + $pageSet->getMissingTitles(); + + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 3 ); + $rd_namespace = (int)$cont[0]; + $this->dieContinueUsageIf( $rd_namespace != $cont[0] ); + $rd_title = $db->addQuotes( $cont[1] ); + $rd_from = (int)$cont[2]; + $this->dieContinueUsageIf( $rd_from != $cont[2] ); + $this->addWhere( + "rd_namespace > $rd_namespace OR " . + "(rd_namespace = $rd_namespace AND " . + "(rd_title > $rd_title OR " . + "(rd_title = $rd_title AND " . + "rd_from >= $rd_from)))" + ); + + // Remove titles that we're past already + $titles = array_filter( $titles, function ( $t ) use ( $rd_namespace, $rd_title ) { + $ns = $t->getNamespace(); + return ( $ns > $rd_namespace || + $ns == $rd_namespace && $t->getDBKey() >= $rd_title + ); + } ); + } + + if ( !$titles ) { + return; // nothing to do + } + + $this->addTables( array( 'redirect', 'page' ) ); + $this->addFields( array( + 'rd_from', + 'rd_namespace', + 'rd_title', + ) ); + + if ( is_null( $resultPageSet ) ) { + $prop = array_flip( $params['prop'] ); + $fld_pageid = isset( $prop['pageid'] ); + $fld_title = isset( $prop['title'] ); + $fld_fragment = isset( $prop['fragment'] ); + + $this->addFieldsIf( 'rd_fragment', $fld_fragment ); + $this->addFieldsIf( array( 'page_namespace', 'page_title' ), $fld_title ); + } else { + $this->addFields( array( 'page_namespace', 'page_title' ) ); + } + + $lb = new LinkBatch( $titles ); + $this->addWhere( array( + 'rd_from = page_id', + "rd_interwiki = $emptyString OR rd_interwiki IS NULL", + $lb->constructSet( 'rd', $db ), + ) ); + + if ( $params['show'] !== null ) { + $show = array_flip( $params['show'] ); + if ( isset( $show['fragment'] ) && isset( $show['!fragment'] ) ) { + $this->dieUsageMsg( 'show' ); + } + $this->addWhereIf( "rd_fragment != $emptyString", isset( $show['fragment'] ) ); + $this->addWhereIf( "rd_fragment = $emptyString OR rd_fragment IS NULL", isset( $show['!fragment'] ) ); + } + + $map = $pageSet->getAllTitlesByNamespace(); + + // Why, MySQL? Why do you do this to us? + $sortby = array(); + if ( count( $map ) > 1 ) { + $sortby[] = 'rd_namespace'; + } + $theTitle = null; + foreach ( $map as $nsTitles ) { + reset( $nsTitles ); + $key = key( $nsTitles ); + if ( $theTitle === null ) { + $theTitle = $key; + } + if ( count( $nsTitles ) > 1 || $key !== $theTitle ) { + $sortby[] = 'rd_title'; + break; + } + } + $sortby[] = 'rd_from'; + $this->addOption( 'ORDER BY', $sortby ); + + $this->addOption( 'LIMIT', $params['limit'] + 1 ); + + $res = $this->select( __METHOD__ ); + + if ( is_null( $resultPageSet ) ) { + $count = 0; + foreach ( $res as $row ) { + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', + "$row->rd_namespace|$row->rd_title|$row->rd_from" + ); + break; + } + + # Get the ID of the current page + $id = $map[$row->rd_namespace][$row->rd_title]; + + $vals = array(); + if ( $fld_pageid ) { + $vals['pageid'] = $row->rd_from; + } + if ( $fld_title ) { + ApiQueryBase::addTitleInfo( $vals, + Title::makeTitle( $row->page_namespace, $row->page_title ) + ); + } + if ( $fld_fragment && $row->rd_fragment !== null && $row->rd_fragment !== '' ) { + $vals['fragment'] = $row->rd_fragment; + } + $fit = $this->addPageSubItem( $id, $vals ); + if ( !$fit ) { + $this->setContinueEnumParameter( 'continue', + "$row->rd_namespace|$row->rd_title|$row->rd_from" + ); + break; + } + } + } else { + $titles = array(); + $count = 0; + foreach ( $res as $row ) { + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', + "$row->rd_namespace|$row->rd_title|$row->rd_from" + ); + break; + } + $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title ); + } + $resultPageSet->populateFromTitles( $titles ); + } + } + + public function getCacheMode( $params ) { + return 'public'; + } + + public function getAllowedParams() { + return array( + 'prop' => array( + ApiBase::PARAM_TYPE => array( + 'pageid', + 'title', + 'fragment', + ), + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_DFLT => 'pageid|title', + ), + 'show' => array( + ApiBase::PARAM_TYPE => array( + 'fragment', '!fragment', + ), + ApiBase::PARAM_ISMULTI => true, + ), + 'limit' => array( + ApiBase::PARAM_DFLT => 10, + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MIN => 1, + ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, + ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 + ), + 'continue' => null, + ); + } + + public function getParamDescription() { + return array( + 'prop' => array( + 'Which properties to get:', + ' pageid - Page id of each redirect', + ' title - Title of each redirect', + ' fragment - Fragment of each redirect, if any', + ), + 'show' => array( + 'Show only items that meet this criteria.', + ' fragment - Only show redirects with a fragment', + ' !fragment - Only show redirects without a fragment', + ), + 'limit' => 'How many redirects to return', + 'continue' => 'When more results are available, use this to continue', + ); + } + + public function getDescription() { + return 'Returns all redirects to the given page(s)'; + } + + public function getExamples() { + return array( + 'api.php?action=query&prop=redirects&titles=Main%20Page' + => 'Get a list of redirects to the [[Main Page]]', + 'api.php?action=query&generator=redirects&titles=Main%20Page&prop=info' + => 'Get information about all redirects to the [[Main Page]]', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Properties#redirects_.2F_rd'; + } +} -- 2.20.1