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