Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.52% covered (warning)
70.52%
189 / 268
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryAllPages
70.79% covered (warning)
70.79%
189 / 267
37.50% covered (danger)
37.50%
3 / 8
192.16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCacheMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 executeGenerator
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 run
63.58% covered (warning)
63.58%
110 / 173
0.00% covered (danger)
0.00%
0 / 1
240.70
 getAllowedParams
97.44% covered (success)
97.44%
76 / 78
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\Cache\GenderCache;
12use MediaWiki\Deferred\LinksUpdate\LangLinksTable;
13use MediaWiki\MainConfigNames;
14use MediaWiki\Permissions\RestrictionStore;
15use MediaWiki\Title\NamespaceInfo;
16use MediaWiki\Title\Title;
17use Wikimedia\ParamValidator\ParamValidator;
18use Wikimedia\ParamValidator\TypeDef\IntegerDef;
19use Wikimedia\Rdbms\IExpression;
20use Wikimedia\Rdbms\LikeValue;
21
22/**
23 * Query module to enumerate all available pages.
24 *
25 * @ingroup API
26 */
27class ApiQueryAllPages extends ApiQueryGeneratorBase {
28
29    public function __construct(
30        ApiQuery $query,
31        string $moduleName,
32        private readonly NamespaceInfo $namespaceInfo,
33        private readonly GenderCache $genderCache,
34        private readonly RestrictionStore $restrictionStore,
35    ) {
36        parent::__construct( $query, $moduleName, 'ap' );
37    }
38
39    public function execute() {
40        $this->run();
41    }
42
43    /** @inheritDoc */
44    public function getCacheMode( $params ) {
45        return 'public';
46    }
47
48    /**
49     * @param ApiPageSet $resultPageSet
50     * @return void
51     */
52    public function executeGenerator( $resultPageSet ) {
53        if ( $resultPageSet->isResolvingRedirects() ) {
54            $this->dieWithError( 'apierror-allpages-generator-redirects', 'params' );
55        }
56
57        $this->run( $resultPageSet );
58    }
59
60    /**
61     * @param ApiPageSet|null $resultPageSet
62     * @return void
63     */
64    private function run( $resultPageSet = null ) {
65        $db = $this->getDB();
66        $this->setVirtualDomain( LangLinksTable::VIRTUAL_DOMAIN );
67        $langLinksDb = $this->getDB();
68
69        $params = $this->extractRequestParams();
70
71        $miserMode = $this->getConfig()->get( MainConfigNames::MiserMode );
72
73        if ( $resultPageSet === null ) {
74            $selectFields = [
75                'page_namespace',
76                'page_title',
77                'page_id'
78            ];
79        } else {
80            $selectFields = $resultPageSet->getPageTableFields();
81        }
82
83        $miserModeFilterRedirValue = null;
84        $miserModeFilterRedir = $miserMode && $params['filterredir'] !== 'all';
85        if ( $miserModeFilterRedir ) {
86            $selectFields[] = 'page_is_redirect';
87
88            if ( $params['filterredir'] == 'redirects' ) {
89                $miserModeFilterRedirValue = 1;
90            } elseif ( $params['filterredir'] == 'nonredirects' ) {
91                $miserModeFilterRedirValue = 0;
92            }
93        }
94
95        $forceNameTitleIndex = !(
96            isset( $params['minsize'] ) ||
97            ( !$miserMode && isset( $params['maxsize'] ) ) ||
98            ( $params['prtype'] || $params['prexpiry'] != 'all' )
99        );
100
101        $limit = $params['limit'];
102
103        // Use loop-based query to handle langlinks filtering separately
104        // (langlinks table is in a different cluster)
105        $filteredRows = [];
106        $pagesWithLangLinks = [];
107        $filterWithLangLinks = $params['filterlanglinks'] == 'withlanglinks';
108        $needLangLinksFilter = $params['filterlanglinks'] !== 'all';
109
110        $continueFrom = null;
111        if ( $params['continue'] !== null ) {
112            $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'string' ] );
113            $continueFrom = $cont[0];
114        }
115
116        $isFirstBatch = true;
117        $loopCount = 0;
118        $maxLoops = 20;
119        while ( count( $filteredRows ) < $limit + 1 ) {
120            if ( $loopCount > $maxLoops ) {
121                // Safety limit to prevent excessive iterations
122                break;
123            }
124
125            $queryBuilder = $db->newSelectQueryBuilder()
126                ->select( $selectFields )
127                ->from( 'page' )
128                ->where( [ 'page_namespace' => $params['namespace'] ] )
129                ->caller( __METHOD__ );
130
131            if ( $continueFrom !== null ) {
132                // Use strict comparison for subsequent batches to skip the continue row
133                if ( $isFirstBatch ) {
134                    $op = $params['dir'] == 'descending' ? '<=' : '>=';
135                } else {
136                    $op = $params['dir'] == 'descending' ? '<' : '>';
137                }
138                $queryBuilder->andWhere( $db->expr( 'page_title', $op, $continueFrom ) );
139            }
140
141            $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
142            $from = ( $params['from'] === null
143                ? null
144                : $this->titlePartToKey( $params['from'], $params['namespace'] ) );
145            $to = ( $params['to'] === null
146                ? null
147                : $this->titlePartToKey( $params['to'], $params['namespace'] ) );
148
149            if ( $dir === 'newer' ) {
150                if ( $from !== null ) {
151                    $queryBuilder->andWhere( $db->expr( 'page_title', '>=', $from ) );
152                }
153                if ( $to !== null ) {
154                    $queryBuilder->andWhere( $db->expr( 'page_title', '<=', $to ) );
155                }
156            } else {
157                if ( $from !== null ) {
158                    $queryBuilder->andWhere( $db->expr( 'page_title', '<=', $from ) );
159                }
160                if ( $to !== null ) {
161                    $queryBuilder->andWhere( $db->expr( 'page_title', '>=', $to ) );
162                }
163            }
164
165            if ( isset( $params['prefix'] ) ) {
166                $queryBuilder->andWhere(
167                    $db->expr(
168                        'page_title',
169                        IExpression::LIKE,
170                        new LikeValue(
171                            $this->titlePartToKey( $params['prefix'], $params['namespace'] ),
172                            $db->anyString()
173                        )
174                    )
175                );
176            }
177
178            if ( !$miserMode ) {
179                if ( $params['filterredir'] == 'redirects' ) {
180                    $queryBuilder->andWhere( [ 'page_is_redirect' => 1 ] );
181                } elseif ( $params['filterredir'] == 'nonredirects' ) {
182                    $queryBuilder->andWhere( [ 'page_is_redirect' => 0 ] );
183                }
184            }
185
186            if ( isset( $params['minsize'] ) ) {
187                $queryBuilder->andWhere( $db->expr( 'page_len', '>=', (int)$params['minsize'] ) );
188            }
189
190            if ( !$miserMode && isset( $params['maxsize'] ) ) {
191                $queryBuilder->andWhere( $db->expr( 'page_len', '<=', (int)$params['maxsize'] ) );
192            }
193
194            // Page protection filtering
195            if ( $params['prtype'] || $params['prexpiry'] != 'all' ) {
196                $queryBuilder->join( 'page_restrictions', null, 'page_id=pr_page' );
197                $queryBuilder->andWhere(
198                    $db->expr( 'pr_expiry', '>', $db->timestamp() )->or( 'pr_expiry', '=', null )
199                );
200
201                if ( $params['prtype'] ) {
202                    $queryBuilder->andWhere( [ 'pr_type' => $params['prtype'] ] );
203
204                    if ( isset( $params['prlevel'] ) ) {
205                        $prlevel = array_diff( $params['prlevel'], [ '', '*' ] );
206                        if ( count( $prlevel ) ) {
207                            $queryBuilder->andWhere( [ 'pr_level' => $prlevel ] );
208                        }
209                    }
210                    if ( $params['prfiltercascade'] == 'cascading' ) {
211                        $queryBuilder->andWhere( [ 'pr_cascade' => 1 ] );
212                    } elseif ( $params['prfiltercascade'] == 'noncascading' ) {
213                        $queryBuilder->andWhere( [ 'pr_cascade' => 0 ] );
214                    }
215                }
216
217                if ( $params['prexpiry'] == 'indefinite' ) {
218                    $queryBuilder->andWhere( [ 'pr_expiry' => [ $db->getInfinity(), null ] ] );
219                } elseif ( $params['prexpiry'] == 'definite' ) {
220                    $queryBuilder->andWhere( $db->expr( 'pr_expiry', '!=', $db->getInfinity() ) );
221                }
222
223                $queryBuilder->distinct();
224            } elseif ( isset( $params['prlevel'] ) ) {
225                $this->dieWithError(
226                    [ 'apierror-invalidparammix-mustusewith', 'prlevel', 'prtype' ], 'invalidparammix'
227                );
228            }
229
230            if ( $forceNameTitleIndex ) {
231                $queryBuilder->useIndex( 'page_name_title' );
232            }
233
234            $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
235            $queryBuilder->orderBy( 'page_title' . $sort );
236            $queryBuilder->limit( $limit + 1 );
237
238            $res = $queryBuilder->fetchResultSet();
239
240            $isFirstBatch = false;
241
242            $batchRows = [];
243            $pageIds = [];
244
245            foreach ( $res as $row ) {
246                $batchRows[] = $row;
247                $pageIds[] = $row->page_id;
248            }
249
250            if ( $pageIds === [] ) {
251                // No more rows available
252                break;
253            }
254
255            if ( $needLangLinksFilter ) {
256                $langLinksQueryBuilder = $langLinksDb->newSelectQueryBuilder()
257                    ->select( 'll_from' )
258                    ->from( 'langlinks' )
259                    ->where( [ 'll_from' => $pageIds ] )
260                    ->caller( __METHOD__ );
261
262                $langLinksRes = $langLinksQueryBuilder->fetchResultSet();
263
264                foreach ( $langLinksRes as $langLinksRow ) {
265                    $pagesWithLangLinks[$langLinksRow->ll_from] = true;
266                }
267            }
268
269            foreach ( $batchRows as $row ) {
270                if ( $needLangLinksFilter ) {
271                    $hasLangLinks = isset( $pagesWithLangLinks[$row->page_id] );
272                    if ( $filterWithLangLinks === $hasLangLinks ) {
273                        $filteredRows[] = $row;
274                    }
275                } else {
276                    $filteredRows[] = $row;
277                }
278            }
279
280            if ( count( $batchRows ) < $limit + 1 ) {
281                break;
282            }
283
284            $lastRow = end( $batchRows );
285            $continueFrom = $lastRow->page_title;
286
287            $loopCount++;
288        }
289
290        $this->resetVirtualDomain();
291
292        $res = $filteredRows;
293
294        // Get gender information
295        if ( $this->namespaceInfo->hasGenderDistinction( $params['namespace'] ) ) {
296            $users = [];
297            foreach ( $res as $row ) {
298                $users[] = $row->page_title;
299            }
300            $this->genderCache->doQuery( $users, __METHOD__ );
301        }
302
303        $count = 0;
304        $result = $this->getResult();
305        foreach ( $res as $row ) {
306            if ( ++$count > $limit ) {
307                // We've reached the one extra which shows that there are
308                // additional pages to be had. Stop here...
309                $this->setContinueEnumParameter( 'continue', $row->page_title );
310                break;
311            }
312
313            if ( $miserModeFilterRedir && (int)$row->page_is_redirect !== $miserModeFilterRedirValue ) {
314                // Filter implemented in PHP due to being in Miser Mode
315                continue;
316            }
317
318            if ( $resultPageSet === null ) {
319                $title = Title::makeTitle( $row->page_namespace, $row->page_title );
320                $vals = [
321                    'pageid' => (int)$row->page_id,
322                    'ns' => $title->getNamespace(),
323                    'title' => $title->getPrefixedText()
324                ];
325                $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
326                if ( !$fit ) {
327                    $this->setContinueEnumParameter( 'continue', $row->page_title );
328                    break;
329                }
330            } else {
331                $resultPageSet->processDbRow( $row );
332            }
333        }
334
335        if ( $resultPageSet === null ) {
336            $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'p' );
337        }
338    }
339
340    /** @inheritDoc */
341    public function getAllowedParams() {
342        $ret = [
343            'from' => null,
344            'continue' => [
345                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
346            ],
347            'to' => null,
348            'prefix' => null,
349            'namespace' => [
350                ParamValidator::PARAM_DEFAULT => NS_MAIN,
351                ParamValidator::PARAM_TYPE => 'namespace',
352            ],
353            'filterredir' => [
354                ParamValidator::PARAM_DEFAULT => 'all',
355                ParamValidator::PARAM_TYPE => [
356                    'all',
357                    'redirects',
358                    'nonredirects'
359                ]
360            ],
361            'filterlanglinks' => [
362                ParamValidator::PARAM_TYPE => [
363                    'withlanglinks',
364                    'withoutlanglinks',
365                    'all'
366                ],
367                ParamValidator::PARAM_DEFAULT => 'all'
368            ],
369            'minsize' => [
370                ParamValidator::PARAM_TYPE => 'integer',
371            ],
372            'maxsize' => [
373                ParamValidator::PARAM_TYPE => 'integer',
374            ],
375            'prtype' => [
376                ParamValidator::PARAM_TYPE => $this->restrictionStore->listAllRestrictionTypes( true ),
377                ParamValidator::PARAM_ISMULTI => true
378            ],
379            'prlevel' => [
380                ParamValidator::PARAM_TYPE =>
381                    $this->getConfig()->get( MainConfigNames::RestrictionLevels ),
382                ParamValidator::PARAM_ISMULTI => true
383            ],
384            'prfiltercascade' => [
385                ParamValidator::PARAM_DEFAULT => 'all',
386                ParamValidator::PARAM_TYPE => [
387                    'cascading',
388                    'noncascading',
389                    'all'
390                ],
391            ],
392            'prexpiry' => [
393                ParamValidator::PARAM_TYPE => [
394                    'indefinite',
395                    'definite',
396                    'all'
397                ],
398                ParamValidator::PARAM_DEFAULT => 'all',
399                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
400            ],
401            'limit' => [
402                ParamValidator::PARAM_DEFAULT => 10,
403                ParamValidator::PARAM_TYPE => 'limit',
404                IntegerDef::PARAM_MIN => 1,
405                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
406                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
407            ],
408            'dir' => [
409                ParamValidator::PARAM_DEFAULT => 'ascending',
410                ParamValidator::PARAM_TYPE => [
411                    'ascending',
412                    'descending'
413                ]
414            ],
415        ];
416
417        if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
418            $ret['filterredir'][ApiBase::PARAM_HELP_MSG_APPEND] = [ 'api-help-param-limited-in-miser-mode' ];
419            $ret['maxsize'][ApiBase::PARAM_HELP_MSG_APPEND] = [ 'api-help-param-disabled-in-miser-mode' ];
420        }
421
422        return $ret;
423    }
424
425    /** @inheritDoc */
426    protected function getExamplesMessages() {
427        return [
428            'action=query&list=allpages&apfrom=B'
429                => 'apihelp-query+allpages-example-b',
430            'action=query&generator=allpages&gaplimit=4&gapfrom=T&prop=info'
431                => 'apihelp-query+allpages-example-generator',
432            'action=query&generator=allpages&gaplimit=2&' .
433                'gapfilterredir=nonredirects&gapfrom=Re&prop=revisions&rvprop=content'
434                => 'apihelp-query+allpages-example-generator-revisions',
435        ];
436    }
437
438    /** @inheritDoc */
439    public function getHelpUrls() {
440        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allpages';
441    }
442}
443
444/** @deprecated class alias since 1.43 */
445class_alias( ApiQueryAllPages::class, 'ApiQueryAllPages' );