Completed
Branch master (19cd63)
by
unknown
40:04
created

QueryPage   F

Complexity

Total Complexity 106

Size/Duplication

Total Lines 823
Duplicated Lines 0.73 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
dl 6
loc 823
rs 1.263
c 0
b 0
f 0
wmc 106
lcom 1
cbo 16

38 Methods

Rating   Name   Duplication   Size   Complexity  
B getPages() 0 46 2
A setListoutput() 0 3 1
A getQueryInfo() 0 3 1
A getSQL() 0 5 1
A getOrderFields() 0 3 1
A usesTimestamps() 0 3 1
A sortDescending() 0 3 1
A isExpensive() 0 3 1
A isCacheable() 0 3 1
A isCached() 0 3 2
A isSyndicated() 0 3 1
formatResult() 0 1 ?
A getPageHeader() 0 3 1
A showEmptyText() 0 3 1
A linkParameters() 0 3 1
A tryLastResult() 0 3 1
C recache() 0 73 10
A getRecacheDB() 0 3 1
F reallyDoQuery() 0 44 12
A doQuery() 0 7 3
B fetchFromCache() 0 22 4
A getCachedTimestamp() 0 9 2
A getLimitOffset() 0 11 2
A getDBLimit() 0 9 2
A getMaxResults() 0 4 1
D execute() 0 108 16
D outputResults() 0 44 12
A openList() 0 3 1
A closeList() 0 3 1
A preprocessResults() 0 2 1
B doFeed() 0 31 5
B feedResult() 0 24 5
A feedItemDesc() 0 3 2
A feedItemAuthor() 0 3 2
A feedTitle() 6 6 1
A feedDesc() 0 3 1
A feedUrl() 0 3 1
A executeLBFromResultWrapper() 0 13 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like QueryPage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QueryPage, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Base code for "query" special pages.
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 SpecialPage
22
 */
23
24
/**
25
 * This is a class for doing query pages; since they're almost all the same,
26
 * we factor out some of the functionality into a superclass, and let
27
 * subclasses derive from it.
28
 * @ingroup SpecialPage
29
 */
