These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * |
||
4 | * |
||
5 | * Created on June 14, 2007 |
||
6 | * |
||
7 | * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" |
||
8 | * |
||
9 | * This program is free software; you can redistribute it and/or modify |
||
10 | * it under the terms of the GNU General Public License as published by |
||
11 | * the Free Software Foundation; either version 2 of the License, or |
||
12 | * (at your option) any later version. |
||
13 | * |
||
14 | * This program is distributed in the hope that it will be useful, |
||
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
17 | * GNU General Public License for more details. |
||
18 | * |
||
19 | * You should have received a copy of the GNU General Public License along |
||
20 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
21 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
22 | * http://www.gnu.org/copyleft/gpl.html |
||
23 | * |
||
24 | * @file |
||
25 | */ |
||
26 | |||
27 | /** |
||
28 | * A query module to enumerate pages that belong to a category. |
||
29 | * |
||
30 | * @ingroup API |
||
31 | */ |
||
32 | class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { |
||
33 | |||
34 | public function __construct( ApiQuery $query, $moduleName ) { |
||
35 | parent::__construct( $query, $moduleName, 'cm' ); |
||
36 | } |
||
37 | |||
38 | public function execute() { |
||
39 | $this->run(); |
||
40 | } |
||
41 | |||
42 | public function getCacheMode( $params ) { |
||
43 | return 'public'; |
||
44 | } |
||
45 | |||
46 | public function executeGenerator( $resultPageSet ) { |
||
47 | $this->run( $resultPageSet ); |
||
48 | } |
||
49 | |||
50 | /** |
||
51 | * @param string $hexSortkey |
||
52 | * @return bool |
||
53 | */ |
||
54 | private function validateHexSortkey( $hexSortkey ) { |
||
55 | // A hex sortkey has an unbound number of 2 letter pairs |
||
56 | return preg_match( '/^(?:[a-fA-F0-9]{2})*$/D', $hexSortkey ); |
||
57 | } |
||
58 | |||
59 | /** |
||
60 | * @param ApiPageSet $resultPageSet |
||
61 | * @return void |
||
62 | */ |
||
63 | private function run( $resultPageSet = null ) { |
||
64 | $params = $this->extractRequestParams(); |
||
65 | |||
66 | $categoryTitle = $this->getTitleOrPageId( $params )->getTitle(); |
||
67 | if ( $categoryTitle->getNamespace() != NS_CATEGORY ) { |
||
68 | $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' ); |
||
69 | } |
||
70 | |||
71 | $prop = array_flip( $params['prop'] ); |
||
72 | $fld_ids = isset( $prop['ids'] ); |
||
73 | $fld_title = isset( $prop['title'] ); |
||
74 | $fld_sortkey = isset( $prop['sortkey'] ); |
||
75 | $fld_sortkeyprefix = isset( $prop['sortkeyprefix'] ); |
||
76 | $fld_timestamp = isset( $prop['timestamp'] ); |
||
77 | $fld_type = isset( $prop['type'] ); |
||
78 | |||
79 | if ( is_null( $resultPageSet ) ) { |
||
80 | $this->addFields( [ 'cl_from', 'cl_sortkey', 'cl_type', 'page_namespace', 'page_title' ] ); |
||
81 | $this->addFieldsIf( 'page_id', $fld_ids ); |
||
82 | $this->addFieldsIf( 'cl_sortkey_prefix', $fld_sortkeyprefix ); |
||
83 | } else { |
||
84 | $this->addFields( $resultPageSet->getPageTableFields() ); // will include page_ id, ns, title |
||
85 | $this->addFields( [ 'cl_from', 'cl_sortkey', 'cl_type' ] ); |
||
86 | } |
||
87 | |||
88 | $this->addFieldsIf( 'cl_timestamp', $fld_timestamp || $params['sort'] == 'timestamp' ); |
||
89 | |||
90 | $this->addTables( [ 'page', 'categorylinks' ] ); // must be in this order for 'USE INDEX' |
||
91 | |||
92 | $this->addWhereFld( 'cl_to', $categoryTitle->getDBkey() ); |
||
93 | $queryTypes = $params['type']; |
||
94 | $contWhere = false; |
||
95 | |||
96 | // Scanning large datasets for rare categories sucks, and I already told |
||
97 | // how to have efficient subcategory access :-) ~~~~ (oh well, domas) |
||
98 | $miser_ns = []; |
||
99 | View Code Duplication | if ( $this->getConfig()->get( 'MiserMode' ) ) { |
|
100 | $miser_ns = $params['namespace']; |
||
101 | } else { |
||
102 | $this->addWhereFld( 'page_namespace', $params['namespace'] ); |
||
103 | } |
||
104 | |||
105 | $dir = in_array( $params['dir'], [ 'asc', 'ascending', 'newer' ] ) ? 'newer' : 'older'; |
||
106 | |||
107 | if ( $params['sort'] == 'timestamp' ) { |
||
108 | $this->addTimestampWhereRange( 'cl_timestamp', |
||
109 | $dir, |
||
110 | $params['start'], |
||
111 | $params['end'] ); |
||
112 | // Include in ORDER BY for uniqueness |
||
113 | $this->addWhereRange( 'cl_from', $dir, null, null ); |
||
114 | |||
115 | View Code Duplication | if ( !is_null( $params['continue'] ) ) { |
|
116 | $cont = explode( '|', $params['continue'] ); |
||
117 | $this->dieContinueUsageIf( count( $cont ) != 2 ); |
||
118 | $op = ( $dir === 'newer' ? '>' : '<' ); |
||
119 | $db = $this->getDB(); |
||
120 | $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) ); |
||
121 | $continueFrom = (int)$cont[1]; |
||
122 | $this->dieContinueUsageIf( $continueFrom != $cont[1] ); |
||
123 | $this->addWhere( "cl_timestamp $op $continueTimestamp OR " . |
||
124 | "(cl_timestamp = $continueTimestamp AND " . |
||
125 | "cl_from $op= $continueFrom)" |
||
126 | ); |
||
127 | } |
||
128 | |||
129 | $this->addOption( 'USE INDEX', 'cl_timestamp' ); |
||
130 | } else { |
||
131 | if ( $params['continue'] ) { |
||
132 | $cont = explode( '|', $params['continue'], 3 ); |
||
133 | $this->dieContinueUsageIf( count( $cont ) != 3 ); |
||
134 | |||
135 | // Remove the types to skip from $queryTypes |
||
136 | $contTypeIndex = array_search( $cont[0], $queryTypes ); |
||
137 | $queryTypes = array_slice( $queryTypes, $contTypeIndex ); |
||
138 | |||
139 | // Add a WHERE clause for sortkey and from |
||
140 | $this->dieContinueUsageIf( !$this->validateHexSortkey( $cont[1] ) ); |
||
141 | $escSortkey = $this->getDB()->addQuotes( hex2bin( $cont[1] ) ); |
||
142 | $from = intval( $cont[2] ); |
||
143 | $op = $dir == 'newer' ? '>' : '<'; |
||
144 | // $contWhere is used further down |
||
145 | $contWhere = "cl_sortkey $op $escSortkey OR " . |
||
146 | "(cl_sortkey = $escSortkey AND " . |
||
147 | "cl_from $op= $from)"; |
||
148 | // The below produces ORDER BY cl_sortkey, cl_from, possibly with DESC added to each of them |
||
149 | $this->addWhereRange( 'cl_sortkey', $dir, null, null ); |
||
150 | $this->addWhereRange( 'cl_from', $dir, null, null ); |
||
151 | } else { |
||
152 | View Code Duplication | if ( $params['startsortkeyprefix'] !== null ) { |
|
153 | $startsortkey = Collation::singleton()->getSortKey( $params['startsortkeyprefix'] ); |
||
154 | } elseif ( $params['starthexsortkey'] !== null ) { |
||
155 | if ( !$this->validateHexSortkey( $params['starthexsortkey'] ) ) { |
||
156 | $this->dieUsage( 'The starthexsortkey provided is not valid', 'bad_starthexsortkey' ); |
||
157 | } |
||
158 | $startsortkey = hex2bin( $params['starthexsortkey'] ); |
||
159 | } else { |
||
160 | $startsortkey = $params['startsortkey']; |
||
161 | } |
||
162 | View Code Duplication | if ( $params['endsortkeyprefix'] !== null ) { |
|
163 | $endsortkey = Collation::singleton()->getSortKey( $params['endsortkeyprefix'] ); |
||
164 | } elseif ( $params['endhexsortkey'] !== null ) { |
||
165 | if ( !$this->validateHexSortkey( $params['endhexsortkey'] ) ) { |
||
166 | $this->dieUsage( 'The endhexsortkey provided is not valid', 'bad_endhexsortkey' ); |
||
167 | } |
||
168 | $endsortkey = hex2bin( $params['endhexsortkey'] ); |
||
169 | } else { |
||
170 | $endsortkey = $params['endsortkey']; |
||
171 | } |
||
172 | |||
173 | // The below produces ORDER BY cl_sortkey, cl_from, possibly with DESC added to each of them |
||
174 | $this->addWhereRange( 'cl_sortkey', |
||
175 | $dir, |
||
176 | $startsortkey, |
||
177 | $endsortkey ); |
||
178 | $this->addWhereRange( 'cl_from', $dir, null, null ); |
||
179 | } |
||
180 | $this->addOption( 'USE INDEX', 'cl_sortkey' ); |
||
181 | } |
||
182 | |||
183 | $this->addWhere( 'cl_from=page_id' ); |
||
184 | |||
185 | $limit = $params['limit']; |
||
186 | $this->addOption( 'LIMIT', $limit + 1 ); |
||
187 | |||
188 | if ( $params['sort'] == 'sortkey' ) { |
||
189 | // Run a separate SELECT query for each value of cl_type. |
||
190 | // This is needed because cl_type is an enum, and MySQL has |
||
191 | // inconsistencies between ORDER BY cl_type and |
||
192 | // WHERE cl_type >= 'foo' making proper paging impossible |
||
193 | // and unindexed. |
||
194 | $rows = []; |
||
195 | $first = true; |
||
196 | foreach ( $queryTypes as $type ) { |
||
197 | $extraConds = [ 'cl_type' => $type ]; |
||
198 | if ( $first && $contWhere ) { |
||
0 ignored issues
–
show
|
|||
199 | // Continuation condition. Only added to the |
||
200 | // first query, otherwise we'll skip things |
||
201 | $extraConds[] = $contWhere; |
||
202 | } |
||
203 | $res = $this->select( __METHOD__, [ 'where' => $extraConds ] ); |
||
204 | $rows = array_merge( $rows, iterator_to_array( $res ) ); |
||
205 | if ( count( $rows ) >= $limit + 1 ) { |
||
206 | break; |
||
207 | } |
||
208 | $first = false; |
||
209 | } |
||
210 | } else { |
||
211 | // Sorting by timestamp |
||
212 | // No need to worry about per-type queries because we |
||
213 | // aren't sorting or filtering by type anyway |
||
214 | $res = $this->select( __METHOD__ ); |
||
215 | $rows = iterator_to_array( $res ); |
||
216 | } |
||
217 | |||
218 | $result = $this->getResult(); |
||
219 | $count = 0; |
||
220 | foreach ( $rows as $row ) { |
||
221 | View Code Duplication | if ( ++$count > $limit ) { |
|
222 | // We've reached the one extra which shows that there are |
||
223 | // additional pages to be had. Stop here... |
||
224 | // @todo Security issue - if the user has no right to view next |
||
225 | // title, it will still be shown |
||
226 | if ( $params['sort'] == 'timestamp' ) { |
||
227 | $this->setContinueEnumParameter( 'continue', "$row->cl_timestamp|$row->cl_from" ); |
||
228 | } else { |
||
229 | $sortkey = bin2hex( $row->cl_sortkey ); |
||
230 | $this->setContinueEnumParameter( 'continue', |
||
231 | "{$row->cl_type}|$sortkey|{$row->cl_from}" |
||
232 | ); |
||
233 | } |
||
234 | break; |
||
235 | } |
||
236 | |||
237 | // Since domas won't tell anyone what he told long ago, apply |
||
238 | // cmnamespace here. This means the query may return 0 actual |
||
239 | // results, but on the other hand it could save returning 5000 |
||
240 | // useless results to the client. ~~~~ |
||
241 | if ( count( $miser_ns ) && !in_array( $row->page_namespace, $miser_ns ) ) { |
||
242 | continue; |
||
243 | } |
||
244 | |||
245 | if ( is_null( $resultPageSet ) ) { |
||
246 | $vals = [ |
||
247 | ApiResult::META_TYPE => 'assoc', |
||
248 | ]; |
||
249 | if ( $fld_ids ) { |
||
250 | $vals['pageid'] = intval( $row->page_id ); |
||
251 | } |
||
252 | if ( $fld_title ) { |
||
253 | $title = Title::makeTitle( $row->page_namespace, $row->page_title ); |
||
254 | ApiQueryBase::addTitleInfo( $vals, $title ); |
||
255 | } |
||
256 | if ( $fld_sortkey ) { |
||
257 | $vals['sortkey'] = bin2hex( $row->cl_sortkey ); |
||
258 | } |
||
259 | if ( $fld_sortkeyprefix ) { |
||
260 | $vals['sortkeyprefix'] = $row->cl_sortkey_prefix; |
||
261 | } |
||
262 | if ( $fld_type ) { |
||
263 | $vals['type'] = $row->cl_type; |
||
264 | } |
||
265 | if ( $fld_timestamp ) { |
||
266 | $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->cl_timestamp ); |
||
267 | } |
||
268 | $fit = $result->addValue( [ 'query', $this->getModuleName() ], |
||
269 | null, $vals ); |
||
270 | View Code Duplication | if ( !$fit ) { |
|
271 | if ( $params['sort'] == 'timestamp' ) { |
||
272 | $this->setContinueEnumParameter( 'continue', "$row->cl_timestamp|$row->cl_from" ); |
||
273 | } else { |
||
274 | $sortkey = bin2hex( $row->cl_sortkey ); |
||
275 | $this->setContinueEnumParameter( 'continue', |
||
276 | "{$row->cl_type}|$sortkey|{$row->cl_from}" |
||
277 | ); |
||
278 | } |
||
279 | break; |
||
280 | } |
||
281 | } else { |
||
282 | $resultPageSet->processDbRow( $row ); |
||
283 | } |
||
284 | } |
||
285 | |||
286 | if ( is_null( $resultPageSet ) ) { |
||
287 | $result->addIndexedTagName( |
||
288 | [ 'query', $this->getModuleName() ], 'cm' ); |
||
289 | } |
||
290 | } |
||
291 | |||
292 | public function getAllowedParams() { |
||
293 | $ret = [ |
||
294 | 'title' => [ |
||
295 | ApiBase::PARAM_TYPE => 'string', |
||
296 | ], |
||
297 | 'pageid' => [ |
||
298 | ApiBase::PARAM_TYPE => 'integer' |
||
299 | ], |
||
300 | 'prop' => [ |
||
301 | ApiBase::PARAM_DFLT => 'ids|title', |
||
302 | ApiBase::PARAM_ISMULTI => true, |
||
303 | ApiBase::PARAM_TYPE => [ |
||
304 | 'ids', |
||
305 | 'title', |
||
306 | 'sortkey', |
||
307 | 'sortkeyprefix', |
||
308 | 'type', |
||
309 | 'timestamp', |
||
310 | ], |
||
311 | ApiBase::PARAM_HELP_MSG_PER_VALUE => [], |
||
312 | ], |
||
313 | 'namespace' => [ |
||
314 | ApiBase::PARAM_ISMULTI => true, |
||
315 | ApiBase::PARAM_TYPE => 'namespace', |
||
316 | ], |
||
317 | 'type' => [ |
||
318 | ApiBase::PARAM_ISMULTI => true, |
||
319 | ApiBase::PARAM_DFLT => 'page|subcat|file', |
||
320 | ApiBase::PARAM_TYPE => [ |
||
321 | 'page', |
||
322 | 'subcat', |
||
323 | 'file' |
||
324 | ] |
||
325 | ], |
||
326 | 'continue' => [ |
||
327 | ApiBase::PARAM_HELP_MSG => 'api-help-param-continue', |
||
328 | ], |
||
329 | 'limit' => [ |
||
330 | ApiBase::PARAM_TYPE => 'limit', |
||
331 | ApiBase::PARAM_DFLT => 10, |
||
332 | ApiBase::PARAM_MIN => 1, |
||
333 | ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, |
||
334 | ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 |
||
335 | ], |
||
336 | 'sort' => [ |
||
337 | ApiBase::PARAM_DFLT => 'sortkey', |
||
338 | ApiBase::PARAM_TYPE => [ |
||
339 | 'sortkey', |
||
340 | 'timestamp' |
||
341 | ] |
||
342 | ], |
||
343 | 'dir' => [ |
||
344 | ApiBase::PARAM_DFLT => 'ascending', |
||
345 | ApiBase::PARAM_TYPE => [ |
||
346 | 'asc', |
||
347 | 'desc', |
||
348 | // Normalising with other modules |
||
349 | 'ascending', |
||
350 | 'descending', |
||
351 | 'newer', |
||
352 | 'older', |
||
353 | ] |
||
354 | ], |
||
355 | 'start' => [ |
||
356 | ApiBase::PARAM_TYPE => 'timestamp' |
||
357 | ], |
||
358 | 'end' => [ |
||
359 | ApiBase::PARAM_TYPE => 'timestamp' |
||
360 | ], |
||
361 | 'starthexsortkey' => null, |
||
362 | 'endhexsortkey' => null, |
||
363 | 'startsortkeyprefix' => null, |
||
364 | 'endsortkeyprefix' => null, |
||
365 | 'startsortkey' => [ |
||
366 | ApiBase::PARAM_DEPRECATED => true, |
||
367 | ], |
||
368 | 'endsortkey' => [ |
||
369 | ApiBase::PARAM_DEPRECATED => true, |
||
370 | ], |
||
371 | ]; |
||
372 | |||
373 | if ( $this->getConfig()->get( 'MiserMode' ) ) { |
||
374 | $ret['namespace'][ApiBase::PARAM_HELP_MSG_APPEND] = [ |
||
375 | 'api-help-param-limited-in-miser-mode', |
||
376 | ]; |
||
377 | } |
||
378 | |||
379 | return $ret; |
||
380 | } |
||
381 | |||
382 | protected function getExamplesMessages() { |
||
383 | return [ |
||
384 | 'action=query&list=categorymembers&cmtitle=Category:Physics' |
||
385 | => 'apihelp-query+categorymembers-example-simple', |
||
386 | 'action=query&generator=categorymembers&gcmtitle=Category:Physics&prop=info' |
||
387 | => 'apihelp-query+categorymembers-example-generator', |
||
388 | ]; |
||
389 | } |
||
390 | |||
391 | public function getHelpUrls() { |
||
392 | return 'https://www.mediawiki.org/wiki/API:Categorymembers'; |
||
393 | } |
||
394 | } |
||
395 |
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
string
values, the empty string''
is a special case, in particular the following results might be unexpected: