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

includes/api/ApiQueryBase.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 Sep 7, 2006
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
 * This is a base class for all Query modules.
29
 * It provides some common functionality such as constructing various SQL
30
 * queries.
31
 *
32
 * @ingroup API
33
 */
34
abstract class ApiQueryBase extends ApiBase {
35
36
	private $mQueryModule, $mDb, $tables, $where, $fields, $options, $join_conds;
0 ignored issues
show
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
37
38
	/**
39
	 * @param ApiQuery $queryModule
40
	 * @param string $moduleName
41
	 * @param string $paramPrefix
42
	 */
43
	public function __construct( ApiQuery $queryModule, $moduleName, $paramPrefix = '' ) {
44
		parent::__construct( $queryModule->getMain(), $moduleName, $paramPrefix );
45
		$this->mQueryModule = $queryModule;
46
		$this->mDb = null;
47
		$this->resetQueryParams();
48
	}
49
50
	/************************************************************************//**
51
	 * @name   Methods to implement
52
	 * @{
53
	 */
54
55
	/**
56
	 * Get the cache mode for the data generated by this module. Override
57
	 * this in the module subclass. For possible return values and other
58
	 * details about cache modes, see ApiMain::setCacheMode()
59
	 *
60
	 * Public caching will only be allowed if *all* the modules that supply
61
	 * data for a given request return a cache mode of public.
62
	 *
63
	 * @param array $params
64
	 * @return string
65
	 */
66
	public function getCacheMode( $params ) {
67
		return 'private';
68
	}
69
70
	/**
71
	 * Override this method to request extra fields from the pageSet
72
	 * using $pageSet->requestField('fieldName')
73
	 *
74
	 * Note this only makes sense for 'prop' modules, as 'list' and 'meta'
75
	 * modules should not be using the pageset.
76
	 *
77
	 * @param ApiPageSet $pageSet
78
	 */
79
	public function requestExtraData( $pageSet ) {
80
	}
81
82
	/**@}*/
83
84
	/************************************************************************//**
85
	 * @name   Data access
86
	 * @{
87
	 */
88
89
	/**
90
	 * Get the main Query module
91
	 * @return ApiQuery
92
	 */
93
	public function getQuery() {
94
		return $this->mQueryModule;
95
	}
96
97
	/**
98
	 * @see ApiBase::getParent()
99
	 */
100
	public function getParent() {
101
		return $this->getQuery();
102
	}
103
104
	/**
105
	 * Get the Query database connection (read-only)
106
	 * @return Database
107
	 */
108
	protected function getDB() {
109
		if ( is_null( $this->mDb ) ) {
110
			$this->mDb = $this->getQuery()->getDB();
111
		}
112
113
		return $this->mDb;
114
	}
115
116
	/**
117
	 * Selects the query database connection with the given name.
118
	 * See ApiQuery::getNamedDB() for more information
119
	 * @param string $name Name to assign to the database connection
120
	 * @param int $db One of the DB_* constants
121
	 * @param array $groups Query groups
122
	 * @return Database
123
	 */
124
	public function selectNamedDB( $name, $db, $groups ) {
125
		$this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups );
126
		return $this->mDb;
127
	}
128
129
	/**
130
	 * Get the PageSet object to work on
131
	 * @return ApiPageSet
132
	 */
133
	protected function getPageSet() {
134
		return $this->getQuery()->getPageSet();
135
	}
136
137
	/**@}*/
138
139
	/************************************************************************//**
140
	 * @name   Querying
141
	 * @{
142
	 */
143
144
	/**
145
	 * Blank the internal arrays with query parameters
146
	 */
147
	protected function resetQueryParams() {
148
		$this->tables = [];
149
		$this->where = [];
150
		$this->fields = [];
151
		$this->options = [];
152
		$this->join_conds = [];
153
	}
154
155
	/**
156
	 * Add a set of tables to the internal array
157
	 * @param string|string[] $tables Table name or array of table names
158
	 * @param string|null $alias Table alias, or null for no alias. Cannot be
159
	 *  used with multiple tables
160
	 */