30
abstract class QueryPage extends SpecialPage {
31
	/** @var bool Whether or not we want plain listoutput rather than an ordered list */
32
	protected $listoutput = false;
33
34
	/** @var int The offset and limit in use, as passed to the query() function */
35
	protected $offset = 0;
36
37
	/** @var int */
38
	protected $limit = 0;
39
40
	/**
41
	 * The number of rows returned by the query. Reading this variable
42
	 * only makes sense in functions that are run after the query has been
43
	 * done, such as preprocessResults() and formatRow().
44
	 */
45
	protected $numRows;
46
47
	protected $cachedTimestamp = null;
48
49
	/**
50
	 * Whether to show prev/next links
51
	 */
52
	protected $shownavigation = true;
53
54
	/**
55
	 * Get a list of query page classes and their associated special pages,
56
	 * for periodic updates.
57
	 *
58
	 * DO NOT CHANGE THIS LIST without testing that
59
	 * maintenance/updateSpecialPages.php still works.
60
	 * @return array
61
	 */
62
	public static function getPages() {
63
		static $qp = null;
64
65
		if ( $qp === null ) {
66
			// QueryPage subclass, Special page name
67
			$qp = [
68
				[ 'AncientPagesPage', 'Ancientpages' ],
69
				[ 'BrokenRedirectsPage', 'BrokenRedirects' ],
70
				[ 'DeadendPagesPage', 'Deadendpages' ],
71
				[ 'DoubleRedirectsPage', 'DoubleRedirects' ],
72
				[ 'FileDuplicateSearchPage', 'FileDuplicateSearch' ],
73
				[ 'ListDuplicatedFilesPage', 'ListDuplicatedFiles' ],
74
				[ 'LinkSearchPage', 'LinkSearch' ],
75
				[ 'ListredirectsPage', 'Listredirects' ],
76
				[ 'LonelyPagesPage', 'Lonelypages' ],
77
				[ 'LongPagesPage', 'Longpages' ],
78
				[ 'MediaStatisticsPage', 'MediaStatistics' ],
79
				[ 'MIMEsearchPage', 'MIMEsearch' ],
80
				[ 'MostcategoriesPage', 'Mostcategories' ],
81
				[ 'MostimagesPage', 'Mostimages' ],
82
				[ 'MostinterwikisPage', 'Mostinterwikis' ],
83
				[ 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ],
84
				[ 'MostlinkedTemplatesPage', 'Mostlinkedtemplates' ],
85
				[ 'MostlinkedPage', 'Mostlinked' ],
86
				[ 'MostrevisionsPage', 'Mostrevisions' ],
87
				[ 'FewestrevisionsPage', 'Fewestrevisions' ],
88
				[ 'ShortPagesPage', 'Shortpages' ],
89
				[ 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ],
90
				[ 'UncategorizedPagesPage', 'Uncategorizedpages' ],
91
				[ 'UncategorizedImagesPage', 'Uncategorizedimages' ],
92
				[ 'UncategorizedTemplatesPage', 'Uncategorizedtemplates' ],
93
				[ 'UnusedCategoriesPage', 'Unusedcategories' ],
94
				[ 'UnusedimagesPage', 'Unusedimages' ],
95
				[ 'WantedCategoriesPage', 'Wantedcategories' ],
96
				[ 'WantedFilesPage', 'Wantedfiles' ],
97
				[ 'WantedPagesPage', 'Wantedpages' ],
98
				[ 'WantedTemplatesPage', 'Wantedtemplates' ],
99
				[ 'UnwatchedpagesPage', 'Unwatchedpages' ],
100
				[ 'UnusedtemplatesPage', 'Unusedtemplates' ],
101
				[ 'WithoutInterwikiPage', 'Withoutinterwiki' ],
102
			];
103
			Hooks::run( 'wgQueryPages', [ &$qp ] );
104
		}
105
106
		return $qp;
107
	}
108
109
	/**
110
	 * A mutator for $this->listoutput;
111
	 *
112
	 * @param bool $bool
113
	 */
114
	function setListoutput( $bool ) {
115
		$this->listoutput = $bool;
116
	}
117
118
	/**
119
	 * Subclasses return an SQL query here, formatted as an array with the
120
	 * following keys:
121
	 *    tables => Table(s) for passing to Database::select()
122
	 *    fields => Field(s) for passing to Database::select(), may be *
123
	 *    conds => WHERE conditions
124
	 *    options => options
125
	 *    join_conds => JOIN conditions
126
	 *
127
	 * Note that the query itself should return the following three columns:
128
	 * 'namespace', 'title', and 'value'. 'value' is used for sorting.
129
	 *
130
	 * These may be stored in the querycache table for expensive queries,
131
	 * and that cached data will be returned sometimes, so the presence of
132
	 * extra fields can't be relied upon. The cached 'value' column will be
133
	 * an integer; non-numeric values are useful only for sorting the
134
	 * initial query (except if they're timestamps, see usesTimestamps()).
135
	 *
136
	 * Don't include an ORDER or LIMIT clause, they will be added.
137
	 *
138
	 * If this function is not overridden or returns something other than
139
	 * an array, getSQL() will be used instead. This is for backwards
140
	 * compatibility only and is strongly deprecated.
141
	 * @return array
142
	 * @since 1.18
143
	 */
144
	public function getQueryInfo() {
145
		return null;
146
	}
147
148
	/**
149
	 * For back-compat, subclasses may return a raw SQL query here, as a string.
150
	 * This is strongly deprecated; getQueryInfo() should be overridden instead.
151
	 * @throws MWException
152
	 * @return string
153
	 */
154
	function getSQL() {
155
		/* Implement getQueryInfo() instead */
156
		throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor "
157
			. "getQuery() properly" );
158
	}
159
160
	/**
161
	 * Subclasses return an array of fields to order by here. Don't append
162
	 * DESC to the field names, that'll be done automatically if
163
	 * sortDescending() returns true.
164
	 * @return array
165
	 * @since 1.18
166
	 */
167
	function getOrderFields() {
168
		return [ 'value' ];
169
	}
