Completed
Branch master (939199)
by
unknown
39:35
created

includes/api/ApiQueryCategoryMembers.php (1 issue)

Upgrade to new PHP Analysis Engine

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
Bug Best Practice introduced by
The expression $contWhere of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch 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:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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