161
	protected function addTables( $tables, $alias = null ) {
162
		if ( is_array( $tables ) ) {
163
			if ( !is_null( $alias ) ) {
164
				ApiBase::dieDebug( __METHOD__, 'Multiple table aliases not supported' );
165
			}
166
			$this->tables = array_merge( $this->tables, $tables );
167
		} else {
168
			if ( !is_null( $alias ) ) {
169
				$this->tables[$alias] = $tables;
170
			} else {
171
				$this->tables[] = $tables;
172
			}
173
		}
174
	}
175
176
	/**
177
	 * Add a set of JOIN conditions to the internal array
178
	 *
179
	 * JOIN conditions are formatted as [ tablename => [ jointype, conditions ] ]
180
	 * e.g. [ 'page' => [ 'LEFT JOIN', 'page_id=rev_page' ] ].
181
	 * Conditions may be a string or an addWhere()-style array.
182
	 * @param array $join_conds JOIN conditions
183
	 */
184
	protected function addJoinConds( $join_conds ) {
185
		if ( !is_array( $join_conds ) ) {
186
			ApiBase::dieDebug( __METHOD__, 'Join conditions have to be arrays' );
187
		}
188
		$this->join_conds = array_merge( $this->join_conds, $join_conds );
189
	}
190
191
	/**
192
	 * Add a set of fields to select to the internal array
193
	 * @param array|string $value Field name or array of field names
194
	 */
195 View Code Duplication
	protected function addFields( $value ) {
196
		if ( is_array( $value ) ) {
197
			$this->fields = array_merge( $this->fields, $value );
198
		} else {
199
			$this->fields[] = $value;
200
		}
201
	}
202
203
	/**
204
	 * Same as addFields(), but add the fields only if a condition is met
205
	 * @param array|string $value See addFields()
206
	 * @param bool $condition If false, do nothing
207
	 * @return bool $condition
208
	 */
209
	protected function addFieldsIf( $value, $condition ) {
210
		if ( $condition ) {
211
			$this->addFields( $value );
212
213
			return true;
214
		}
215
216
		return false;
217
	}
218
219
	/**
220
	 * Add a set of WHERE clauses to the internal array.
221
	 * Clauses can be formatted as 'foo=bar' or [ 'foo' => 'bar' ],
222
	 * the latter only works if the value is a constant (i.e. not another field)
223
	 *
224
	 * If $value is an empty array, this function does nothing.
225
	 *
226
	 * For example, [ 'foo=bar', 'baz' => 3, 'bla' => 'foo' ] translates
227
	 * to "foo=bar AND baz='3' AND bla='foo'"
228
	 * @param string|array $value
229
	 */
230 View Code Duplication
	protected function addWhere( $value ) {
231
		if ( is_array( $value ) ) {
232
			// Sanity check: don't insert empty arrays,
233
			// Database::makeList() chokes on them
234
			if ( count( $value ) ) {
235
				$this->where = array_merge( $this->where, $value );
236
			}
237
		} else {
238
			$this->where[] = $value;
239
		}
240
	}
241
242
	/**
243
	 * Same as addWhere(), but add the WHERE clauses only if a condition is met
244
	 * @param string|array $value
245
	 * @param bool $condition If false, do nothing
246
	 * @return bool $condition
247
	 */
248
	protected function addWhereIf( $value, $condition ) {
249
		if ( $condition ) {
250
			$this->addWhere( $value );
251
252
			return true;
253
		}
254
255
		return false;
256
	}
257
258
	/**
259
	 * Equivalent to addWhere(array($field => $value))
260
	 * @param string $field Field name
261
	 * @param string $value Value; ignored if null or empty array;
262
	 */
263
	protected function addWhereFld( $field, $value ) {
264
		// Use count() to its full documented capabilities to simultaneously
265
		// test for null, empty array or empty countable object
266
		if ( count( $value ) ) {
267
			$this->where[$field] = $value;
268
		}
269
	}
270
271
	/**
272
	 * Add a WHERE clause corresponding to a range, and an ORDER BY
273
	 * clause to sort in the right direction
274
	 * @param string $field Field name
275
	 * @param string $dir If 'newer', sort in ascending order, otherwise
276
	 *  sort in descending order
277
	 * @param string $start Value to start the list at. If $dir == 'newer'
278
	 *  this is the lower boundary, otherwise it's the upper boundary
279
	 * @param string $end Value to end the list at. If $dir == 'newer' this
280
	 *  is the upper boundary, otherwise it's the lower boundary
281
	 * @param bool $sort If false, don't add an ORDER BY clause
282
	 */