170
171
	/**
172
	 * Does this query return timestamps rather than integers in its
173
	 * 'value' field? If true, this class will convert 'value' to a
174
	 * UNIX timestamp for caching.
175
	 * NOTE: formatRow() may get timestamps in TS_MW (mysql), TS_DB (pgsql)
176
	 *       or TS_UNIX (querycache) format, so be sure to always run them
177
	 *       through wfTimestamp()
178
	 * @return bool
179
	 * @since 1.18
180
	 */
181
	public function usesTimestamps() {
182
		return false;
183
	}
184
185
	/**
186
	 * Override to sort by increasing values
187
	 *
188
	 * @return bool
189
	 */
190
	function sortDescending() {
191
		return true;
192
	}
193
194
	/**
195
	 * Is this query expensive (for some definition of expensive)? Then we
196
	 * don't let it run in miser mode. $wgDisableQueryPages causes all query
197
	 * pages to be declared expensive. Some query pages are always expensive.
198
	 *
199
	 * @return bool
200
	 */
201
	public function isExpensive() {
202
		return $this->getConfig()->get( 'DisableQueryPages' );
203
	}
204
205
	/**
206
	 * Is the output of this query cacheable? Non-cacheable expensive pages
207
	 * will be disabled in miser mode and will not have their results written
208
	 * to the querycache table.
209
	 * @return bool
210
	 * @since 1.18
211
	 */
212
	public function isCacheable() {
213
		return true;
214
	}
215
216
	/**
217
	 * Whether or not the output of the page in question is retrieved from
218
	 * the database cache.
219
	 *
220
	 * @return bool
221
	 */
222
	public function isCached() {
223
		return $this->isExpensive() && $this->getConfig()->get( 'MiserMode' );
224
	}
225
226
	/**
227
	 * Sometime we don't want to build rss / atom feeds.
228
	 *
229
	 * @return bool
230
	 */
231
	function isSyndicated() {
232
		return true;
233
	}
234
235
	/**
236
	 * Formats the results of the query for display. The skin is the current
237
	 * skin; you can use it for making links. The result is a single row of
238
	 * result data. You should be able to grab SQL results off of it.
239
	 * If the function returns false, the line output will be skipped.
240
	 * @param Skin $skin
241
	 * @param object $result Result row
242
	 * @return string|bool String or false to skip
243
	 */
244
	abstract function formatResult( $skin, $result );
245
246
	/**
247
	 * The content returned by this function will be output before any result
248
	 *
249
	 * @return string
250
	 */
251
	function getPageHeader() {
252
		return '';
253
	}
254
255
	/**
256
	 * Outputs some kind of an informative message (via OutputPage) to let the
257
	 * user know that the query returned nothing and thus there's nothing to
258
	 * show.
259
	 *
260
	 * @since 1.26
261
	 */
262
	protected function showEmptyText() {
263
		$this->getOutput()->addWikiMsg( 'specialpage-empty' );
264
	}
265
266
	/**
267
	 * If using extra form wheely-dealies, return a set of parameters here
268
	 * as an associative array. They will be encoded and added to the paging
269
	 * links (prev/next/lengths).
270
	 *
271
	 * @return array
272
	 */
273
	function linkParameters() {
274
		return [];
275
	}
276
277
	/**
278
	 * Some special pages (for example SpecialListusers used to) might not return the
279
	 * current object formatted, but return the previous one instead.
280
	 * Setting this to return true will ensure formatResult() is called
281
	 * one more time to make sure that the very last result is formatted
282
	 * as well.
283
	 *
284
	 * @deprecated since 1.27
285
	 *
286
	 * @return bool
287
	 */
288
	function tryLastResult() {
289
		return false;
290
	}
291
292
	/**
293
	 * Clear the cache and save new results
294
	 *
295
	 * @param int|bool $limit Limit for SQL statement
296
	 * @param bool $ignoreErrors Whether to ignore database errors
297
	 * @throws DBError|Exception
298
	 * @return bool|int
299
	 */
