Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 170
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryCategories
0.00% covered (danger)
0.00%
0 / 169
0.00% covered (danger)
0.00%
0 / 8
2162
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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 / 120
0.00% covered (danger)
0.00%
0 / 1
1560
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 6
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\Deferred\LinksUpdate\CategoryLinksTable;
12use MediaWiki\Title\Title;
13use Wikimedia\ParamValidator\ParamValidator;
14use Wikimedia\ParamValidator\TypeDef\IntegerDef;
15use Wikimedia\Timestamp\TimestampFormat as TS;
16
17/**
18 * A query module to enumerate categories the set of pages belong to.
19 *
20 * @ingroup API
21 */
22class ApiQueryCategories extends ApiQueryGeneratorBase {
23
24    public function __construct( ApiQuery $query, string $moduleName ) {
25        parent::__construct( $query, $moduleName, 'cl' );
26    }
27
28    public function execute() {
29        $this->run();
30    }
31
32    /** @inheritDoc */
33    public function getCacheMode( $params ) {
34        return 'public';
35    }
36
37    /** @inheritDoc */
38    public function executeGenerator( $resultPageSet ) {
39        $this->run( $resultPageSet );
40    }
41
42    /**
43     * @param ApiPageSet|null $resultPageSet
44     */
45    private function run( $resultPageSet = null ) {
46        $pages = $this->getPageSet()->getGoodPages();
47        if ( $pages === [] ) {
48            return; // nothing to do
49        }
50
51        $params = $this->extractRequestParams();
52        $prop = array_fill_keys( (array)$params['prop'], true );
53        $show = array_fill_keys( (array)$params['show'], true );
54
55        $cats = [];
56        if ( $params['categories'] ) {
57            foreach ( $params['categories'] as $cat ) {
58                $title = Title::newFromText( $cat );
59                if ( !$title || $title->getNamespace() !== NS_CATEGORY ) {
60                    $this->addWarning( [ 'apiwarn-invalidcategory', wfEscapeWikiText( $cat ) ] );
61                } else {
62                    $cats[] = $title->getDBkey();
63                }
64            }
65
66            if ( !$cats ) {
67                // No titles so no results
68                return;
69            }
70        }
71
72        $filteredRows = [];
73        $hiddenCategories = [];
74        $needHidden = isset( $prop['hidden'] ) || isset( $show['!hidden'] ) || isset( $show['hidden'] );
75        $needFiltering = isset( $show['hidden'] ) || isset( $show['!hidden'] );
76
77        $continueFrom = null;
78        if ( $params['continue'] !== null ) {
79            $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'string' ] );
80            $continueFrom = [ $cont[0], $cont[1] ];
81        }
82
83        $db = $this->getDB();
84
85        $this->setVirtualDomain( CategoryLinksTable::VIRTUAL_DOMAIN );
86        $categoryLinksDb = $this->getDB();
87
88        $fields = [ 'cl_from', 'lt_title' ];
89        if ( isset( $prop['sortkey'] ) ) {
90            $fields[] = 'cl_sortkey';
91            $fields[] = 'cl_sortkey_prefix';
92        }
93        if ( isset( $prop['timestamp'] ) ) {
94            $fields[] = 'cl_timestamp';
95        }
96
97        $isFirstBatch = true;
98        $loopCount = 0;
99        $maxLoops = 20;
100        while ( count( $filteredRows ) < $params['limit'] + 1 ) {
101            if ( $loopCount > $maxLoops ) {
102                // Safety limit to prevent excessive iterations
103                break;
104            }
105
106            $queryBuilder = $categoryLinksDb->newSelectQueryBuilder()
107                ->select( $fields )
108                ->from( 'categorylinks' )
109                ->join( 'linktarget', null, 'cl_target_id = lt_id' )
110                ->where( [ 'cl_from' => array_keys( $pages ), 'lt_namespace' => NS_CATEGORY ] )
111                ->caller( __METHOD__ );
112
113            if ( $cats !== [] ) {
114                $queryBuilder->andWhere( [ 'lt_title' => $cats ] );
115            }
116
117            if ( $continueFrom !== null ) {
118                // Use strict comparison for subsequent batches to skip the continue row
119                if ( $isFirstBatch ) {
120                    $op = $params['dir'] == 'descending' ? '<=' : '>=';
121                } else {
122                    $op = $params['dir'] == 'descending' ? '<' : '>';
123                }
124                $queryBuilder->andWhere( $categoryLinksDb->buildComparison( $op, [
125                    'cl_from' => $continueFrom[0],
126                    'lt_title' => $continueFrom[1],
127                ] ) );
128            }
129
130            $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
131            if ( count( $pages ) === 1 ) {
132                $queryBuilder->orderBy( 'lt_title' . $sort );
133            } else {
134                $queryBuilder->orderBy( [ 'cl_from' . $sort, 'lt_title' . $sort ] );
135            }
136            $queryBuilder->limit( $params['limit'] + 1 );
137
138            $res = $queryBuilder->fetchResultSet();
139
140            $isFirstBatch = false;
141
142            $batchRows = [];
143            $categories = [];
144
145            foreach ( $res as $row ) {
146                $batchRows[] = $row;
147                $categories[] = $row->lt_title;
148            }
149
150            if ( $categories === [] ) {
151                // No more rows available
152                break;
153            }
154
155            if ( $needHidden ) {
156                $hiddenQueryBuilder = $db->newSelectQueryBuilder()
157                    ->select( [ 'pp_page', 'pp_propname', 'page_title' ] )
158                    ->from( 'page_props' )
159                    ->join( 'page', null, 'page_id = pp_page' )
160                    ->where( [
161                        'pp_propname' => 'hiddencat',
162                        'page_namespace' => NS_CATEGORY,
163                        'page_title' => $categories
164                    ] )
165                    ->caller( __METHOD__ );
166
167                $hiddenRes = $hiddenQueryBuilder->fetchResultSet();
168
169                foreach ( $hiddenRes as $hiddenRow ) {
170                    $hiddenCategories[$hiddenRow->page_title] = true;
171                }
172            }
173
174            if ( $needFiltering ) {
175                foreach ( $batchRows as $row ) {
176                    if ( isset( $show['hidden'] ) === isset( $hiddenCategories[$row->lt_title] ) ) {
177                        $filteredRows[] = $row;
178                    }
179                }
180            } else {
181                $filteredRows = array_merge( $filteredRows, $batchRows );
182            }
183
184            if ( count( $batchRows ) < $params['limit'] + 1 ) {
185                break;
186            }
187
188            $loopCount++;
189            $lastRow = end( $batchRows );
190            $continueFrom = [ $lastRow->cl_from, $lastRow->lt_title ];
191        }
192
193        $this->resetVirtualDomain();
194
195        $count = 0;
196        if ( $resultPageSet === null ) {
197            foreach ( $filteredRows as $row ) {
198                if ( ++$count > $params['limit'] ) {
199                    // We've reached the one extra which shows that
200                    // there are additional pages to be had. Stop here...
201                    $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->lt_title );
202                    break;
203                }
204
205                $title = Title::makeTitle( NS_CATEGORY, $row->lt_title );
206                $vals = [];
207                ApiQueryBase::addTitleInfo( $vals, $title );
208                if ( isset( $prop['sortkey'] ) ) {
209                    $vals['sortkey'] = bin2hex( $row->cl_sortkey );
210                    $vals['sortkeyprefix'] = $row->cl_sortkey_prefix;
211                }
212                if ( isset( $prop['timestamp'] ) ) {
213                    $vals['timestamp'] = wfTimestamp( TS::ISO_8601, $row->cl_timestamp );
214                }
215                if ( isset( $prop['hidden'] ) ) {
216                    $vals['hidden'] = isset( $hiddenCategories[$row->lt_title] );
217                }
218
219                $fit = $this->addPageSubItem( $row->cl_from, $vals );
220                if ( !$fit ) {
221                    $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->lt_title );
222                    break;
223                }
224            }
225        } else {
226            $titles = [];
227            foreach ( $filteredRows as $row ) {
228                if ( ++$count > $params['limit'] ) {
229                    // We've reached the one extra which shows that
230                    // there are additional pages to be had. Stop here...
231                    $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->lt_title );
232                    break;
233                }
234
235                $titles[] = Title::makeTitle( NS_CATEGORY, $row->lt_title );
236            }
237            $resultPageSet->populateFromTitles( $titles );
238        }
239    }
240
241    /** @inheritDoc */
242    public function getAllowedParams() {
243        return [
244            'prop' => [
245                ParamValidator::PARAM_ISMULTI => true,
246                ParamValidator::PARAM_TYPE => [
247                    'sortkey',
248                    'timestamp',
249                    'hidden',
250                ],
251                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
252            ],
253            'show' => [
254                ParamValidator::PARAM_ISMULTI => true,
255                ParamValidator::PARAM_TYPE => [
256                    'hidden',
257                    '!hidden',
258                ]
259            ],
260            'limit' => [
261                ParamValidator::PARAM_DEFAULT => 10,
262                ParamValidator::PARAM_TYPE => 'limit',
263                IntegerDef::PARAM_MIN => 1,
264                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
265                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
266            ],
267            'continue' => [
268                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
269            ],
270            'categories' => [
271                ParamValidator::PARAM_ISMULTI => true,
272            ],
273            'dir' => [
274                ParamValidator::PARAM_DEFAULT => 'ascending',
275                ParamValidator::PARAM_TYPE => [
276                    'ascending',
277                    'descending'
278                ]
279            ],
280        ];
281    }
282
283    /** @inheritDoc */
284    protected function getExamplesMessages() {
285        return [
286            'action=query&prop=categories&titles=Albert%20Einstein'
287                => 'apihelp-query+categories-example-simple',
288            'action=query&generator=categories&titles=Albert%20Einstein&prop=info'
289                => 'apihelp-query+categories-example-generator',
290        ];
291    }
292
293    /** @inheritDoc */
294    public function getHelpUrls() {
295        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Categories';
296    }
297}
298
299/** @deprecated class alias since 1.43 */
300class_alias( ApiQueryCategories::class, 'ApiQueryCategories' );