wiki.techinc.nl/includes/api/ApiQueryBacklinks.php
Brad Jorsch 47e2bec361 API: Allow finding log events and links to special pages
Log events are sometimes attributed to a special page; it should be
allowed to use rcnamespace or lenamespace to filter for these.

It's also possible for special pages to be the targets of redirects, so
list=allredirects and prop=redirects should find them.

Maybe someday we'll record links to and transclusions of special pages
too (see T19597), so we may as well make it possible to query for those
as well via list=alllinks, list=alltransclusions, list=backlinks,
list=embeddedin, prop=linkshere, prop=transcludedin, prop=links, and
prop=templates.

NS_MEDIA has similar considerations: although we currently "normalize"
page links to the corresponding File and I don't think anything logs the
Media title rather than the File, transclusions and redirects do show
up in those tables.

Bug: T154319
Change-Id: I00348f83855c6c703b6bd6015f6d3bedc5bfd1c5
2017-01-06 13:07:29 -05:00

580 lines
17 KiB
PHP

<?php
/**
*
*
* Created on Oct 16, 2006
*
* Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
*
* 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
*/
/**
* This is a three-in-one module to query:
* * backlinks - links pointing to the given page,
* * embeddedin - what pages transclude the given page within themselves,
* * imageusage - what pages use the given image
*
* @ingroup API
*/
class ApiQueryBacklinks extends ApiQueryGeneratorBase {
/**
* @var Title
*/
private $rootTitle;
private $params, $cont, $redirect;
private $bl_ns, $bl_from, $bl_from_ns, $bl_table, $bl_code, $bl_title, $bl_fields, $hasNS;
/**
* Maps ns and title to pageid
*
* @var array
*/
private $pageMap = [];
private $resultArr;
private $redirTitles = [];
private $continueStr = null;
// output element name, database column field prefix, database table
private $backlinksSettings = [
'backlinks' => [
'code' => 'bl',
'prefix' => 'pl',
'linktbl' => 'pagelinks',
'helpurl' => 'https://www.mediawiki.org/wiki/API:Backlinks',
],
'embeddedin' => [
'code' => 'ei',
'prefix' => 'tl',
'linktbl' => 'templatelinks',
'helpurl' => 'https://www.mediawiki.org/wiki/API:Embeddedin',
],
'imageusage' => [
'code' => 'iu',
'prefix' => 'il',
'linktbl' => 'imagelinks',
'helpurl' => 'https://www.mediawiki.org/wiki/API:Imageusage',
]
];
public function __construct( ApiQuery $query, $moduleName ) {
$settings = $this->backlinksSettings[$moduleName];
$prefix = $settings['prefix'];
$code = $settings['code'];
$this->resultArr = [];
parent::__construct( $query, $moduleName, $code );
$this->bl_ns = $prefix . '_namespace';
$this->bl_from = $prefix . '_from';
$this->bl_from_ns = $prefix . '_from_namespace';
$this->bl_table = $settings['linktbl'];
$this->bl_code = $code;
$this->helpUrl = $settings['helpurl'];
$this->hasNS = $moduleName !== 'imageusage';
if ( $this->hasNS ) {
$this->bl_title = $prefix . '_title';
$this->bl_fields = [
$this->bl_ns,
$this->bl_title
];
} else {
$this->bl_title = $prefix . '_to';
$this->bl_fields = [
$this->bl_title
];
}
}
public function execute() {
$this->run();
}
public function getCacheMode( $params ) {
return 'public';
}
public function executeGenerator( $resultPageSet ) {
$this->run( $resultPageSet );
}
/**
* @param ApiPageSet $resultPageSet
* @return void
*/
private function runFirstQuery( $resultPageSet = null ) {
$this->addTables( [ $this->bl_table, 'page' ] );
$this->addWhere( "{$this->bl_from}=page_id" );
if ( is_null( $resultPageSet ) ) {
$this->addFields( [ 'page_id', 'page_title', 'page_namespace' ] );
} else {
$this->addFields( $resultPageSet->getPageTableFields() );
}
$this->addFields( [ 'page_is_redirect', 'from_ns' => 'page_namespace' ] );
$this->addWhereFld( $this->bl_title, $this->rootTitle->getDBkey() );
if ( $this->hasNS ) {
$this->addWhereFld( $this->bl_ns, $this->rootTitle->getNamespace() );
}
$this->addWhereFld( $this->bl_from_ns, $this->params['namespace'] );
if ( count( $this->cont ) >= 2 ) {
$op = $this->params['dir'] == 'descending' ? '<' : '>';
if ( count( $this->params['namespace'] ) > 1 ) {
$this->addWhere(
"{$this->bl_from_ns} $op {$this->cont[0]} OR " .
"({$this->bl_from_ns} = {$this->cont[0]} AND " .
"{$this->bl_from} $op= {$this->cont[1]})"
);
} else {
$this->addWhere( "{$this->bl_from} $op= {$this->cont[1]}" );
}
}
if ( $this->params['filterredir'] == 'redirects' ) {
$this->addWhereFld( 'page_is_redirect', 1 );
} elseif ( $this->params['filterredir'] == 'nonredirects' && !$this->redirect ) {
// bug 22245 - Check for !redirect, as filtering nonredirects, when
// getting what links to them is contradictory
$this->addWhereFld( 'page_is_redirect', 0 );
}
$this->addOption( 'LIMIT', $this->params['limit'] + 1 );
$sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
$orderBy = [];
if ( count( $this->params['namespace'] ) > 1 ) {
$orderBy[] = $this->bl_from_ns . $sort;
}
$orderBy[] = $this->bl_from . $sort;
$this->addOption( 'ORDER BY', $orderBy );
$this->addOption( 'STRAIGHT_JOIN' );
$res = $this->select( __METHOD__ );
$count = 0;
foreach ( $res as $row ) {
if ( ++$count > $this->params['limit'] ) {
// We've reached the one extra which shows that there are
// additional pages to be had. Stop here...
// Continue string may be overridden at a later step
$this->continueStr = "{$row->from_ns}|{$row->page_id}";
break;
}
// Fill in continuation fields for later steps
if ( count( $this->cont ) < 2 ) {
$this->cont[] = $row->from_ns;
$this->cont[] = $row->page_id;
}
$this->pageMap[$row->page_namespace][$row->page_title] = $row->page_id;
$t = Title::makeTitle( $row->page_namespace, $row->page_title );
if ( $row->page_is_redirect ) {
$this->redirTitles[] = $t;
}
if ( is_null( $resultPageSet ) ) {
$a = [ 'pageid' => intval( $row->page_id ) ];
ApiQueryBase::addTitleInfo( $a, $t );
if ( $row->page_is_redirect ) {
$a['redirect'] = true;
}
// Put all the results in an array first
$this->resultArr[$a['pageid']] = $a;
} else {
$resultPageSet->processDbRow( $row );
}
}
}
/**
* @param ApiPageSet $resultPageSet
* @return void
*/
private function runSecondQuery( $resultPageSet = null ) {
$db = $this->getDB();
$this->addTables( [ 'page', $this->bl_table ] );
$this->addWhere( "{$this->bl_from}=page_id" );
if ( is_null( $resultPageSet ) ) {
$this->addFields( [ 'page_id', 'page_title', 'page_namespace', 'page_is_redirect' ] );
} else {
$this->addFields( $resultPageSet->getPageTableFields() );
}
$this->addFields( [ $this->bl_title, 'from_ns' => 'page_namespace' ] );
if ( $this->hasNS ) {
$this->addFields( $this->bl_ns );
}
// We can't use LinkBatch here because $this->hasNS may be false
$titleWhere = [];
$allRedirNs = [];
$allRedirDBkey = [];
/** @var $t Title */
foreach ( $this->redirTitles as $t ) {
$redirNs = $t->getNamespace();
$redirDBkey = $t->getDBkey();
$titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $redirDBkey ) .
( $this->hasNS ? " AND {$this->bl_ns} = {$redirNs}" : '' );
$allRedirNs[$redirNs] = true;
$allRedirDBkey[$redirDBkey] = true;
}
$this->addWhere( $db->makeList( $titleWhere, LIST_OR ) );
$this->addWhereFld( 'page_namespace', $this->params['namespace'] );
if ( count( $this->cont ) >= 6 ) {
$op = $this->params['dir'] == 'descending' ? '<' : '>';
$where = "{$this->bl_from} $op= {$this->cont[5]}";
// Don't bother with namespace, title, or from_namespace if it's
// otherwise constant in the where clause.
if ( count( $this->params['namespace'] ) > 1 ) {
$where = "{$this->bl_from_ns} $op {$this->cont[4]} OR " .
"({$this->bl_from_ns} = {$this->cont[4]} AND ($where))";
}
if ( count( $allRedirDBkey ) > 1 ) {
$title = $db->addQuotes( $this->cont[3] );
$where = "{$this->bl_title} $op $title OR " .
"({$this->bl_title} = $title AND ($where))";
}
if ( $this->hasNS && count( $allRedirNs ) > 1 ) {
$where = "{$this->bl_ns} $op {$this->cont[2]} OR " .
"({$this->bl_ns} = {$this->cont[2]} AND ($where))";
}
$this->addWhere( $where );
}
if ( $this->params['filterredir'] == 'redirects' ) {
$this->addWhereFld( 'page_is_redirect', 1 );
} elseif ( $this->params['filterredir'] == 'nonredirects' ) {
$this->addWhereFld( 'page_is_redirect', 0 );
}
$this->addOption( 'LIMIT', $this->params['limit'] + 1 );
$orderBy = [];
$sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
// Don't order by namespace/title/from_namespace if it's constant in the WHERE clause
if ( $this->hasNS && count( $allRedirNs ) > 1 ) {
$orderBy[] = $this->bl_ns . $sort;
}
if ( count( $allRedirDBkey ) > 1 ) {
$orderBy[] = $this->bl_title . $sort;
}
if ( count( $this->params['namespace'] ) > 1 ) {
$orderBy[] = $this->bl_from_ns . $sort;
}
$orderBy[] = $this->bl_from . $sort;
$this->addOption( 'ORDER BY', $orderBy );
$this->addOption( 'USE INDEX', [ 'page' => 'PRIMARY' ] );
$res = $this->select( __METHOD__ );
$count = 0;
foreach ( $res as $row ) {
$ns = $this->hasNS ? $row->{$this->bl_ns} : NS_FILE;
if ( ++$count > $this->params['limit'] ) {
// We've reached the one extra which shows that there are
// additional pages to be had. Stop here...
// Note we must keep the parameters for the first query constant
// This may be overridden at a later step
$title = $row->{$this->bl_title};
$this->continueStr = implode( '|', array_slice( $this->cont, 0, 2 ) ) .
"|$ns|$title|{$row->from_ns}|{$row->page_id}";
break;
}
// Fill in continuation fields for later steps
if ( count( $this->cont ) < 6 ) {
$this->cont[] = $ns;
$this->cont[] = $row->{$this->bl_title};
$this->cont[] = $row->from_ns;
$this->cont[] = $row->page_id;
}
if ( is_null( $resultPageSet ) ) {
$a['pageid'] = intval( $row->page_id );
ApiQueryBase::addTitleInfo( $a, Title::makeTitle( $row->page_namespace, $row->page_title ) );
if ( $row->page_is_redirect ) {
$a['redirect'] = true;
}
$parentID = $this->pageMap[$ns][$row->{$this->bl_title}];
// Put all the results in an array first
$this->resultArr[$parentID]['redirlinks'][$row->page_id] = $a;
} else {
$resultPageSet->processDbRow( $row );
}
}
}
/**
* @param ApiPageSet $resultPageSet
* @return void
*/
private function run( $resultPageSet = null ) {
$this->params = $this->extractRequestParams( false );
$this->redirect = isset( $this->params['redirect'] ) && $this->params['redirect'];
$userMax = ( $this->redirect ? ApiBase::LIMIT_BIG1 / 2 : ApiBase::LIMIT_BIG1 );
$botMax = ( $this->redirect ? ApiBase::LIMIT_BIG2 / 2 : ApiBase::LIMIT_BIG2 );
$result = $this->getResult();
if ( $this->params['limit'] == 'max' ) {
$this->params['limit'] = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
$result->addParsedLimit( $this->getModuleName(), $this->params['limit'] );
} else {
$this->params['limit'] = intval( $this->params['limit'] );
$this->validateLimit( 'limit', $this->params['limit'], 1, $userMax, $botMax );
}
$this->rootTitle = $this->getTitleFromTitleOrPageId( $this->params );
// only image titles are allowed for the root in imageinfo mode
if ( !$this->hasNS && $this->rootTitle->getNamespace() !== NS_FILE ) {
$this->dieWithError(
[ 'apierror-imageusage-badtitle', $this->getModuleName() ],
'bad_image_title'
);
}
// Parse and validate continuation parameter
$this->cont = [];
if ( $this->params['continue'] !== null ) {
$cont = explode( '|', $this->params['continue'] );
switch ( count( $cont ) ) {
case 8:
// redirect page ID for result adding
$this->cont[7] = (int)$cont[7];
$this->dieContinueUsageIf( $cont[7] !== (string)$this->cont[7] );
/* Fall through */
case 7:
// top-level page ID for result adding
$this->cont[6] = (int)$cont[6];
$this->dieContinueUsageIf( $cont[6] !== (string)$this->cont[6] );
/* Fall through */
case 6:
// ns for 2nd query (even for imageusage)
$this->cont[2] = (int)$cont[2];
$this->dieContinueUsageIf( $cont[2] !== (string)$this->cont[2] );
// title for 2nd query
$this->cont[3] = $cont[3];
// from_ns for 2nd query
$this->cont[4] = (int)$cont[4];
$this->dieContinueUsageIf( $cont[4] !== (string)$this->cont[4] );
// from_id for 1st query
$this->cont[5] = (int)$cont[5];
$this->dieContinueUsageIf( $cont[5] !== (string)$this->cont[5] );
/* Fall through */
case 2:
// from_ns for 1st query
$this->cont[0] = (int)$cont[0];
$this->dieContinueUsageIf( $cont[0] !== (string)$this->cont[0] );
// from_id for 1st query
$this->cont[1] = (int)$cont[1];
$this->dieContinueUsageIf( $cont[1] !== (string)$this->cont[1] );
break;
default:
$this->dieContinueUsageIf( true );
}
ksort( $this->cont );
}
$this->runFirstQuery( $resultPageSet );
if ( $this->redirect && count( $this->redirTitles ) ) {
$this->resetQueryParams();
$this->runSecondQuery( $resultPageSet );
}
// Fill in any missing fields in case it's needed below
$this->cont += [ 0, 0, 0, '', 0, 0, 0 ];
if ( is_null( $resultPageSet ) ) {
// Try to add the result data in one go and pray that it fits
$code = $this->bl_code;
$data = array_map( function ( $arr ) use ( $result, $code ) {
if ( isset( $arr['redirlinks'] ) ) {
$arr['redirlinks'] = array_values( $arr['redirlinks'] );
ApiResult::setIndexedTagName( $arr['redirlinks'], $code );
}
return $arr;
}, array_values( $this->resultArr ) );
$fit = $result->addValue( 'query', $this->getModuleName(), $data );
if ( !$fit ) {
// It didn't fit. Add elements one by one until the
// result is full.
ksort( $this->resultArr );
if ( count( $this->cont ) >= 7 ) {
$startAt = $this->cont[6];
} else {
reset( $this->resultArr );
$startAt = key( $this->resultArr );
}
$idx = 0;
foreach ( $this->resultArr as $pageID => $arr ) {
if ( $pageID < $startAt ) {
continue;
}
// Add the basic entry without redirlinks first
$fit = $result->addValue(
[ 'query', $this->getModuleName() ],
$idx, array_diff_key( $arr, [ 'redirlinks' => '' ] ) );
if ( !$fit ) {
$this->continueStr = implode( '|', array_slice( $this->cont, 0, 6 ) ) .
"|$pageID";
break;
}
$hasRedirs = false;
$redirLinks = isset( $arr['redirlinks'] ) ? (array)$arr['redirlinks'] : [];
ksort( $redirLinks );
if ( count( $this->cont ) >= 8 && $pageID == $startAt ) {
$redirStartAt = $this->cont[7];
} else {
reset( $redirLinks );
$redirStartAt = key( $redirLinks );
}
foreach ( $redirLinks as $key => $redir ) {
if ( $key < $redirStartAt ) {
continue;
}
$fit = $result->addValue(
[ 'query', $this->getModuleName(), $idx, 'redirlinks' ],
null, $redir );
if ( !$fit ) {
$this->continueStr = implode( '|', array_slice( $this->cont, 0, 6 ) ) .
"|$pageID|$key";
break;
}
$hasRedirs = true;
}
if ( $hasRedirs ) {
$result->addIndexedTagName(
[ 'query', $this->getModuleName(), $idx, 'redirlinks' ],
$this->bl_code );
}
if ( !$fit ) {
break;
}
$idx++;
}
}
$result->addIndexedTagName(
[ 'query', $this->getModuleName() ],
$this->bl_code
);
}
if ( !is_null( $this->continueStr ) ) {
$this->setContinueEnumParameter( 'continue', $this->continueStr );
}
}
public function getAllowedParams() {
$retval = [
'title' => [
ApiBase::PARAM_TYPE => 'string',
],
'pageid' => [
ApiBase::PARAM_TYPE => 'integer',
],
'continue' => [
ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
],
'namespace' => [
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => 'namespace'
],
'dir' => [
ApiBase::PARAM_DFLT => 'ascending',
ApiBase::PARAM_TYPE => [
'ascending',
'descending'
]
],
'filterredir' => [
ApiBase::PARAM_DFLT => 'all',
ApiBase::PARAM_TYPE => [
'all',
'redirects',
'nonredirects'
]
],
'limit' => [
ApiBase::PARAM_DFLT => 10,
ApiBase::PARAM_TYPE => 'limit',
ApiBase::PARAM_MIN => 1,
ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
]
];
if ( $this->getModuleName() == 'embeddedin' ) {
return $retval;
}
$retval['redirect'] = false;
return $retval;
}
protected function getExamplesMessages() {
static $examples = [
'backlinks' => [
'action=query&list=backlinks&bltitle=Main%20Page'
=> 'apihelp-query+backlinks-example-simple',
'action=query&generator=backlinks&gbltitle=Main%20Page&prop=info'
=> 'apihelp-query+backlinks-example-generator',
],
'embeddedin' => [
'action=query&list=embeddedin&eititle=Template:Stub'
=> 'apihelp-query+embeddedin-example-simple',
'action=query&generator=embeddedin&geititle=Template:Stub&prop=info'
=> 'apihelp-query+embeddedin-example-generator',
],
'imageusage' => [
'action=query&list=imageusage&iutitle=File:Albert%20Einstein%20Head.jpg'
=> 'apihelp-query+imageusage-example-simple',
'action=query&generator=imageusage&giutitle=File:Albert%20Einstein%20Head.jpg&prop=info'
=> 'apihelp-query+imageusage-example-generator',
]
];
return $examples[$this->getModuleName()];
}
public function getHelpUrls() {
return $this->helpUrl;
}
}