300
	public function recache( $limit, $ignoreErrors = true ) {
301
		if ( !$this->isCacheable() ) {
302
			return 0;
303
		}
304
305
		$fname = get_class( $this ) . '::recache';
0 ignored issues
show
Unused Code introduced by
$fname 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...
306
		$dbw = wfGetDB( DB_MASTER );
307
		if ( !$dbw ) {
308
			return false;
309
		}
310
311
		try {
312
			# Do query
313
			$res = $this->reallyDoQuery( $limit, false );
314
			$num = false;
315
			if ( $res ) {
316
				$num = $res->numRows();
317
				# Fetch results
318
				$vals = [];
319
				foreach ( $res as $row ) {
320
					if ( isset( $row->value ) ) {
321
						if ( $this->usesTimestamps() ) {
322
							$value = wfTimestamp( TS_UNIX,
323
								$row->value );
324
						} else {
325
							$value = intval( $row->value ); // @bug 14414
326
						}
327
					} else {
328
						$value = 0;
329
					}
330
331
					$vals[] = [
332
						'qc_type' => $this->getName(),
333
						'qc_namespace' => $row->namespace,
334
						'qc_title' => $row->title,
335
						'qc_value' => $value
336
					];
337
				}
338
339
				$dbw->doAtomicSection(
340
					__METHOD__,
341
					function ( IDatabase $dbw, $fname ) use ( $vals ) {
342
						# Clear out any old cached data
343
						$dbw->delete( 'querycache',
344
							[ 'qc_type' => $this->getName() ],
345
							$fname
346
						);
347
						# Save results into the querycache table on the master
348
						if ( count( $vals ) ) {
349
							$dbw->insert( 'querycache', $vals, $fname );
350
						}
351
						# Update the querycache_info record for the page
352
						$dbw->delete( 'querycache_info',
353
							[ 'qci_type' => $this->getName() ],
354
							$fname
355
						);
356
						$dbw->insert( 'querycache_info',
357
							[ 'qci_type' => $this->getName(),
358
								'qci_timestamp' => $dbw->timestamp() ],
359
							$fname
360
						);
361
					}
362
				);
363
			}
364
		} catch ( DBError $e ) {
365
			if ( !$ignoreErrors ) {
366
				throw $e; // report query error
367
			}
368
			$num = false; // set result to false to indicate error
369
		}
370
371
		return $num;
372
	}
373
374
	/**
375
	 * Get a DB connection to be used for slow recache queries
376
	 * @return IDatabase
377
	 */
378
	function getRecacheDB() {
379
		return wfGetDB( DB_REPLICA, [ $this->getName(), 'QueryPage::recache', 'vslow' ] );
380
	}
381
382
	/**
383
	 * Run the query and return the result
384
	 * @param int|bool $limit Numerical limit or false for no limit
385
	 * @param int|bool $offset Numerical offset or false for no offset
386
	 * @return ResultWrapper
387
	 * @since 1.18
388
	 */
389
	public function reallyDoQuery( $limit, $offset = false ) {
390
		$fname = get_class( $this ) . "::reallyDoQuery";
391
		$dbr = $this->getRecacheDB();
392
		$query = $this->getQueryInfo();
393
		$order = $this->getOrderFields();
394
395
		if ( $this->sortDescending() ) {
396
			foreach ( $order as &$field ) {
397
				$field .= ' DESC';
398
			}
399
		}
400
401
		if ( is_array( $query ) ) {
402
			$tables = isset( $query['tables'] ) ? (array)$query['tables'] : [];
403
			$fields = isset( $query['fields'] ) ? (array)$query['fields'] : [];
404
			$conds = isset( $query['conds'] ) ? (array)$query['conds'] : [];
405
			$options = isset( $query['options'] ) ? (array)$query['options'] : [];
406
			$join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : [];
407
408
			if ( count( $order ) ) {
409
				$options['ORDER BY'] = $order;
410
			}
411
412
			if ( $limit !== false ) {
413
				$options['LIMIT'] = intval( $limit );
414
			}
415
416
			if ( $offset !== false ) {
417
				$options['OFFSET'] = intval( $offset );
418
			}
419
420
			$res = $dbr->select( $tables, $fields, $conds, $fname,
421
					$options, $join_conds
422
			);
423
		} else {
424
			// Old-fashioned raw SQL style, deprecated
425
			$sql = $this->getSQL();
426
			$sql .= ' ORDER BY ' . implode( ', ', $order );
427
			$sql = $dbr->limitResult( $sql, $limit, $offset );
0 ignored issues
show
Bug introduced by
It seems like $limit defined by parameter $limit on line 389 can also be of type boolean; however, Database::limitResult() does only seem to accept integer, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
428
			$res = $dbr->query( $sql, $fname );
429
		}
430
431
		return $res;
432
	}
