Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/pager/IndexPager.php (7 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
 * Efficient paging for SQL queries.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Pager
22
 */
23
24
/**
25
 * IndexPager is an efficient pager which uses a (roughly unique) index in the
26
 * data set to implement paging, rather than a "LIMIT offset,limit" clause.
27
 * In MySQL, such a limit/offset clause requires counting through the
28
 * specified number of offset rows to find the desired data, which can be
29
 * expensive for large offsets.
30
 *
31
 * ReverseChronologicalPager is a child class of the abstract IndexPager, and
32
 * contains  some formatting and display code which is specific to the use of
33
 * timestamps as  indexes. Here is a synopsis of its operation:
34
 *
35
 *    * The query is specified by the offset, limit and direction (dir)
36
 *      parameters, in addition to any subclass-specific parameters.
37
 *    * The offset is the non-inclusive start of the DB query. A row with an
38
 *      index value equal to the offset will never be shown.
39
 *    * The query may either be done backwards, where the rows are returned by
40
 *      the database in the opposite order to which they are displayed to the
41
 *      user, or forwards. This is specified by the "dir" parameter, dir=prev
42
 *      means backwards, anything else means forwards. The offset value
43
 *      specifies the start of the database result set, which may be either
44
 *      the start or end of the displayed data set. This allows "previous"
45
 *      links to be implemented without knowledge of the index value at the
46
 *      start of the previous page.
47
 *    * An additional row beyond the user-specified limit is always requested.
48
 *      This allows us to tell whether we should display a "next" link in the
49
 *      case of forwards mode, or a "previous" link in the case of backwards
50
 *      mode. Determining whether to display the other link (the one for the
51
 *      page before the start of the database result set) can be done
52
 *      heuristically by examining the offset.
53
 *
54
 *    * An empty offset indicates that the offset condition should be omitted
55
 *      from the query. This naturally produces either the first page or the
56
 *      last page depending on the dir parameter.
57
 *
58
 *  Subclassing the pager to implement concrete functionality should be fairly
59
 *  simple, please see the examples in HistoryAction.php and
60
 *  SpecialBlockList.php. You just need to override formatRow(),
61
 *  getQueryInfo() and getIndexField(). Don't forget to call the parent
62
 *  constructor if you override it.
63
 *
64
 * @ingroup Pager
65
 */
66
abstract class IndexPager extends ContextSource implements Pager {
67
	/**
68
	 * Constants for the $mDefaultDirection field.
69
	 *
70
	 * These are boolean for historical reasons and should stay boolean for backwards-compatibility.
71
	 */
72
	const DIR_ASCENDING = false;
73
	const DIR_DESCENDING = true;
74
75
	public $mRequest;
76
	public $mLimitsShown = [ 20, 50, 100, 250, 500 ];
77
	public $mDefaultLimit = 50;
78
	public $mOffset, $mLimit;
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...
79
	public $mQueryDone = false;
80
	public $mDb;
81
	public $mPastTheEndRow;
82
83
	/**
84
	 * The index to actually be used for ordering. This is a single column,
85
	 * for one ordering, even if multiple orderings are supported.
86
	 */
87
	protected $mIndexField;
88
	/**
89
	 * An array of secondary columns to order by. These fields are not part of the offset.
90
	 * This is a column list for one ordering, even if multiple orderings are supported.
91
	 */
92
	protected $mExtraSortFields;
93
	/** For pages that support multiple types of ordering, which one to use.
94
	 */
95
	protected $mOrderType;
96
	/**
97
	 * $mDefaultDirection gives the direction to use when sorting results:
98
	 * DIR_ASCENDING or DIR_DESCENDING.  If $mIsBackwards is set, we
99
	 * start from the opposite end, but we still sort the page itself according
100
	 * to $mDefaultDirection.  E.g., if $mDefaultDirection is false but we're
101
	 * going backwards, we'll display the last page of results, but the last
102
	 * result will be at the bottom, not the top.
103
	 *
104
	 * Like $mIndexField, $mDefaultDirection will be a single value even if the
105
	 * class supports multiple default directions for different order types.
106
	 */
107
	public $mDefaultDirection;
108
	public $mIsBackwards;
109
110
	/** True if the current result set is the first one */
111
	public $mIsFirst;
112
	public $mIsLast;
113
114
	protected $mLastShown, $mFirstShown, $mPastTheEndIndex, $mDefaultQuery, $mNavigationBar;
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...
115
116
	/**
117
	 * Whether to include the offset in the query
118
	 */
119
	protected $mIncludeOffset = false;
120
121
	/**
122
	 * Result object for the query. Warning: seek before use.
123
	 *
124
	 * @var ResultWrapper
125
	 */
126
	public $mResult;
127
128
	public function __construct( IContextSource $context = null ) {
129
		if ( $context ) {
130
			$this->setContext( $context );
131
		}
132
133
		$this->mRequest = $this->getRequest();
134
135
		# NB: the offset is quoted, not validated. It is treated as an
136
		# arbitrary string to support the widest variety of index types. Be
137
		# careful outputting it into HTML!
138
		$this->mOffset = $this->mRequest->getText( 'offset' );
139
140
		# Use consistent behavior for the limit options
141
		$this->mDefaultLimit = $this->getUser()->getIntOption( 'rclimit' );
142
		if ( !$this->mLimit ) {
143
			// Don't override if a subclass calls $this->setLimit() in its constructor.
144
			list( $this->mLimit, /* $offset */ ) = $this->mRequest->getLimitOffset();
145
		}
146
147
		$this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' );
148
		# Let the subclass set the DB here; otherwise use a replica DB for the current wiki
149
		$this->mDb = $this->mDb ?: wfGetDB( DB_REPLICA );
150
151
		$index = $this->getIndexField(); // column to sort on
152
		$extraSort = $this->getExtraSortFields(); // extra columns to sort on for query planning
153
		$order = $this->mRequest->getVal( 'order' );
154
		if ( is_array( $index ) && isset( $index[$order] ) ) {
155
			$this->mOrderType = $order;
156
			$this->mIndexField = $index[$order];
157
			$this->mExtraSortFields = isset( $extraSort[$order] )
158
				? (array)$extraSort[$order]
159
				: [];
160
		} elseif ( is_array( $index ) ) {
161
			# First element is the default
162
			reset( $index );
163
			list( $this->mOrderType, $this->mIndexField ) = each( $index );
164
			$this->mExtraSortFields = isset( $extraSort[$this->mOrderType] )
165
				? (array)$extraSort[$this->mOrderType]
166
				: [];
167
		} else {
168
			# $index is not an array
169
			$this->mOrderType = null;
170
			$this->mIndexField = $index;
171
			$this->mExtraSortFields = (array)$extraSort;
172
		}
173
174
		if ( !isset( $this->mDefaultDirection ) ) {
175
			$dir = $this->getDefaultDirections();
176
			$this->mDefaultDirection = is_array( $dir )
177
				? $dir[$this->mOrderType]
178
				: $dir;
179
		}
180
	}
181
182
	/**
183
	 * Get the Database object in use
184
	 *
185
	 * @return IDatabase
186
	 */
187
	public function getDatabase() {
188
		return $this->mDb;
189
	}
190
191
	/**
192
	 * Do the query, using information from the object context. This function
193
	 * has been kept minimal to make it overridable if necessary, to allow for
194
	 * result sets formed from multiple DB queries.
195
	 */
196
	public function doQuery() {
197
		# Use the child class name for profiling
198
		$fname = __METHOD__ . ' (' . get_class( $this ) . ')';
199
		$section = Profiler::instance()->scopedProfileIn( $fname );
0 ignored issues
show
$section is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
200
201
		// @todo This should probably compare to DIR_DESCENDING and DIR_ASCENDING constants
202
		$descending = ( $this->mIsBackwards == $this->mDefaultDirection );
203
		# Plus an extra row so that we can tell the "next" link should be shown
204
		$queryLimit = $this->mLimit + 1;
205
206
		if ( $this->mOffset == '' ) {
207
			$isFirst = true;
208
		} else {
209
			// If there's an offset, we may or may not be at the first entry.
210
			// The only way to tell is to run the query in the opposite
211
			// direction see if we get a row.
212
			$oldIncludeOffset = $this->mIncludeOffset;
213
			$this->mIncludeOffset = !$this->mIncludeOffset;
214
			$isFirst = !$this->reallyDoQuery( $this->mOffset, 1, !$descending )->numRows();
215
			$this->mIncludeOffset = $oldIncludeOffset;
216
		}
217
218
		$this->mResult = $this->reallyDoQuery(
219
			$this->mOffset,
220
			$queryLimit,
221
			$descending
222
		);
223
224
		$this->extractResultInfo( $isFirst, $queryLimit, $this->mResult );
225
		$this->mQueryDone = true;
226
227
		$this->preprocessResults( $this->mResult );
228
		$this->mResult->rewind(); // Paranoia
229
	}
230
231
	/**
232
	 * @return ResultWrapper The result wrapper.
233
	 */
234
	function getResult() {
235
		return $this->mResult;
236
	}
237
238
	/**
239
	 * Set the offset from an other source than the request
240
	 *
241
	 * @param int|string $offset
242
	 */
243
	function setOffset( $offset ) {
244
		$this->mOffset = $offset;
245
	}
246
247
	/**
248
	 * Set the limit from an other source than the request
249
	 *
250
	 * Verifies limit is between 1 and 5000
251
	 *
252
	 * @param int|string $limit
253
	 */
254
	function setLimit( $limit ) {
255
		$limit = (int)$limit;
256
		// WebRequest::getLimitOffset() puts a cap of 5000, so do same here.
257
		if ( $limit > 5000 ) {
258
			$limit = 5000;
259
		}
260
		if ( $limit > 0 ) {
261
			$this->mLimit = $limit;
262
		}
263
	}
264
265
	/**
266
	 * Get the current limit
267
	 *
268
	 * @return int
269
	 */
270
	function getLimit() {
271
		return $this->mLimit;
272
	}
273
274
	/**
275
	 * Set whether a row matching exactly the offset should be also included
276
	 * in the result or not. By default this is not the case, but when the
277
	 * offset is user-supplied this might be wanted.
278
	 *
279
	 * @param bool $include
280
	 */
281
	public function setIncludeOffset( $include ) {
282
		$this->mIncludeOffset = $include;
283
	}
284
285
	/**
286
	 * Extract some useful data from the result object for use by
287
	 * the navigation bar, put it into $this
288
	 *
289
	 * @param bool $isFirst False if there are rows before those fetched (i.e.
290
	 *     if a "previous" link would make sense)
291
	 * @param int $limit Exact query limit
292
	 * @param ResultWrapper $res
293
	 */
294
	function extractResultInfo( $isFirst, $limit, ResultWrapper $res ) {
295
		$numRows = $res->numRows();
296
		if ( $numRows ) {
297
			# Remove any table prefix from index field
298
			$parts = explode( '.', $this->mIndexField );
299
			$indexColumn = end( $parts );
300
301
			$row = $res->fetchRow();
302
			$firstIndex = $row[$indexColumn];
303
304
			# Discard the extra result row if there is one
305
			if ( $numRows > $this->mLimit && $numRows > 1 ) {
306
				$res->seek( $numRows - 1 );
307
				$this->mPastTheEndRow = $res->fetchObject();
308
				$this->mPastTheEndIndex = $this->mPastTheEndRow->$indexColumn;
309
				$res->seek( $numRows - 2 );
310
				$row = $res->fetchRow();
311
				$lastIndex = $row[$indexColumn];
312
			} else {
313
				$this->mPastTheEndRow = null;
314
				# Setting indexes to an empty string means that they will be
315
				# omitted if they would otherwise appear in URLs. It just so
316
				# happens that this  is the right thing to do in the standard
317
				# UI, in all the relevant cases.
318
				$this->mPastTheEndIndex = '';
319
				$res->seek( $numRows - 1 );
320
				$row = $res->fetchRow();
321
				$lastIndex = $row[$indexColumn];
322
			}
323
		} else {
324
			$firstIndex = '';
325
			$lastIndex = '';
326
			$this->mPastTheEndRow = null;
327
			$this->mPastTheEndIndex = '';
328
		}
329
330
		if ( $this->mIsBackwards ) {
331
			$this->mIsFirst = ( $numRows < $limit );
332
			$this->mIsLast = $isFirst;
333
			$this->mLastShown = $firstIndex;
334
			$this->mFirstShown = $lastIndex;
335
		} else {
336
			$this->mIsFirst = $isFirst;
337
			$this->mIsLast = ( $numRows < $limit );
338
			$this->mLastShown = $lastIndex;
339
			$this->mFirstShown = $firstIndex;
340
		}
341
	}
342
343
	/**
344
	 * Get some text to go in brackets in the "function name" part of the SQL comment
345
	 *
346
	 * @return string
347
	 */
348
	function getSqlComment() {
349
		return get_class( $this );
350
	}
351
352
	/**
353
	 * Do a query with specified parameters, rather than using the object
354
	 * context
355
	 *
356
	 * @param string $offset Index offset, inclusive
357
	 * @param int $limit Exact query limit
358
	 * @param bool $descending Query direction, false for ascending, true for descending
359
	 * @return ResultWrapper
360
	 */
361
	public function reallyDoQuery( $offset, $limit, $descending ) {
362
		list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
363
			$this->buildQueryInfo( $offset, $limit, $descending );
364
365
		return $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
366
	}
367
368
	/**
369
	 * Build variables to use by the database wrapper.
370
	 *
371
	 * @param string $offset Index offset, inclusive
372
	 * @param int $limit Exact query limit
373
	 * @param bool $descending Query direction, false for ascending, true for descending
374
	 * @return array
375
	 */
376
	protected function buildQueryInfo( $offset, $limit, $descending ) {
377
		$fname = __METHOD__ . ' (' . $this->getSqlComment() . ')';
378
		$info = $this->getQueryInfo();
379
		$tables = $info['tables'];
380
		$fields = $info['fields'];
381
		$conds = isset( $info['conds'] ) ? $info['conds'] : [];
382
		$options = isset( $info['options'] ) ? $info['options'] : [];
383
		$join_conds = isset( $info['join_conds'] ) ? $info['join_conds'] : [];
384
		$sortColumns = array_merge( [ $this->mIndexField ], $this->mExtraSortFields );
385
		if ( $descending ) {
386
			$options['ORDER BY'] = $sortColumns;
387
			$operator = $this->mIncludeOffset ? '>=' : '>';
388
		} else {
389
			$orderBy = [];
390
			foreach ( $sortColumns as $col ) {
391
				$orderBy[] = $col . ' DESC';
392
			}
393
			$options['ORDER BY'] = $orderBy;
394
			$operator = $this->mIncludeOffset ? '<=' : '<';
395
		}
396
		if ( $offset != '' ) {
397
			$conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset );
398
		}
399
		$options['LIMIT'] = intval( $limit );
400
		return [ $tables, $fields, $conds, $fname, $options, $join_conds ];
401
	}
402
403
	/**
404
	 * Pre-process results; useful for performing batch existence checks, etc.
405
	 *
406
	 * @param ResultWrapper $result
407
	 */
408
	protected function preprocessResults( $result ) {
409
	}
410
411
	/**
412
	 * Get the formatted result list. Calls getStartBody(), formatRow() and
413
	 * getEndBody(), concatenates the results and returns them.
414
	 *
415
	 * @return string
416
	 */
417
	public function getBody() {
418
		if ( !$this->mQueryDone ) {
419
			$this->doQuery();
420
		}
421
422
		if ( $this->mResult->numRows() ) {
423
			# Do any special query batches before display
424
			$this->doBatchLookups();
425
		}
426
427
		# Don't use any extra rows returned by the query
428
		$numRows = min( $this->mResult->numRows(), $this->mLimit );
429
430
		$s = $this->getStartBody();
431
		if ( $numRows ) {
432
			if ( $this->mIsBackwards ) {
433
				for ( $i = $numRows - 1; $i >= 0; $i-- ) {
434
					$this->mResult->seek( $i );
435
					$row = $this->mResult->fetchObject();
436
					$s .= $this->formatRow( $row );
0 ignored issues
show
It seems like $row defined by $this->mResult->fetchObject() on line 435 can also be of type boolean; however, IndexPager::formatRow() does only seem to accept array|object<stdClass>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
437
				}
438
			} else {
439
				$this->mResult->seek( 0 );
440
				for ( $i = 0; $i < $numRows; $i++ ) {
441
					$row = $this->mResult->fetchObject();
442
					$s .= $this->formatRow( $row );
0 ignored issues
show
It seems like $row defined by $this->mResult->fetchObject() on line 441 can also be of type boolean; however, IndexPager::formatRow() does only seem to accept array|object<stdClass>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
443
				}
444
			}
445
		} else {
446
			$s .= $this->getEmptyBody();
447
		}
448
		$s .= $this->getEndBody();
449
		return $s;
450
	}
451
452
	/**
453
	 * Make a self-link
454
	 *
455
	 * @param string $text Text displayed on the link
456
	 * @param array $query Associative array of parameter to be in the query string
457
	 * @param string $type Link type used to create additional attributes, like "rel", "class" or
458
	 *  "title". Valid values (non-exhaustive list): 'first', 'last', 'prev', 'next', 'asc', 'desc'.
459
	 * @return string HTML fragment
460
	 */
461
	function makeLink( $text, array $query = null, $type = null ) {
462
		if ( $query === null ) {
463
			return $text;
464
		}
465
466
		$attrs = [];
467
		if ( in_array( $type, [ 'prev', 'next' ] ) ) {
468
			$attrs['rel'] = $type;
469
		}
470
471
		if ( in_array( $type, [ 'asc', 'desc' ] ) ) {
472
			$attrs['title'] = wfMessage( $type == 'asc' ? 'sort-ascending' : 'sort-descending' )->text();
473
		}
474
475
		if ( $type ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
476
			$attrs['class'] = "mw-{$type}link";
477
		}
478
479
		return Linker::linkKnown(
480
			$this->getTitle(),
481
			$text,
482
			$attrs,
483
			$query + $this->getDefaultQuery()
484
		);
485
	}
486
487
	/**
488
	 * Called from getBody(), before getStartBody() is called and
489
	 * after doQuery() was called. This will be called only if there
490
	 * are rows in the result set.
491
	 *
492
	 * @return void
493
	 */
494
	protected function doBatchLookups() {
495
	}
496
497
	/**
498
	 * Hook into getBody(), allows text to be inserted at the start. This
499
	 * will be called even if there are no rows in the result set.
500
	 *
501
	 * @return string
502
	 */
503
	protected function getStartBody() {
504
		return '';
505
	}
506
507
	/**
508
	 * Hook into getBody() for the end of the list
509
	 *
510
	 * @return string
511
	 */
512
	protected function getEndBody() {
513
		return '';
514
	}
515
516
	/**
517
	 * Hook into getBody(), for the bit between the start and the
518
	 * end when there are no rows
519
	 *
520
	 * @return string
521
	 */
522
	protected function getEmptyBody() {
523
		return '';
524
	}
525
526
	/**
527
	 * Get an array of query parameters that should be put into self-links.
528
	 * By default, all parameters passed in the URL are used, except for a
529
	 * short blacklist.
530
	 *
531
	 * @return array Associative array
532
	 */
533 View Code Duplication
	function getDefaultQuery() {
534
		if ( !isset( $this->mDefaultQuery ) ) {
535
			$this->mDefaultQuery = $this->getRequest()->getQueryValues();
536
			unset( $this->mDefaultQuery['title'] );
537
			unset( $this->mDefaultQuery['dir'] );
538
			unset( $this->mDefaultQuery['offset'] );
539
			unset( $this->mDefaultQuery['limit'] );
540
			unset( $this->mDefaultQuery['order'] );
541
			unset( $this->mDefaultQuery['month'] );
542
			unset( $this->mDefaultQuery['year'] );
543
		}
544
		return $this->mDefaultQuery;
545
	}
546
547
	/**
548
	 * Get the number of rows in the result set
549
	 *
550
	 * @return int
551
	 */
552
	function getNumRows() {
553
		if ( !$this->mQueryDone ) {
554
			$this->doQuery();
555
		}
556
		return $this->mResult->numRows();
557
	}
558
559
	/**
560
	 * Get a URL query array for the prev, next, first and last links.
561
	 *
562
	 * @return array
563
	 */
564
	function getPagingQueries() {
565
		if ( !$this->mQueryDone ) {
566
			$this->doQuery();
567
		}
568
569
		# Don't announce the limit everywhere if it's the default
570
		$urlLimit = $this->mLimit == $this->mDefaultLimit ? null : $this->mLimit;
571
572 View Code Duplication
		if ( $this->mIsFirst ) {
573
			$prev = false;
574
			$first = false;
575
		} else {
576
			$prev = [
577
				'dir' => 'prev',
578
				'offset' => $this->mFirstShown,
579
				'limit' => $urlLimit
580
			];
581
			$first = [ 'limit' => $urlLimit ];
582
		}
583 View Code Duplication
		if ( $this->mIsLast ) {
584
			$next = false;
585
			$last = false;
586
		} else {
587
			$next = [ 'offset' => $this->mLastShown, 'limit' => $urlLimit ];
588
			$last = [ 'dir' => 'prev', 'limit' => $urlLimit ];
589
		}
590
		return [
591
			'prev' => $prev,
592
			'next' => $next,
593
			'first' => $first,
594
			'last' => $last
595
		];
596
	}
597
598
	/**
599
	 * Returns whether to show the "navigation bar"
600
	 *
601
	 * @return bool
602
	 */
603
	function isNavigationBarShown() {
604
		if ( !$this->mQueryDone ) {
605
			$this->doQuery();
606
		}
607
		// Hide navigation by default if there is nothing to page
608
		return !( $this->mIsFirst && $this->mIsLast );
609
	}
610
611
	/**
612
	 * Get paging links. If a link is disabled, the item from $disabledTexts
613
	 * will be used. If there is no such item, the unlinked text from
614
	 * $linkTexts will be used. Both $linkTexts and $disabledTexts are arrays
615
	 * of HTML.
616
	 *
617
	 * @param array $linkTexts
618
	 * @param array $disabledTexts
619
	 * @return array
620
	 */
621
	function getPagingLinks( $linkTexts, $disabledTexts = [] ) {
622
		$queries = $this->getPagingQueries();
623
		$links = [];
624
625
		foreach ( $queries as $type => $query ) {
626
			if ( $query !== false ) {
627
				$links[$type] = $this->makeLink(
628
					$linkTexts[$type],
629
					$queries[$type],
0 ignored issues
show
It seems like $queries[$type] can also be of type false; however, IndexPager::makeLink() does only seem to accept null|array, did you maybe forget to handle an error condition?
Loading history...
630
					$type
631
				);
632
			} elseif ( isset( $disabledTexts[$type] ) ) {
633
				$links[$type] = $disabledTexts[$type];
634
			} else {
635
				$links[$type] = $linkTexts[$type];
636
			}
637
		}
638
639
		return $links;
640
	}
641
642
	function getLimitLinks() {
643
		$links = [];
644
		if ( $this->mIsBackwards ) {
645
			$offset = $this->mPastTheEndIndex;
646
		} else {
647
			$offset = $this->mOffset;
648
		}
649
		foreach ( $this->mLimitsShown as $limit ) {
650
			$links[] = $this->makeLink(
651
				$this->getLanguage()->formatNum( $limit ),
652
				[ 'offset' => $offset, 'limit' => $limit ],
653
				'num'
654
			);
655
		}
656
		return $links;
657
	}
658
659
	/**
660
	 * Abstract formatting function. This should return an HTML string
661
	 * representing the result row $row. Rows will be concatenated and
662
	 * returned by getBody()
663
	 *
664
	 * @param array|stdClass $row Database row
665
	 * @return string
666
	 */
667
	abstract function formatRow( $row );
668
669
	/**
670
	 * This function should be overridden to provide all parameters
671
	 * needed for the main paged query. It returns an associative
672
	 * array with the following elements:
673
	 *    tables => Table(s) for passing to Database::select()
674
	 *    fields => Field(s) for passing to Database::select(), may be *
675
	 *    conds => WHERE conditions
676
	 *    options => option array
677
	 *    join_conds => JOIN conditions
678
	 *
679
	 * @return array
680
	 */
681
	abstract function getQueryInfo();
682
683
	/**
684
	 * This function should be overridden to return the name of the index fi-
685
	 * eld.  If the pager supports multiple orders, it may return an array of
686
	 * 'querykey' => 'indexfield' pairs, so that a request with &count=querykey
687
	 * will use indexfield to sort.  In this case, the first returned key is
688
	 * the default.
689
	 *
690
	 * Needless to say, it's really not a good idea to use a non-unique index
691
	 * for this!  That won't page right.
692
	 *
693
	 * @return string|array
694
	 */
695
	abstract function getIndexField();
696
697
	/**
698
	 * This function should be overridden to return the names of secondary columns
699
	 * to order by in addition to the column in getIndexField(). These fields will
700
	 * not be used in the pager offset or in any links for users.
701
	 *
702
	 * If getIndexField() returns an array of 'querykey' => 'indexfield' pairs then
703
	 * this must return a corresponding array of 'querykey' => [ fields... ] pairs
704
	 * in order for a request with &count=querykey to use [ fields... ] to sort.
705
	 *
706
	 * This is useful for pagers that GROUP BY a unique column (say page_id)
707
	 * and ORDER BY another (say page_len). Using GROUP BY and ORDER BY both on
708
	 * page_len,page_id avoids temp tables (given a page_len index). This would
709
	 * also work if page_id was non-unique but we had a page_len,page_id index.
710
	 *
711
	 * @return array
712
	 */
713
	protected function getExtraSortFields() {
714
		return [];
715
	}
716
717
	/**
718
	 * Return the default sorting direction: DIR_ASCENDING or DIR_DESCENDING.
719
	 * You can also have an associative array of ordertype => dir,
720
	 * if multiple order types are supported.  In this case getIndexField()
721
	 * must return an array, and the keys of that must exactly match the keys
722
	 * of this.
723
	 *
724
	 * For backward compatibility, this method's return value will be ignored
725
	 * if $this->mDefaultDirection is already set when the constructor is
726
	 * called, for instance if it's statically initialized.  In that case the
727
	 * value of that variable (which must be a boolean) will be used.
728
	 *
729
	 * Note that despite its name, this does not return the value of the
730
	 * $this->mDefaultDirection member variable.  That's the default for this
731
	 * particular instantiation, which is a single value.  This is the set of
732
	 * all defaults for the class.
733
	 *
734
	 * @return bool
735
	 */
736
	protected function getDefaultDirections() {
737
		return IndexPager::DIR_ASCENDING;
738
	}
739
}
740