Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
64.29% covered (warning)
64.29%
144 / 224
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryAllPages
64.57% covered (warning)
64.57%
144 / 223
37.50% covered (danger)
37.50%
3 / 8
166.64
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
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
49.21% covered (danger)
49.21%
62 / 126
0.00% covered (danger)
0.00%
0 / 1
273.17
 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\MainConfigNames;
13use MediaWiki\Permissions\RestrictionStore;
14use MediaWiki\Title\NamespaceInfo;
15use MediaWiki\Title\Title;
16use Wikimedia\ParamValidator\ParamValidator;
17use Wikimedia\ParamValidator\TypeDef\IntegerDef;
18use Wikimedia\Rdbms\IExpression;
19use Wikimedia\Rdbms\LikeValue;
20
21/**
22 * Query module to enumerate all available pages.
23 *
24 * @ingroup API
25 */
26class ApiQueryAllPages extends ApiQueryGeneratorBase {
27
28    private NamespaceInfo $namespaceInfo;
29    private GenderCache $genderCache;
30    private RestrictionStore $restrictionStore;
31
32    public function __construct(
33        ApiQuery $query,
34        string $moduleName,
35        NamespaceInfo $namespaceInfo,
36        GenderCache $genderCache,
37        RestrictionStore $restrictionStore
38    ) {
39        parent::__construct( $query, $moduleName, 'ap' );
40        $this->namespaceInfo = $namespaceInfo;
41        $this->genderCache = $genderCache;
42        $this->restrictionStore = $restrictionStore;
43    }
44
45    public function execute() {
46        $this->run();
47    }
48
49    /** @inheritDoc */
50    public function getCacheMode( $params ) {
51        return 'public';
52    }
53
54    /**
55     * @param ApiPageSet $resultPageSet
56     * @return void
57     */
58    public function executeGenerator( $resultPageSet ) {
59        if ( $resultPageSet->isResolvingRedirects() ) {
60            $this->dieWithError( 'apierror-allpages-generator-redirects', 'params' );
61        }
62
63        $this->run( $resultPageSet );
64    }
65
66    /**
67     * @param ApiPageSet|null $resultPageSet
68     * @return void
69     */
70    private function run( $resultPageSet = null ) {
71        $db = $this->getDB();
72
73        $params = $this->extractRequestParams();
74
75        // Page filters
76        $this->addTables( 'page' );
77
78        if ( $params['continue'] !== null ) {
79            $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'string' ] );
80            $op = $params['dir'] == 'descending' ? '<=' : '>=';
81            $this->addWhere( $db->expr( 'page_title', $op, $cont[0] ) );
82        }
83
84        $miserMode = $this->getConfig()->get( MainConfigNames::MiserMode );
85        if ( !$miserMode ) {
86            if ( $params['filterredir'] == 'redirects' ) {
87                $this->addWhereFld( 'page_is_redirect', 1 );
88            } elseif ( $params['filterredir'] == 'nonredirects' ) {
89                $this->addWhereFld( 'page_is_redirect', 0 );
90            }
91        }
92
93        $this->addWhereFld( 'page_namespace', $params['namespace'] );
94        $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
95        $from = ( $params['from'] === null
96            ? null
97            : $this->titlePartToKey( $params['from'], $params['namespace'] ) );
98        $to = ( $params['to'] === null
99            ? null
100            : $this->titlePartToKey( $params['to'], $params['namespace'] ) );
101        $this->addWhereRange( 'page_title', $dir, $from, $to );
102
103        if ( isset( $params['prefix'] ) ) {
104            $this->addWhere(
105                $db->expr(
106                    'page_title',
107                    IExpression::LIKE,
108                    new LikeValue( $this->titlePartToKey( $params['prefix'], $params['namespace'] ), $db->anyString() )
109                )
110            );
111        }
112
113        if ( $resultPageSet === null ) {
114            $selectFields = [
115                'page_namespace',
116                'page_title',
117                'page_id'
118            ];
119        } else {
120            $selectFields = $resultPageSet->getPageTableFields();
121        }
122
123        $miserModeFilterRedirValue = null;
124        $miserModeFilterRedir = $miserMode && $params['filterredir'] !== 'all';
125        if ( $miserModeFilterRedir ) {
126            $selectFields[] = 'page_is_redirect';
127
128            if ( $params['filterredir'] == 'redirects' ) {
129                $miserModeFilterRedirValue = 1;
130            } elseif ( $params['filterredir'] == 'nonredirects' ) {
131                $miserModeFilterRedirValue = 0;
132            }
133        }
134
135        $this->addFields( $selectFields );
136        $forceNameTitleIndex = true;
137        if ( isset( $params['minsize'] ) ) {
138            $this->addWhere( 'page_len>=' . (int)$params['minsize'] );
139            $forceNameTitleIndex = false;
140        }
141
142        if ( !$miserMode && isset( $params['maxsize'] ) ) {
143            $this->addWhere( 'page_len<=' . (int)$params['maxsize'] );
144            $forceNameTitleIndex = false;
145        }
146
147        // Page protection filtering
148        if ( $params['prtype'] || $params['prexpiry'] != 'all' ) {
149            $this->addTables( 'page_restrictions' );
150            $this->addWhere( 'page_id=pr_page' );
151            $this->addWhere(
152                $db->expr( 'pr_expiry', '>', $db->timestamp() )->or( 'pr_expiry', '=', null )
153            );
154
155            if ( $params['prtype'] ) {
156                $this->addWhereFld( 'pr_type', $params['prtype'] );
157
158                if ( isset( $params['prlevel'] ) ) {
159                    // Remove the empty string and '*' from the prlevel array
160                    $prlevel = array_diff( $params['prlevel'], [ '', '*' ] );
161
162                    if ( count( $prlevel ) ) {
163                        $this->addWhereFld( 'pr_level', $prlevel );
164                    }
165                }
166                if ( $params['prfiltercascade'] == 'cascading' ) {
167                    $this->addWhereFld( 'pr_cascade', 1 );
168                } elseif ( $params['prfiltercascade'] == 'noncascading' ) {
169                    $this->addWhereFld( 'pr_cascade', 0 );
170                }
171            }
172            $forceNameTitleIndex = false;
173
174            if ( $params['prexpiry'] == 'indefinite' ) {
175                $this->addWhereFld( 'pr_expiry', [ $db->getInfinity(), null ] );
176            } elseif ( $params['prexpiry'] == 'definite' ) {
177                $this->addWhere( $db->expr( 'pr_expiry', '!=', $db->getInfinity() ) );
178            }
179
180            $this->addOption( 'DISTINCT' );
181        } elseif ( isset( $params['prlevel'] ) ) {
182            $this->dieWithError(
183                [ 'apierror-invalidparammix-mustusewith', 'prlevel', 'prtype' ], 'invalidparammix'
184            );
185        }
186
187        if ( $params['filterlanglinks'] == 'withoutlanglinks' ) {
188            $this->addTables( 'langlinks' );
189            $this->addJoinConds( [ 'langlinks' => [ 'LEFT JOIN', 'page_id=ll_from' ] ] );
190            $this->addWhere( [ 'll_from' => null ] );
191            $forceNameTitleIndex = false;
192        } elseif ( $params['filterlanglinks'] == 'withlanglinks' ) {
193            $this->addTables( 'langlinks' );
194            $this->addWhere( 'page_id=ll_from' );
195            $this->addOption( 'STRAIGHT_JOIN' );
196
197            $dbType = $db->getType();
198            if ( $dbType === 'mysql' || $dbType === 'sqlite' ) {
199                // Avoid MySQL filesort reported in 2015 (T78276)
200                $this->addOption( 'GROUP BY', [ 'page_title' ] );
201            } else {
202                // SQL:1999 rules only counting primary keys
203                $this->addOption( 'GROUP BY', [ 'page_title', 'page_id' ] );
204            }
205
206            $forceNameTitleIndex = false;
207        }
208
209        if ( $forceNameTitleIndex ) {
210            $this->addOption( 'USE INDEX', 'page_name_title' );
211        }
212
213        $limit = $params['limit'];
214        $this->addOption( 'LIMIT', $limit + 1 );
215        $res = $this->select( __METHOD__ );
216
217        // Get gender information
218        if ( $this->namespaceInfo->hasGenderDistinction( $params['namespace'] ) ) {
219            $users = [];
220            foreach ( $res as $row ) {
221                $users[] = $row->page_title;
222            }
223            $this->genderCache->doQuery( $users, __METHOD__ );
224            $res->rewind(); // reset
225        }
226
227        $count = 0;
228        $result = $this->getResult();
229        foreach ( $res as $row ) {
230            if ( ++$count > $limit ) {
231                // We've reached the one extra which shows that there are
232                // additional pages to be had. Stop here...
233                $this->setContinueEnumParameter( 'continue', $row->page_title );
234                break;
235            }
236
237            if ( $miserModeFilterRedir && (int)$row->page_is_redirect !== $miserModeFilterRedirValue ) {
238                // Filter implemented in PHP due to being in Miser Mode
239                continue;
240            }
241
242            if ( $resultPageSet === null ) {
243                $title = Title::makeTitle( $row->page_namespace, $row->page_title );
244                $vals = [
245                    'pageid' => (int)$row->page_id,
246                    'ns' => $title->getNamespace(),
247                    'title' => $title->getPrefixedText()
248                ];
249                $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
250                if ( !$fit ) {
251                    $this->setContinueEnumParameter( 'continue', $row->page_title );
252                    break;
253                }
254            } else {
255                $resultPageSet->processDbRow( $row );
256            }
257        }
258
259        if ( $resultPageSet === null ) {
260            $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'p' );
261        }
262    }
263
264    /** @inheritDoc */
265    public function getAllowedParams() {
266        $ret = [
267            'from' => null,
268            'continue' => [
269                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
270            ],
271            'to' => null,
272            'prefix' => null,
273            'namespace' => [
274                ParamValidator::PARAM_DEFAULT => NS_MAIN,
275                ParamValidator::PARAM_TYPE => 'namespace',
276            ],
277            'filterredir' => [
278                ParamValidator::PARAM_DEFAULT => 'all',
279                ParamValidator::PARAM_TYPE => [
280                    'all',
281                    'redirects',
282                    'nonredirects'
283                ]
284            ],
285            'filterlanglinks' => [
286                ParamValidator::PARAM_TYPE => [
287                    'withlanglinks',
288                    'withoutlanglinks',
289                    'all'
290                ],
291                ParamValidator::PARAM_DEFAULT => 'all'
292            ],
293            'minsize' => [
294                ParamValidator::PARAM_TYPE => 'integer',
295            ],
296            'maxsize' => [
297                ParamValidator::PARAM_TYPE => 'integer',
298            ],
299            'prtype' => [
300                ParamValidator::PARAM_TYPE => $this->restrictionStore->listAllRestrictionTypes( true ),
301                ParamValidator::PARAM_ISMULTI => true
302            ],
303            'prlevel' => [
304                ParamValidator::PARAM_TYPE =>
305                    $this->getConfig()->get( MainConfigNames::RestrictionLevels ),
306                ParamValidator::PARAM_ISMULTI => true
307            ],
308            'prfiltercascade' => [
309                ParamValidator::PARAM_DEFAULT => 'all',
310                ParamValidator::PARAM_TYPE => [
311                    'cascading',
312                    'noncascading',
313                    'all'
314                ],
315            ],
316            'prexpiry' => [
317                ParamValidator::PARAM_TYPE => [
318                    'indefinite',
319                    'definite',
320                    'all'
321                ],
322                ParamValidator::PARAM_DEFAULT => 'all',
323                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
324            ],
325            'limit' => [
326                ParamValidator::PARAM_DEFAULT => 10,
327                ParamValidator::PARAM_TYPE => 'limit',
328                IntegerDef::PARAM_MIN => 1,
329                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
330                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
331            ],
332            'dir' => [
333                ParamValidator::PARAM_DEFAULT => 'ascending',
334                ParamValidator::PARAM_TYPE => [
335                    'ascending',
336                    'descending'
337                ]
338            ],
339        ];
340
341        if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
342            $ret['filterredir'][ApiBase::PARAM_HELP_MSG_APPEND] = [ 'api-help-param-limited-in-miser-mode' ];
343            $ret['maxsize'][ApiBase::PARAM_HELP_MSG_APPEND] = [ 'api-help-param-disabled-in-miser-mode' ];
344        }
345
346        return $ret;
347    }
348
349    /** @inheritDoc */
350    protected function getExamplesMessages() {
351        return [
352            'action=query&list=allpages&apfrom=B'
353                => 'apihelp-query+allpages-example-b',
354            'action=query&generator=allpages&gaplimit=4&gapfrom=T&prop=info'
355                => 'apihelp-query+allpages-example-generator',
356            'action=query&generator=allpages&gaplimit=2&' .
357                'gapfilterredir=nonredirects&gapfrom=Re&prop=revisions&rvprop=content'
358                => 'apihelp-query+allpages-example-generator-revisions',
359        ];
360    }
361
362    /** @inheritDoc */
363    public function getHelpUrls() {
364        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allpages';
365    }
366}
367
368/** @deprecated class alias since 1.43 */
369class_alias( ApiQueryAllPages::class, 'ApiQueryAllPages' );