433
434
	/**
435
	 * Somewhat deprecated, you probably want to be using execute()
436
	 * @param int|bool $offset
437
	 * @param int|bool $limit
438
	 * @return ResultWrapper
439
	 */
440
	public function doQuery( $offset = false, $limit = false ) {
441
		if ( $this->isCached() && $this->isCacheable() ) {
442
			return $this->fetchFromCache( $limit, $offset );
443
		} else {
444
			return $this->reallyDoQuery( $limit, $offset );
445
		}
446
	}
447
448
	/**
449
	 * Fetch the query results from the query cache
450
	 * @param int|bool $limit Numerical limit or false for no limit
451
	 * @param int|bool $offset Numerical offset or false for no offset
452
	 * @return ResultWrapper
453
	 * @since 1.18
454
	 */
455
	public function fetchFromCache( $limit, $offset = false ) {
456
		$dbr = wfGetDB( DB_REPLICA );
457
		$options = [];
458
		if ( $limit !== false ) {
459
			$options['LIMIT'] = intval( $limit );
460
		}
461
		if ( $offset !== false ) {
462
			$options['OFFSET'] = intval( $offset );
463
		}
464
		if ( $this->sortDescending() ) {
465
			$options['ORDER BY'] = 'qc_value DESC';
466
		} else {
467
			$options['ORDER BY'] = 'qc_value ASC';
468
		}
469
		return $dbr->select( 'querycache', [ 'qc_type',
470
				'namespace' => 'qc_namespace',
471
				'title' => 'qc_title',
472
				'value' => 'qc_value' ],
473
				[ 'qc_type' => $this->getName() ],
474
				__METHOD__, $options
475
		);
476
	}
477
478
	public function getCachedTimestamp() {
479
		if ( is_null( $this->cachedTimestamp ) ) {
480
			$dbr = wfGetDB( DB_REPLICA );
481
			$fname = get_class( $this ) . '::getCachedTimestamp';
482
			$this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
483
				[ 'qci_type' => $this->getName() ], $fname );
484
		}
485
		return $this->cachedTimestamp;
486
	}
487
488
	/**
489
	 * Returns limit and offset, as returned by $this->getRequest()->getLimitOffset().
490
	 * Subclasses may override this to further restrict or modify limit and offset.
491
	 *
492
	 * @note Restricts the offset parameter, as most query pages have inefficient paging
493
	 *
494
	 * Its generally expected that the returned limit will not be 0, and the returned
495
	 * offset will be less than the max results.
496
	 *
497
	 * @since 1.26
498
	 * @return int[] list( $limit, $offset )
499
	 */
500
	protected function getLimitOffset() {
501
		list( $limit, $offset ) = $this->getRequest()->getLimitOffset();
502
		if ( $this->getConfig()->get( 'MiserMode' ) ) {
503
			$maxResults = $this->getMaxResults();
504
			// Can't display more than max results on a page
505
			$limit = min( $limit, $maxResults );
506
			// Can't skip over more than the end of $maxResults
507
			$offset = min( $offset, $maxResults + 1 );
508
		}
509
		return [ $limit, $offset ];
510
	}
511
512
	/**
513
	 * What is limit to fetch from DB
514
	 *
515
	 * Used to make it appear the DB stores less results then it actually does
516
	 * @param int $uiLimit Limit from UI
517
	 * @param int $uiOffset Offset from UI
518
	 * @return int Limit to use for DB (not including extra row to see if at end)
519
	 */
520
	protected function getDBLimit( $uiLimit, $uiOffset ) {
521
		$maxResults = $this->getMaxResults();
522
		if ( $this->getConfig()->get( 'MiserMode' ) ) {
523
			$limit = min( $uiLimit + 1, $maxResults - $uiOffset );
524
			return max( $limit, 0 );
525
		} else {
526
			return $uiLimit + 1;
527
		}
528
	}
