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

includes/api/ApiQueryBase.php (3 issues)

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();
0 ignored issues
show
The method getDB() cannot be called from this context as it is declared protected in class ApiBase.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
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 );
0 ignored issues
show
It seems like $likeQuery can also be of type false; however, LinkFilter::keepOneWildcard() does only seem to accept array, did you maybe forget to handle an error condition?
Loading history...
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