Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 227
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryAllLinks
0.00% covered (danger)
0.00%
0 / 226
0.00% covered (danger)
0.00%
0 / 8
3080
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
42
 execute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCacheMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 executeGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 run
0.00% covered (danger)
0.00%
0 / 131
0.00% covered (danger)
0.00%
0 / 1
1806
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
6
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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 */
22
23namespace MediaWiki\Api;
24
25use MediaWiki\Cache\GenderCache;
26use MediaWiki\Linker\LinksMigration;
27use MediaWiki\ParamValidator\TypeDef\NamespaceDef;
28use MediaWiki\Title\NamespaceInfo;
29use MediaWiki\Title\Title;
30use Wikimedia\ParamValidator\ParamValidator;
31use Wikimedia\ParamValidator\TypeDef\IntegerDef;
32use Wikimedia\Rdbms\IExpression;
33use Wikimedia\Rdbms\LikeValue;
34
35/**
36 * Query module to enumerate links from all pages together.
37 *
38 * @ingroup API
39 */
40class ApiQueryAllLinks extends ApiQueryGeneratorBase {
41
42    private string $table;
43    private string $tablePrefix;
44    private string $indexTag;
45    /** @var string */
46    private $fieldTitle = 'title';
47    /** @var int */
48    private $dfltNamespace = NS_MAIN;
49    /** @var bool */
50    private $hasNamespace = true;
51    /** @var string|null */
52    private $useIndex = null;
53    /** @var array */
54    private $props = [];
55
56    private NamespaceInfo $namespaceInfo;
57    private GenderCache $genderCache;
58    private LinksMigration $linksMigration;
59
60    public function __construct(
61        ApiQuery $query,
62        string $moduleName,
63        NamespaceInfo $namespaceInfo,
64        GenderCache $genderCache,
65        LinksMigration $linksMigration
66    ) {
67        switch ( $moduleName ) {
68            case 'alllinks':
69                $prefix = 'al';
70                $this->table = 'pagelinks';
71                $this->tablePrefix = 'pl_';
72                $this->useIndex = 'pl_namespace';
73                $this->indexTag = 'l';
74                break;
75            case 'alltransclusions':
76                $prefix = 'at';
77                $this->table = 'templatelinks';
78                $this->tablePrefix = 'tl_';
79                $this->dfltNamespace = NS_TEMPLATE;
80                $this->indexTag = 't';
81                break;
82            case 'allfileusages':
83                $prefix = 'af';
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';
90                break;
91            case 'allredirects':
92                $prefix = 'ar';
93                $this->table = 'redirect';
94                $this->tablePrefix = 'rd_';
95                $this->indexTag = 'r';
96                $this->props = [
97                    'fragment' => 'rd_fragment',
98                    'interwiki' => 'rd_interwiki',
99                ];
100                break;
101            default:
102                ApiBase::dieDebug( __METHOD__, 'Unknown module name' );
103        }
104
105        parent::__construct( $query, $moduleName, $prefix );
106        $this->namespaceInfo = $namespaceInfo;
107        $this->genderCache = $genderCache;
108        $this->linksMigration = $linksMigration;
109    }
110
111    public function execute() {
112        $this->run();
113    }
114
115    public function getCacheMode( $params ) {
116        return 'public';
117    }
118
119    public function executeGenerator( $resultPageSet ) {
120        $this->run( $resultPageSet );
121    }
122
123    /**
124     * @param ApiPageSet|null $resultPageSet
125     * @return void
126     */
127    private function run( $resultPageSet = null ) {
128        $db = $this->getDB();
129        $params = $this->extractRequestParams();
130
131        $pfx = $this->tablePrefix;
132
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 ] );
146            }
147        } else {
148            if ( $this->useIndex ) {
149                $this->addOption( 'USE INDEX', $this->useIndex );
150            }
151            $this->addTables( $this->table );
152        }
153
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'];
159        } else {
160            $namespace = $this->dfltNamespace;
161        }
162
163        if ( $params['unique'] ) {
164            $matches = array_intersect_key( $prop, $this->props + [ 'ids' => 1 ] );
165            if ( $matches ) {
166                $p = $this->getModulePrefix();
167                $this->dieWithError(
168                    [
169                        'apierror-invalidparammix-cannotusewith',
170                        "{$p}prop=" . implode( '|', array_keys( $matches ) ),
171                        "{$p}unique"
172                    ],
173                    'invalidparammix'
174                );
175            }
176            $this->addOption( 'DISTINCT' );
177        }
178
179        if ( $this->hasNamespace ) {
180            $this->addWhereFld( $nsField, $namespace );
181        }
182
183        $continue = $params['continue'] !== null;
184        if ( $continue ) {
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],
194                ] ) );
195            } else {
196                $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'int' ] );
197                $this->addWhere( $db->buildComparison( $op, [
198                    $targetIdColumn => $cont[0],
199                    "{$pfx}from" => $cont[1],
200                ] ) );
201            }
202        }
203
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 );
210
211        if ( isset( $params['prefix'] ) ) {
212            $this->addWhere(
213                $db->expr(
214                    $titleField,
215                    IExpression::LIKE,
216                    new LikeValue( $this->titlePartToKey( $params['prefix'], $namespace ), $db->anyString() )
217                )
218            );
219        }
220
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] ) );
225        }
226
227        $limit = $params['limit'];
228        $this->addOption( 'LIMIT', $limit + 1 );
229
230        $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
231        $orderBy = [];
232        if ( $linktargetReadNew ) {
233            $orderBy[] = $targetIdColumn;
234        } else {
235            $orderBy[] = $titleField . $sort;
236        }
237        if ( !$params['unique'] ) {
238            $orderBy[] = $pfx . 'from' . $sort;
239        }
240        $this->addOption( 'ORDER BY', $orderBy );
241
242        $res = $this->select( __METHOD__ );
243
244        // Get gender information
245        if ( $resultPageSet === null && $res->numRows() && $this->namespaceInfo->hasGenderDistinction( $namespace ) ) {
246            $users = [];
247            foreach ( $res as $row ) {
248                $users[] = $row->pl_title;
249            }
250            if ( $users !== [] ) {
251                $this->genderCache->doQuery( $users, __METHOD__ );
252            }
253        }
254
255        $pageids = [];
256        $titles = [];
257        $count = 0;
258        $result = $this->getResult();
259
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 );
268                } else {
269                    $this->setContinueEnumParameter( 'continue', $row->pl_title . '|' . $row->pl_from );
270                }
271                break;
272            }
273
274            if ( $resultPageSet === null ) {
275                $vals = [
276                    ApiResult::META_TYPE => 'assoc',
277                ];
278                if ( $fld_ids ) {
279                    $vals['fromid'] = (int)$row->pl_from;
280                }
281                if ( $fld_title ) {
282                    $title = Title::makeTitle( $namespace, $row->pl_title );
283                    ApiQueryBase::addTitleInfo( $vals, $title );
284                }
285                foreach ( $this->props as $name => $field ) {
286                    if ( isset( $prop[$name] ) && $row->$field !== null && $row->$field !== '' ) {
287                        $vals[$name] = $row->$field;
288                    }
289                }
290                $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
291                if ( !$fit ) {
292                    if ( $params['unique'] ) {
293                        $this->setContinueEnumParameter( 'continue', $row->pl_title );
294                    } elseif ( $linktargetReadNew ) {
295                        $this->setContinueEnumParameter( 'continue', $row->{$targetIdColumn} . '|' . $row->pl_from );
296                    } else {
297                        $this->setContinueEnumParameter( 'continue', $row->pl_title . '|' . $row->pl_from );
298                    }
299                    break;
300                }
301            } elseif ( $params['unique'] ) {
302                $titles[] = Title::makeTitle( $namespace, $row->pl_title );
303            } else {
304                $pageids[] = $row->pl_from;
305            }
306        }
307
308        if ( $resultPageSet === null ) {
309            $result->addIndexedTagName( [ 'query', $this->getModuleName() ], $this->indexTag );
310        } elseif ( $params['unique'] ) {
311            $resultPageSet->populateFromTitles( $titles );
312        } else {
313            $resultPageSet->populateFromPageIDs( $pageids );
314        }
315    }
316
317    public function getAllowedParams() {
318        $allowedParams = [
319            'continue' => [
320                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
321            ],
322            'from' => null,
323            'to' => null,
324            'prefix' => null,
325            'unique' => false,
326            'prop' => [
327                ParamValidator::PARAM_ISMULTI => true,
328                ParamValidator::PARAM_DEFAULT => 'title',
329                ParamValidator::PARAM_TYPE => array_merge(
330                    [ 'ids', 'title' ], array_keys( $this->props )
331                ),
332                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
333            ],
334            'namespace' => [
335                ParamValidator::PARAM_DEFAULT => $this->dfltNamespace,
336                ParamValidator::PARAM_TYPE => 'namespace',
337                NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ],
338            ],
339            'limit' => [
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
345            ],
346            'dir' => [
347                ParamValidator::PARAM_DEFAULT => 'ascending',
348                ParamValidator::PARAM_TYPE => [
349                    'ascending',
350                    'descending'
351                ]
352            ],
353        ];
354        if ( !$this->hasNamespace ) {
355            unset( $allowedParams['namespace'] );
356        }
357
358        return $allowedParams;
359    }
360
361    protected function getExamplesMessages() {
362        $p = $this->getModulePrefix();
363        $name = $this->getModuleName();
364        $path = $this->getModulePath();
365
366        return [
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",
375        ];
376    }
377
378    public function getHelpUrls() {
379        $name = ucfirst( $this->getModuleName() );
380
381        return "https://www.mediawiki.org/wiki/Special:MyLanguage/API:{$name}";
382    }
383}
384
385/** @deprecated class alias since 1.43 */
386class_alias( ApiQueryAllLinks::class, 'ApiQueryAllLinks' );