529
530
	/**
531
	 * Get max number of results we can return in miser mode.
532
	 *
533
	 * Most QueryPage subclasses use inefficient paging, so limit the max amount we return
534
	 * This matters for uncached query pages that might otherwise accept an offset of 3 million
535
	 *
536
	 * @since 1.27
537
	 * @return int
538
	 */
539
	protected function getMaxResults() {
540
		// Max of 10000, unless we store more than 10000 in query cache.
541
		return max( $this->getConfig()->get( 'QueryCacheLimit' ), 10000 );
542
	}
543
544
	/**
545
	 * This is the actual workhorse. It does everything needed to make a
546
	 * real, honest-to-gosh query page.
547
	 * @param string $par
548
	 */
549
	public function execute( $par ) {
550
		$user = $this->getUser();
551
		if ( !$this->userCanExecute( $user ) ) {
552
			$this->displayRestrictionError();
553
			return;
554
		}
555
556
		$this->setHeaders();
557
		$this->outputHeader();
558
559
		$out = $this->getOutput();
560
561
		if ( $this->isCached() && !$this->isCacheable() ) {
562
			$out->addWikiMsg( 'querypage-disabled' );
563
			return;
564
		}
565
566
		$out->setSyndicated( $this->isSyndicated() );
567
568
		if ( $this->limit == 0 && $this->offset == 0 ) {
569
			list( $this->limit, $this->offset ) = $this->getLimitOffset();
570
		}
571
		$dbLimit = $this->getDBLimit( $this->limit, $this->offset );
572
		// @todo Use doQuery()
573
		if ( !$this->isCached() ) {
574
			# select one extra row for navigation
575
			$res = $this->reallyDoQuery( $dbLimit, $this->offset );
576
		} else {
577
			# Get the cached result, select one extra row for navigation
578
			$res = $this->fetchFromCache( $dbLimit, $this->offset );
579
			if ( !$this->listoutput ) {
580
581
				# Fetch the timestamp of this update
582
				$ts = $this->getCachedTimestamp();
583
				$lang = $this->getLanguage();
584
				$maxResults = $lang->formatNum( $this->getConfig()->get( 'QueryCacheLimit' ) );
585
586
				if ( $ts ) {
587
					$updated = $lang->userTimeAndDate( $ts, $user );
588
					$updateddate = $lang->userDate( $ts, $user );
589
					$updatedtime = $lang->userTime( $ts, $user );
590
					$out->addMeta( 'Data-Cache-Time', $ts );
591
					$out->addJsConfigVars( 'dataCacheTime', $ts );
592
					$out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
593
				} else {
594
					$out->addWikiMsg( 'perfcached', $maxResults );
595
				}
596
597
				# If updates on this page have been disabled, let the user know
598
				# that the data set won't be refreshed for now
599
				if ( is_array( $this->getConfig()->get( 'DisableQueryPageUpdate' ) )
600
					&& in_array( $this->getName(), $this->getConfig()->get( 'DisableQueryPageUpdate' ) )
601
				) {
602
					$out->wrapWikiMsg(
603
						"<div class=\"mw-querypage-no-updates\">\n$1\n</div>",
604
						'querypage-no-updates'
605
					);
606
				}
607
			}
608
		}
609
610
		$this->numRows = $res->numRows();
611
612
		$dbr = $this->getRecacheDB();
613
		$this->preprocessResults( $dbr, $res );
0 ignored issues
show
Bug introduced by
It seems like $dbr defined by $this->getRecacheDB() on line 612 can be null; however, QueryPage::preprocessResults() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
614
615
		$out->addHTML( Xml::openElement( 'div', [ 'class' => 'mw-spcontent' ] ) );
616
617
		# Top header and navigation
618
		if ( $this->shownavigation ) {
619
			$out->addHTML( $this->getPageHeader() );
620
			if ( $this->numRows > 0 ) {
621
				$out->addHTML( $this->msg( 'showingresultsinrange' )->numParams(
622
					min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
623
					$this->offset + 1, ( min( $this->numRows, $this->limit ) + $this->offset ) )->parseAsBlock() );
624
				# Disable the "next" link when we reach the end
625
				$miserMaxResults = $this->getConfig()->get( 'MiserMode' )
626
					&& ( $this->offset + $this->limit >= $this->getMaxResults() );
627
				$atEnd = ( $this->numRows <= $this->limit ) || $miserMaxResults;
628
				$paging = $this->getLanguage()->viewPrevNext( $this->getPageTitle( $par ), $this->offset,
629
					$this->limit, $this->linkParameters(), $atEnd );
630
				$out->addHTML( '<p>' . $paging . '</p>' );
631
			} else {
632
				# No results to show, so don't bother with "showing X of Y" etc.
633
				# -- just let the user know and give up now
634
				$this->showEmptyText();
635
				$out->addHTML( Xml::closeElement( 'div' ) );
636
				return;
637
			}
638
		}
639
640
		# The actual results; specialist subclasses will want to handle this
641
		# with more than a straight list, so we hand them the info, plus
642
		# an OutputPage, and let them get on with it
643
		$this->outputResults( $out,
644
			$this->getSkin(),
645
			$dbr, # Should use a ResultWrapper for this
0 ignored issues
show
Bug introduced by
It seems like $dbr defined by $this->getRecacheDB() on line 612 can be null; however, QueryPage::outputResults() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
646
			$res,
647
			min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
648
			$this->offset );
649
650
		# Repeat the paging links at the bottom
651
		if ( $this->shownavigation ) {
652
			$out->addHTML( '<p>' . $paging . '</p>' );
0 ignored issues
show
Bug introduced by
The variable $paging does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
653
		}
654
655
		$out->addHTML( Xml::closeElement( 'div' ) );
656
	}
