3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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.
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.
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
23 namespace MediaWiki\Api
;
25 use MediaWiki\Cache\GenderCache
;
26 use MediaWiki\Linker\LinksMigration
;
27 use MediaWiki\ParamValidator\TypeDef\NamespaceDef
;
28 use MediaWiki\Title\NamespaceInfo
;
29 use MediaWiki\Title\Title
;
30 use Wikimedia\ParamValidator\ParamValidator
;
31 use Wikimedia\ParamValidator\TypeDef\IntegerDef
;
32 use Wikimedia\Rdbms\IExpression
;
33 use Wikimedia\Rdbms\LikeValue
;
36 * Query module to enumerate links from all pages together.
40 class ApiQueryAllLinks
extends ApiQueryGeneratorBase
{
42 private string $table;
43 private string $tablePrefix;
44 private string $indexTag;
46 private $fieldTitle = 'title';
48 private $dfltNamespace = NS_MAIN
;
50 private $hasNamespace = true;
51 /** @var string|null */
52 private $useIndex = null;
56 private NamespaceInfo
$namespaceInfo;
57 private GenderCache
$genderCache;
58 private LinksMigration
$linksMigration;
60 public function __construct(
63 NamespaceInfo
$namespaceInfo,
64 GenderCache
$genderCache,
65 LinksMigration
$linksMigration
67 switch ( $moduleName ) {
70 $this->table
= 'pagelinks';
71 $this->tablePrefix
= 'pl_';
72 $this->useIndex
= 'pl_namespace';
73 $this->indexTag
= 'l';
75 case 'alltransclusions':
77 $this->table
= 'templatelinks';
78 $this->tablePrefix
= 'tl_';
79 $this->dfltNamespace
= NS_TEMPLATE
;
80 $this->indexTag
= 't';
84 $this->table
= 'imagelinks';
85 $this->tablePrefix
= 'il_';
86 $this->fieldTitle
= 'to';
87 $this->dfltNamespace
= NS_FILE
;
88 $this->hasNamespace
= false;
89 $this->indexTag
= 'f';
93 $this->table
= 'redirect';
94 $this->tablePrefix
= 'rd_';
95 $this->indexTag
= 'r';
97 'fragment' => 'rd_fragment',
98 'interwiki' => 'rd_interwiki',
102 ApiBase
::dieDebug( __METHOD__
, 'Unknown module name' );
105 parent
::__construct( $query, $moduleName, $prefix );
106 $this->namespaceInfo
= $namespaceInfo;
107 $this->genderCache
= $genderCache;
108 $this->linksMigration
= $linksMigration;
111 public function execute() {
115 public function getCacheMode( $params ) {
119 public function executeGenerator( $resultPageSet ) {
120 $this->run( $resultPageSet );
124 * @param ApiPageSet|null $resultPageSet
127 private function run( $resultPageSet = null ) {
128 $db = $this->getDB();
129 $params = $this->extractRequestParams();
131 $pfx = $this->tablePrefix
;
133 $nsField = $pfx . 'namespace';
134 $titleField = $pfx . $this->fieldTitle
;
135 $linktargetReadNew = false;
136 $targetIdColumn = '';
137 if ( isset( $this->linksMigration
::$mapping[$this->table
] ) ) {
138 [ $nsField, $titleField ] = $this->linksMigration
->getTitleFields( $this->table
);
139 $queryInfo = $this->linksMigration
->getQueryInfo( $this->table
, 'linktarget', 'STRAIGHT_JOIN' );
140 $this->addTables( $queryInfo['tables'] );
141 $this->addJoinConds( $queryInfo['joins'] );
142 if ( in_array( 'linktarget', $queryInfo['tables'] ) ) {
143 $linktargetReadNew = true;
144 $targetIdColumn = "{$pfx}target_id";
145 $this->addFields( [ $targetIdColumn ] );
148 if ( $this->useIndex
) {
149 $this->addOption( 'USE INDEX', $this->useIndex
);
151 $this->addTables( $this->table
);
154 $prop = array_fill_keys( $params['prop'], true );
155 $fld_ids = isset( $prop['ids'] );
156 $fld_title = isset( $prop['title'] );
157 if ( $this->hasNamespace
) {
158 $namespace = $params['namespace'];
160 $namespace = $this->dfltNamespace
;
163 if ( $params['unique'] ) {
164 $matches = array_intersect_key( $prop, $this->props +
[ 'ids' => 1 ] );
166 $p = $this->getModulePrefix();
169 'apierror-invalidparammix-cannotusewith',
170 "{$p}prop=" . implode( '|', array_keys( $matches ) ),
176 $this->addOption( 'DISTINCT' );
179 if ( $this->hasNamespace
) {
180 $this->addWhereFld( $nsField, $namespace );
183 $continue = $params['continue'] !== null;
185 $op = $params['dir'] == 'descending' ?
'<=' : '>=';
186 if ( $params['unique'] ) {
187 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'string' ] );
188 $this->addWhere( $db->expr( $titleField, $op, $cont[0] ) );
189 } elseif ( !$linktargetReadNew ) {
190 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'string', 'int' ] );
191 $this->addWhere( $db->buildComparison( $op, [
192 $titleField => $cont[0],
193 "{$pfx}from" => $cont[1],
196 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'int' ] );
197 $this->addWhere( $db->buildComparison( $op, [
198 $targetIdColumn => $cont[0],
199 "{$pfx}from" => $cont[1],
204 // 'continue' always overrides 'from'
205 $from = $continue ||
$params['from'] === null ?
null :
206 $this->titlePartToKey( $params['from'], $namespace );
207 $to = $params['to'] === null ?
null :
208 $this->titlePartToKey( $params['to'], $namespace );
209 $this->addWhereRange( $titleField, 'newer', $from, $to );
211 if ( isset( $params['prefix'] ) ) {
216 new LikeValue( $this->titlePartToKey( $params['prefix'], $namespace ), $db->anyString() )
221 $this->addFields( [ 'pl_title' => $titleField ] );
222 $this->addFieldsIf( [ 'pl_from' => $pfx . 'from' ], !$params['unique'] );
223 foreach ( $this->props
as $name => $field ) {
224 $this->addFieldsIf( $field, isset( $prop[$name] ) );
227 $limit = $params['limit'];
228 $this->addOption( 'LIMIT', $limit +
1 );
230 $sort = ( $params['dir'] == 'descending' ?
' DESC' : '' );
232 if ( $linktargetReadNew ) {
233 $orderBy[] = $targetIdColumn;
235 $orderBy[] = $titleField . $sort;
237 if ( !$params['unique'] ) {
238 $orderBy[] = $pfx . 'from' . $sort;
240 $this->addOption( 'ORDER BY', $orderBy );
242 $res = $this->select( __METHOD__
);
244 // Get gender information
245 if ( $resultPageSet === null && $res->numRows() && $this->namespaceInfo
->hasGenderDistinction( $namespace ) ) {
247 foreach ( $res as $row ) {
248 $users[] = $row->pl_title
;
250 if ( $users !== [] ) {
251 $this->genderCache
->doQuery( $users, __METHOD__
);
258 $result = $this->getResult();
260 foreach ( $res as $row ) {
261 if ( ++
$count > $limit ) {
262 // We've reached the one extra which shows that there are
263 // additional pages to be had. Stop here...
264 if ( $params['unique'] ) {
265 $this->setContinueEnumParameter( 'continue', $row->pl_title
);
266 } elseif ( $linktargetReadNew ) {
267 $this->setContinueEnumParameter( 'continue', $row->{$targetIdColumn} . '|' . $row->pl_from
);
269 $this->setContinueEnumParameter( 'continue', $row->pl_title
. '|' . $row->pl_from
);
274 if ( $resultPageSet === null ) {
276 ApiResult
::META_TYPE
=> 'assoc',
279 $vals['fromid'] = (int)$row->pl_from
;
282 $title = Title
::makeTitle( $namespace, $row->pl_title
);
283 ApiQueryBase
::addTitleInfo( $vals, $title );
285 foreach ( $this->props
as $name => $field ) {
286 if ( isset( $prop[$name] ) && $row->$field !== null && $row->$field !== '' ) {
287 $vals[$name] = $row->$field;
290 $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
292 if ( $params['unique'] ) {
293 $this->setContinueEnumParameter( 'continue', $row->pl_title
);
294 } elseif ( $linktargetReadNew ) {
295 $this->setContinueEnumParameter( 'continue', $row->{$targetIdColumn} . '|' . $row->pl_from
);
297 $this->setContinueEnumParameter( 'continue', $row->pl_title
. '|' . $row->pl_from
);
301 } elseif ( $params['unique'] ) {
302 $titles[] = Title
::makeTitle( $namespace, $row->pl_title
);
304 $pageids[] = $row->pl_from
;
308 if ( $resultPageSet === null ) {
309 $result->addIndexedTagName( [ 'query', $this->getModuleName() ], $this->indexTag
);
310 } elseif ( $params['unique'] ) {
311 $resultPageSet->populateFromTitles( $titles );
313 $resultPageSet->populateFromPageIDs( $pageids );
317 public function getAllowedParams() {
320 ApiBase
::PARAM_HELP_MSG
=> 'api-help-param-continue',
327 ParamValidator
::PARAM_ISMULTI
=> true,
328 ParamValidator
::PARAM_DEFAULT
=> 'title',
329 ParamValidator
::PARAM_TYPE
=> array_merge(
330 [ 'ids', 'title' ], array_keys( $this->props
)
332 ApiBase
::PARAM_HELP_MSG_PER_VALUE
=> [],
335 ParamValidator
::PARAM_DEFAULT
=> $this->dfltNamespace
,
336 ParamValidator
::PARAM_TYPE
=> 'namespace',
337 NamespaceDef
::PARAM_EXTRA_NAMESPACES
=> [ NS_MEDIA
, NS_SPECIAL
],
340 ParamValidator
::PARAM_DEFAULT
=> 10,
341 ParamValidator
::PARAM_TYPE
=> 'limit',
342 IntegerDef
::PARAM_MIN
=> 1,
343 IntegerDef
::PARAM_MAX
=> ApiBase
::LIMIT_BIG1
,
344 IntegerDef
::PARAM_MAX2
=> ApiBase
::LIMIT_BIG2
347 ParamValidator
::PARAM_DEFAULT
=> 'ascending',
348 ParamValidator
::PARAM_TYPE
=> [
354 if ( !$this->hasNamespace
) {
355 unset( $allowedParams['namespace'] );
358 return $allowedParams;
361 protected function getExamplesMessages() {
362 $p = $this->getModulePrefix();
363 $name = $this->getModuleName();
364 $path = $this->getModulePath();
367 "action=query&list={$name}&{$p}from=B&{$p}prop=ids|title"
368 => "apihelp-$path-example-b",
369 "action=query&list={$name}&{$p}unique=&{$p}from=B"
370 => "apihelp-$path-example-unique",
371 "action=query&generator={$name}&g{$p}unique=&g{$p}from=B"
372 => "apihelp-$path-example-unique-generator",
373 "action=query&generator={$name}&g{$p}from=B"
374 => "apihelp-$path-example-generator",
378 public function getHelpUrls() {
379 $name = ucfirst( $this->getModuleName() );
381 return "https://www.mediawiki.org/wiki/Special:MyLanguage/API:{$name}";
385 /** @deprecated class alias since 1.43 */
386 class_alias( ApiQueryAllLinks
::class, 'ApiQueryAllLinks' );