283
	protected function addWhereRange( $field, $dir, $start, $end, $sort = true ) {
284
		$isDirNewer = ( $dir === 'newer' );
285
		$after = ( $isDirNewer ? '>=' : '<=' );
286
		$before = ( $isDirNewer ? '<=' : '>=' );
287
		$db = $this->getDB();
288
289
		if ( !is_null( $start ) ) {
290
			$this->addWhere( $field . $after . $db->addQuotes( $start ) );
291
		}
292
293
		if ( !is_null( $end ) ) {
294
			$this->addWhere( $field . $before . $db->addQuotes( $end ) );
295
		}
296
297
		if ( $sort ) {
298
			$order = $field . ( $isDirNewer ? '' : ' DESC' );
299
			// Append ORDER BY
300
			$optionOrderBy = isset( $this->options['ORDER BY'] )
301
				? (array)$this->options['ORDER BY']
302
				: [];
303
			$optionOrderBy[] = $order;
304
			$this->addOption( 'ORDER BY', $optionOrderBy );
305
		}
306
	}
307
308
	/**
309
	 * Add a WHERE clause corresponding to a range, similar to addWhereRange,
310
	 * but converts $start and $end to database timestamps.
311
	 * @see addWhereRange
312
	 * @param string $field
313
	 * @param string $dir
314
	 * @param string $start
315
	 * @param string $end
316
	 * @param bool $sort
317
	 */
318
	protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) {
319
		$db = $this->getDB();
320
		$this->addWhereRange( $field, $dir,
321
			$db->timestampOrNull( $start ), $db->timestampOrNull( $end ), $sort );
322
	}
323
324
	/**
325
	 * Add an option such as LIMIT or USE INDEX. If an option was set
326
	 * before, the old value will be overwritten
327
	 * @param string $name Option name
328
	 * @param string $value Option value
329
	 */
330
	protected function addOption( $name, $value = null ) {
331
		if ( is_null( $value ) ) {
332
			$this->options[] = $name;
333
		} else {
334
			$this->options[$name] = $value;
335
		}
336
	}
337
338
	/**
339
	 * Execute a SELECT query based on the values in the internal arrays
340
	 * @param string $method Function the query should be attributed to.
341
	 *  You should usually use __METHOD__ here
342
	 * @param array $extraQuery Query data to add but not store in the object
343
	 *  Format is [
344
	 *    'tables' => ...,
345
	 *    'fields' => ...,
346
	 *    'where' => ...,
347
	 *    'options' => ...,
348
	 *    'join_conds' => ...
349
	 *  ]
350
	 * @param array|null &$hookData If set, the ApiQueryBaseBeforeQuery and
351
	 *  ApiQueryBaseAfterQuery hooks will be called, and the
352
	 *  ApiQueryBaseProcessRow hook will be expected.
353
	 * @return ResultWrapper
354
	 */
355
	protected function select( $method, $extraQuery = [], array &$hookData = null ) {
356
357
		$tables = array_merge(
358
			$this->tables,
359
			isset( $extraQuery['tables'] ) ? (array)$extraQuery['tables'] : []
360
		);
361
		$fields = array_merge(
362
			$this->fields,
363
			isset( $extraQuery['fields'] ) ? (array)$extraQuery['fields'] : []
364
		);
365
		$where = array_merge(
366
			$this->where,
367
			isset( $extraQuery['where'] ) ? (array)$extraQuery['where'] : []
368
		);
369
		$options = array_merge(
370
			$this->options,
371
			isset( $extraQuery['options'] ) ? (array)$extraQuery['options'] : []
372
		);
373
		$join_conds = array_merge(
374
			$this->join_conds,
375
			isset( $extraQuery['join_conds'] ) ? (array)$extraQuery['join_conds'] : []
376
		);
377
378
		if ( $hookData !== null ) {
379
			Hooks::run( 'ApiQueryBaseBeforeQuery',
380
				[ $this, &$tables, &$fields, &$where, &$options, &$join_conds, &$hookData ]
381
			);
382
		}
383
384
		$res = $this->getDB()->select( $tables, $fields, $where, $method, $options, $join_conds );
385
386
		if ( $hookData !== null ) {
387
			Hooks::run( 'ApiQueryBaseAfterQuery', [ $this, $res, &$hookData ] );
388
		}
389
390
		return $res;
391
	}