657
658
	/**
659
	 * Format and output report results using the given information plus
660
	 * OutputPage
661
	 *
662
	 * @param OutputPage $out OutputPage to print to
663
	 * @param Skin $skin User skin to use
664
	 * @param IDatabase $dbr Database (read) connection to use
665
	 * @param ResultWrapper $res Result pointer
666
	 * @param int $num Number of available result rows
667
	 * @param int $offset Paging offset
668
	 */
669
	protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
670
		global $wgContLang;
671
672
		if ( $num > 0 ) {
673
			$html = [];
674
			if ( !$this->listoutput ) {
675
				$html[] = $this->openList( $offset );
676
			}
677
678
			# $res might contain the whole 1,000 rows, so we read up to
679
			# $num [should update this to use a Pager]
680
			// @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
681
			for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
682
				// @codingStandardsIgnoreEnd
683
				$line = $this->formatResult( $skin, $row );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $res->fetchObject() on line 681 can also be of type boolean; however, QueryPage::formatResult() does only seem to accept object, 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...
684
				if ( $line ) {
685
					$html[] = $this->listoutput
686
						? $line
687
						: "<li>{$line}</li>\n";
688
				}
689
			}
690
691
			# Flush the final result
692
			if ( $this->tryLastResult() ) {
693
				$row = null;
694
				$line = $this->formatResult( $skin, $row );
695
				if ( $line ) {
696
					$html[] = $this->listoutput
697
						? $line
698
						: "<li>{$line}</li>\n";
699
				}
700
			}
701
702
			if ( !$this->listoutput ) {
703
				$html[] = $this->closeList();
704
			}
705
706
			$html = $this->listoutput
707
				? $wgContLang->listToText( $html )
708
				: implode( '', $html );
709
710
			$out->addHTML( $html );
711
		}
712
	}
713
714
	/**
715
	 * @param int $offset
716
	 * @return string
717
	 */
718
	function openList( $offset ) {
719
		return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
720
	}
721
722
	/**
723
	 * @return string
724
	 */
725
	function closeList() {
726
		return "</ol>\n";
727
	}
728
729
	/**
730
	 * Do any necessary preprocessing of the result object.
731
	 * @param IDatabase $db
732
	 * @param ResultWrapper $res
733
	 */
734
	function preprocessResults( $db, $res ) {
735
	}
736
737
	/**
738
	 * Similar to above, but packaging in a syndicated feed instead of a web page
739
	 * @param string $class
740
	 * @param int $limit
741
	 * @return bool
742
	 */
743
	function doFeed( $class = '', $limit = 50 ) {
744
		if ( !$this->getConfig()->get( 'Feed' ) ) {
745
			$this->getOutput()->addWikiMsg( 'feed-unavailable' );
746
			return false;
747
		}
748
749
		$limit = min( $limit, $this->getConfig()->get( 'FeedLimit' ) );
750
751
		$feedClasses = $this->getConfig()->get( 'FeedClasses' );
752
		if ( isset( $feedClasses[$class] ) ) {
753
			/** @var RSSFeed|AtomFeed $feed */
754
			$feed = new $feedClasses[$class](
755
				$this->feedTitle(),
756
				$this->feedDesc(),
757
				$this->feedUrl() );
758
			$feed->outHeader();
759
760
			$res = $this->reallyDoQuery( $limit, 0 );
761
			foreach ( $res as $obj ) {
762
				$item = $this->feedResult( $obj );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $item is correct as $this->feedResult($obj) (which targets QueryPage::feedResult()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
It seems like $obj defined by $obj on line 761 can be null; however, QueryPage::feedResult() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
763
				if ( $item ) {
764
					$feed->outItem( $item );
765
				}
766
			}
767
768
			$feed->outFooter();
769
			return true;
770
		} else {
771
			return false;
772
		}
773
	}
774
775
	/**
776
	 * Override for custom handling. If the titles/links are ok, just do
777
	 * feedItemDesc()
778
	 * @param object $row
779
	 * @return FeedItem|null
780
	 */
781
	function feedResult( $row ) {
782
		if ( !isset( $row->title ) ) {
783
			return null;
784
		}
785
		$title = Title::makeTitle( intval( $row->namespace ), $row->title );
786
		if ( $title ) {
787
			$date = isset( $row->timestamp ) ? $row->timestamp : '';
788
			$comments = '';
789
			if ( $title ) {
790
				$talkpage = $title->getTalkPage();
791
				$comments = $talkpage->getFullURL();
792
			}
793
794
			return new FeedItem(
795
				$title->getPrefixedText(),
796
				$this->feedItemDesc( $row ),
797
				$title->getFullURL(),
798
				$date,
799
				$this->feedItemAuthor( $row ),
800
				$comments );
801
		} else {
802
			return null;
803
		}
804
	}
805
806
	function feedItemDesc( $row ) {
807
		return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
808
	}
809
810
	function feedItemAuthor( $row ) {
811
		return isset( $row->user_text ) ? $row->user_text : '';
812
	}
813
814 View Code Duplication
	function feedTitle() {
815
		$desc = $this->getDescription();
816
		$code = $this->getConfig()->get( 'LanguageCode' );
817
		$sitename = $this->getConfig()->get( 'Sitename' );
818
		return "$sitename - $desc [$code]";
819
	}
820
821
	function feedDesc() {
822
		return $this->msg( 'tagline' )->text();
823
	}
824
825
	function feedUrl() {
826
		return $this->getPageTitle()->getFullURL();
827
	}
828
829
	/**
830
	 * Creates a new LinkBatch object, adds all pages from the passed ResultWrapper (MUST include
831
	 * title and optional the namespace field) and executes the batch. This operation will pre-cache
832
	 * LinkCache information like page existence and information for stub color and redirect hints.
833
	 *
834
	 * @param ResultWrapper $res The ResultWrapper object to process. Needs to include the title
835
	 *  field and namespace field, if the $ns parameter isn't set.
836
	 * @param null $ns Use this namespace for the given titles in the ResultWrapper object,
837
	 *  instead of the namespace value of $res.
838
	 */
839
	protected function executeLBFromResultWrapper( ResultWrapper $res, $ns = null ) {
840
		if ( !$res->numRows() ) {
841
			return;
842
		}
843
844
		$batch = new LinkBatch;
845
		foreach ( $res as $row ) {
846
			$batch->add( $ns !== null ? $ns : $row->namespace, $row->title );
847
		}
848
		$batch->execute();
849
850
		$res->seek( 0 );
851
	}
852
}
853