392
393
	/**
394
	 * Call the ApiQueryBaseProcessRow hook
395
	 *
396
	 * Generally, a module that passed $hookData to self::select() will call
397
	 * this just before calling ApiResult::addValue(), and treat a false return
398
	 * here in the same way it treats a false return from addValue().
399
	 *
400
	 * @since 1.28
401
	 * @param object $row Database row
402
	 * @param array &$data Data to be added to the result
403
	 * @param array &$hookData Hook data from ApiQueryBase::select()
404
	 * @return bool Return false if row processing should end with continuation
405
	 */
406
	protected function processRow( $row, array &$data, array &$hookData ) {
407
		return Hooks::run( 'ApiQueryBaseProcessRow', [ $this, $row, &$data, &$hookData ] );
408
	}
409
410
	/**
411
	 * @param string $query
412
	 * @param string $protocol
413
	 * @return null|string
414
	 */
415
	public function prepareUrlQuerySearchString( $query = null, $protocol = null ) {
416
		$db = $this->getDB();
417
		if ( !is_null( $query ) || $query != '' ) {
418
			if ( is_null( $protocol ) ) {
419
				$protocol = 'http://';
420
			}
421
422
			$likeQuery = LinkFilter::makeLikeArray( $query, $protocol );
423
			if ( !$likeQuery ) {
424
				$this->dieUsage( 'Invalid query', 'bad_query' );
425
			}
426
427
			$likeQuery = LinkFilter::keepOneWildcard( $likeQuery );
428
429
			return 'el_index ' . $db->buildLike( $likeQuery );
430
		} elseif ( !is_null( $protocol ) ) {
431
			return 'el_index ' . $db->buildLike( "$protocol", $db->anyString() );
432
		}
433
434
		return null;
435
	}
436
437
	/**
438
	 * Filters hidden users (where the user doesn't have the right to view them)
439
	 * Also adds relevant block information
440
	 *
441
	 * @param bool $showBlockInfo
442
	 * @return void
443
	 */
444
	public function showHiddenUsersAddBlockInfo( $showBlockInfo ) {
445
		$this->addTables( 'ipblocks' );
446
		$this->addJoinConds( [
447
			'ipblocks' => [ 'LEFT JOIN', 'ipb_user=user_id' ],
448
		] );
449
450
		$this->addFields( 'ipb_deleted' );
451
452
		if ( $showBlockInfo ) {
453
			$this->addFields( [
454
				'ipb_id',
455
				'ipb_by',
456
				'ipb_by_text',
457
				'ipb_reason',
458
				'ipb_expiry',
459
				'ipb_timestamp'
460
			] );
461
		}
462
463
		// Don't show hidden names
464
		if ( !$this->getUser()->isAllowed( 'hideuser' ) ) {
465
			$this->addWhere( 'ipb_deleted = 0 OR ipb_deleted IS NULL' );
466
		}
467
	}
468
469
	/**@}*/
470
471
	/************************************************************************//**
472
	 * @name   Utility methods
473
	 * @{
474
	 */
475
476
	/**
477
	 * Add information (title and namespace) about a Title object to a
478
	 * result array
479
	 * @param array $arr Result array à la ApiResult
480
	 * @param Title $title
481
	 * @param string $prefix Module prefix
482
	 */
483
	public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
484
		$arr[$prefix . 'ns'] = intval( $title->getNamespace() );
485
		$arr[$prefix . 'title'] = $title->getPrefixedText();
486
	}
487
488
	/**
489
	 * Add a sub-element under the page element with the given page ID
490
	 * @param int $pageId Page ID
491
	 * @param array $data Data array à la ApiResult
492
	 * @return bool Whether the element fit in the result
493
	 */
494
	protected function addPageSubItems( $pageId, $data ) {
495
		$result = $this->getResult();
496
		ApiResult::setIndexedTagName( $data, $this->getModulePrefix() );
497
498
		return $result->addValue( [ 'query', 'pages', intval( $pageId ) ],
499
			$this->getModuleName(),
500
			$data );
501
	}
502
503
	/**
504
	 * Same as addPageSubItems(), but one element of $data at a time
505
	 * @param int $pageId Page ID
506
	 * @param array $item Data array à la ApiResult
507
	 * @param string $elemname XML element name. If null, getModuleName()
508
	 *  is used
509
	 * @return bool Whether the element fit in the result
510
	 */
511
	protected function addPageSubItem( $pageId, $item, $elemname = null ) {
512
		if ( is_null( $elemname ) ) {
513
			$elemname = $this->getModulePrefix();
514
		}
515
		$result = $this->getResult();
516
		$fit = $result->addValue( [ 'query', 'pages', $pageId,
517
			$this->getModuleName() ], null, $item );
518
		if ( !$fit ) {
519
			return false;
520
		}
521
		$result->addIndexedTagName( [ 'query', 'pages', $pageId,
522
			$this->getModuleName() ], $elemname );
523
524
		return true;
525
	}
526
527
	/**
528
	 * Set a query-continue value
529
	 * @param string $paramName Parameter name
530
	 * @param string|array $paramValue Parameter value
531
	 */
532
	protected function setContinueEnumParameter( $paramName, $paramValue ) {
533
		$this->getContinuationManager()->addContinueParam( $this, $paramName, $paramValue );
534
	}
535
536
	/**
537
	 * Convert an input title or title prefix into a dbkey.
538
	 *
539
	 * $namespace should always be specified in order to handle per-namespace
540
	 * capitalization settings.
541
	 *
542
	 * @param string $titlePart Title part
543
	 * @param int $namespace Namespace of the title
544
	 * @return string DBkey (no namespace prefix)
545
	 */
546 View Code Duplication
	public function titlePartToKey( $titlePart, $namespace = NS_MAIN ) {
547
		$t = Title::makeTitleSafe( $namespace, $titlePart . 'x' );
548
		if ( !$t || $t->hasFragment() ) {
549
			// Invalid title (e.g. bad chars) or contained a '#'.
550
			$this->dieUsageMsg( [ 'invalidtitle', $titlePart ] );
551
		}
552
		if ( $namespace != $t->getNamespace() || $t->isExternal() ) {
553
			// This can happen in two cases. First, if you call titlePartToKey with a title part
554
			// that looks like a namespace, but with $defaultNamespace = NS_MAIN. It would be very
555
			// difficult to handle such a case. Such cases cannot exist and are therefore treated
556
			// as invalid user input. The second case is when somebody specifies a title interwiki
557
			// prefix.
558
			$this->dieUsageMsg( [ 'invalidtitle', $titlePart ] );
559
		}
560
561
		return substr( $t->getDBkey(), 0, -1 );
562
	}
563
564
	/**
565
	 * Convert an input title or title prefix into a namespace constant and dbkey.
566
	 *
567
	 * @since 1.26
568
	 * @param string $titlePart Title part
569
	 * @param int $defaultNamespace Default namespace if none is given
570
	 * @return array (int, string) Namespace number and DBkey
571
	 */
572 View Code Duplication
	public function prefixedTitlePartToKey( $titlePart, $defaultNamespace = NS_MAIN ) {
573
		$t = Title::newFromText( $titlePart . 'x', $defaultNamespace );
574
		if ( !$t || $t->hasFragment() || $t->isExternal() ) {
575
			// Invalid title (e.g. bad chars) or contained a '#'.
576
			$this->dieUsageMsg( [ 'invalidtitle', $titlePart ] );
577
		}
578
579
		return [ $t->getNamespace(), substr( $t->getDBkey(), 0, -1 ) ];
580
	}
581
582
	/**
583
	 * @param string $hash
584
	 * @return bool
585
	 */
586
	public function validateSha1Hash( $hash ) {
587
		return preg_match( '/^[a-f0-9]{40}$/', $hash );
588
	}
589
590
	/**
591
	 * @param string $hash
592
	 * @return bool
593
	 */
594
	public function validateSha1Base36Hash( $hash ) {
595
		return preg_match( '/^[a-z0-9]{31}$/', $hash );
596
	}
597
598
	/**
599
	 * Check whether the current user has permission to view revision-deleted
600
	 * fields.
601
	 * @return bool
602
	 */
603
	public function userCanSeeRevDel() {
604
		return $this->getUser()->isAllowedAny(
605
			'deletedhistory',
606
			'deletedtext',
607
			'suppressrevision',
608
			'viewsuppressed'
609
		);
610
	}
611
612
	/**@}*/
613
}
614