Completed
Branch master (246348)
by
unknown
22:34
created

SpecialRecentChanges::getCacheTTL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
/**
3
 * Implements Special:Recentchanges
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
use MediaWiki\MediaWikiServices;
25
26
/**
27
 * A special page that lists last changes made to the wiki
28
 *
29
 * @ingroup SpecialPage
30
 */
31
class SpecialRecentChanges extends ChangesListSpecialPage {
32
	// @codingStandardsIgnoreStart Needed "useless" override to change parameters.
33
	public function __construct( $name = 'Recentchanges', $restriction = '' ) {
34
		parent::__construct( $name, $restriction );
35
	}
36
	// @codingStandardsIgnoreEnd
37
38
	/**
39
	 * Main execution point
40
	 *
41
	 * @param string $subpage
42
	 */
43
	public function execute( $subpage ) {
44
		// Backwards-compatibility: redirect to new feed URLs
45
		$feedFormat = $this->getRequest()->getVal( 'feed' );
46
		if ( !$this->including() && $feedFormat ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $feedFormat of type null|string 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...
47
			$query = $this->getFeedQuery();
48
			$query['feedformat'] = $feedFormat === 'atom' ? 'atom' : 'rss';
49
			$this->getOutput()->redirect( wfAppendQuery( wfScript( 'api' ), $query ) );
50
51
			return;
52
		}
53
54
		// 10 seconds server-side caching max
55
		$this->getOutput()->setCdnMaxage( 10 );
56
		// Check if the client has a cached version
57
		$lastmod = $this->checkLastModified();
58
		if ( $lastmod === false ) {
59
			return;
60
		}
61
62
		$this->addHelpLink(
63
			'//meta.wikimedia.org/wiki/Special:MyLanguage/Help:Recent_changes',
64
			true
65
		);
66
		parent::execute( $subpage );
67
	}
68
69
	/**
70
	 * Get a FormOptions object containing the default options
71
	 *
72
	 * @return FormOptions
73
	 */
74
	public function getDefaultOptions() {
75
		$opts = parent::getDefaultOptions();
76
		$user = $this->getUser();
77
78
		$opts->add( 'days', $user->getIntOption( 'rcdays' ) );
79
		$opts->add( 'limit', $user->getIntOption( 'rclimit' ) );
80
		$opts->add( 'from', '' );
81
82
		$opts->add( 'hideminor', $user->getBoolOption( 'hideminor' ) );
83
		$opts->add( 'hidebots', true );
84
		$opts->add( 'hideanons', false );
85
		$opts->add( 'hideliu', false );
86
		$opts->add( 'hidepatrolled', $user->getBoolOption( 'hidepatrolled' ) );
87
		$opts->add( 'hidemyself', false );
88
		$opts->add( 'hidecategorization', $user->getBoolOption( 'hidecategorization' ) );
89
90
		$opts->add( 'categories', '' );
91
		$opts->add( 'categories_any', false );
92
		$opts->add( 'tagfilter', '' );
93
94
		return $opts;
95
	}
96
97
	/**
98
	 * Get custom show/hide filters
99
	 *
100
	 * @return array Map of filter URL param names to properties (msg/default)
101
	 */
102 View Code Duplication
	protected function getCustomFilters() {
103
		if ( $this->customFilters === null ) {
104
			$this->customFilters = parent::getCustomFilters();
105
			Hooks::run( 'SpecialRecentChangesFilters', [ $this, &$this->customFilters ], '1.23' );
106
		}
107
108
		return $this->customFilters;
109
	}
110
111
	/**
112
	 * Process $par and put options found in $opts. Used when including the page.
113
	 *
114
	 * @param string $par
115
	 * @param FormOptions $opts
116
	 */
117
	public function parseParameters( $par, FormOptions $opts ) {
118
		$bits = preg_split( '/\s*,\s*/', trim( $par ) );
119
		foreach ( $bits as $bit ) {
120
			if ( 'hidebots' === $bit ) {
121
				$opts['hidebots'] = true;
122
			}
123
			if ( 'bots' === $bit ) {
124
				$opts['hidebots'] = false;
125
			}
126
			if ( 'hideminor' === $bit ) {
127
				$opts['hideminor'] = true;
128
			}
129
			if ( 'minor' === $bit ) {
130
				$opts['hideminor'] = false;
131
			}
132
			if ( 'hideliu' === $bit ) {
133
				$opts['hideliu'] = true;
134
			}
135
			if ( 'hidepatrolled' === $bit ) {
136
				$opts['hidepatrolled'] = true;
137
			}
138
			if ( 'hideanons' === $bit ) {
139
				$opts['hideanons'] = true;
140
			}
141
			if ( 'hidemyself' === $bit ) {
142
				$opts['hidemyself'] = true;
143
			}
144
			if ( 'hidecategorization' === $bit ) {
145
				$opts['hidecategorization'] = true;
146
			}
147
148
			if ( is_numeric( $bit ) ) {
149
				$opts['limit'] = $bit;
150
			}
151
152
			$m = [];
153
			if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
154
				$opts['limit'] = $m[1];
155
			}
156
			if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) {
157
				$opts['days'] = $m[1];
158
			}
159
			if ( preg_match( '/^namespace=(\d+)$/', $bit, $m ) ) {
160
				$opts['namespace'] = $m[1];
161
			}
162
		}
163
	}
164
165
	public function validateOptions( FormOptions $opts ) {
166
		$opts->validateIntBounds( 'limit', 0, 5000 );
167
		parent::validateOptions( $opts );
168
	}
169
170
	/**
171
	 * Return an array of conditions depending of options set in $opts
172
	 *
173
	 * @param FormOptions $opts
174
	 * @return array
175
	 */
176
	public function buildMainQueryConds( FormOptions $opts ) {
177
		$dbr = $this->getDB();
178
		$conds = parent::buildMainQueryConds( $opts );
179
180
		// Calculate cutoff
181
		$cutoff_unixtime = time() - ( $opts['days'] * 86400 );
182
		$cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
183
		$cutoff = $dbr->timestamp( $cutoff_unixtime );
184
185
		$fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
186
		if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) {
187
			$cutoff = $dbr->timestamp( $opts['from'] );
188
		} else {
189
			$opts->reset( 'from' );
190
		}
191
192
		$conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
0 ignored issues
show
Security Bug introduced by
It seems like $cutoff can also be of type false; however, DatabaseBase::addQuotes() does only seem to accept string|object<Blob>, did you maybe forget to handle an error condition?
Loading history...
193
194
		return $conds;
195
	}
196
197
	/**
198
	 * Process the query
199
	 *
200
	 * @param array $conds
201
	 * @param FormOptions $opts
202
	 * @return bool|ResultWrapper Result or false (for Recentchangeslinked only)
203
	 */
204
	public function doMainQuery( $conds, $opts ) {
205
		$dbr = $this->getDB();
206
		$user = $this->getUser();
207
208
		$tables = [ 'recentchanges' ];
209
		$fields = RecentChange::selectFields();
210
		$query_options = [];
211
		$join_conds = [];
212
213
		// JOIN on watchlist for users
214 View Code Duplication
		if ( $user->getId() && $user->isAllowed( 'viewmywatchlist' ) ) {
215
			$tables[] = 'watchlist';
216
			$fields[] = 'wl_user';
217
			$fields[] = 'wl_notificationtimestamp';
218
			$join_conds['watchlist'] = [ 'LEFT JOIN', [
219
				'wl_user' => $user->getId(),
220
				'wl_title=rc_title',
221
				'wl_namespace=rc_namespace'
222
			] ];
223
		}
224
225 View Code Duplication
		if ( $user->isAllowed( 'rollback' ) ) {
226
			$tables[] = 'page';
227
			$fields[] = 'page_latest';
228
			$join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
229
		}
230
231
		ChangeTags::modifyDisplayQuery(
232
			$tables,
233
			$fields,
234
			$conds,
235
			$join_conds,
236
			$query_options,
237
			$opts['tagfilter']
238
		);
239
240
		if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
241
			$opts )
242
		) {
243
			return false;
244
		}
245
246
		// array_merge() is used intentionally here so that hooks can, should
247
		// they so desire, override the ORDER BY / LIMIT condition(s); prior to
248
		// MediaWiki 1.26 this used to use the plus operator instead, which meant
249
		// that extensions weren't able to change these conditions
250
		$query_options = array_merge( [
251
			'ORDER BY' => 'rc_timestamp DESC',
252
			'LIMIT' => $opts['limit'] ], $query_options );
253
		$rows = $dbr->select(
254
			$tables,
255
			$fields,
256
			// rc_new is not an ENUM, but adding a redundant rc_new IN (0,1) gives mysql enough
257
			// knowledge to use an index merge if it wants (it may use some other index though).
258
			$conds + [ 'rc_new' => [ 0, 1 ] ],
259
			__METHOD__,
260
			$query_options,
261
			$join_conds
262
		);
263
264
		// Build the final data
265
		if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) {
266
			$this->filterByCategories( $rows, $opts );
0 ignored issues
show
Bug introduced by
It seems like $rows defined by $dbr->select($tables, $f...y_options, $join_conds) on line 253 can also be of type boolean; however, SpecialRecentChanges::filterByCategories() does only seem to accept object<ResultWrapper>, 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...
267
		}
268
269
		return $rows;
270
	}
271
272 View Code Duplication
	protected function runMainQueryHook( &$tables, &$fields, &$conds,
273
		&$query_options, &$join_conds, $opts
274
	) {
275
		return parent::runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts )
276
			&& Hooks::run(
277
				'SpecialRecentChangesQuery',
278
				[ &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ],
279
				'1.23'
280
			);
281
	}
282
283
	protected function getDB() {
284
		return wfGetDB( DB_SLAVE, 'recentchanges' );
285
	}
286
287
	public function outputFeedLinks() {
288
		$this->addFeedLinks( $this->getFeedQuery() );
289
	}
290
291
	/**
292
	 * Get URL query parameters for action=feedrecentchanges API feed of current recent changes view.
293
	 *
294
	 * @return array
295
	 */
296
	protected function getFeedQuery() {
297
		$query = array_filter( $this->getOptions()->getAllValues(), function ( $value ) {
298
			// API handles empty parameters in a different way
299
			return $value !== '';
300
		} );
301
		$query['action'] = 'feedrecentchanges';
302
		$feedLimit = $this->getConfig()->get( 'FeedLimit' );
303
		if ( $query['limit'] > $feedLimit ) {
304
			$query['limit'] = $feedLimit;
305
		}
306
307
		return $query;
308
	}
309
310
	/**
311
	 * Build and output the actual changes list.
312
	 *
313
	 * @param array $rows Database rows
314
	 * @param FormOptions $opts
315
	 */
316
	public function outputChangesList( $rows, $opts ) {
317
		$limit = $opts['limit'];
318
319
		$showWatcherCount = $this->getConfig()->get( 'RCShowWatchingUsers' )
320
			&& $this->getUser()->getOption( 'shownumberswatching' );
321
		$watcherCache = [];
322
323
		$dbr = $this->getDB();
0 ignored issues
show
Unused Code introduced by
$dbr 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...
324
325
		$counter = 1;
326
		$list = ChangesList::newFromContext( $this->getContext() );
327
		$list->initChangesListRows( $rows );
328
329
		$userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' );
330
		$rclistOutput = $list->beginRecentChangesList();
331
		foreach ( $rows as $obj ) {
332
			if ( $limit == 0 ) {
333
				break;
334
			}
335
			$rc = RecentChange::newFromRow( $obj );
336
337
			# Skip CatWatch entries for hidden cats based on user preference
338
			if (
339
				$rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE &&
340
				!$userShowHiddenCats &&
341
				$rc->getParam( 'hidden-cat' )
342
			) {
343
				continue;
344
			}
345
346
			$rc->counter = $counter++;
347
			# Check if the page has been updated since the last visit
348
			if ( $this->getConfig()->get( 'ShowUpdatedMarker' )
349
				&& !empty( $obj->wl_notificationtimestamp )
350
			) {
351
				$rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp );
352
			} else {
353
				$rc->notificationtimestamp = false; // Default
354
			}
355
			# Check the number of users watching the page
356
			$rc->numberofWatchingusers = 0; // Default
357
			if ( $showWatcherCount && $obj->rc_namespace >= 0 ) {
358
				if ( !isset( $watcherCache[$obj->rc_namespace][$obj->rc_title] ) ) {
359
					$watcherCache[$obj->rc_namespace][$obj->rc_title] =
360
						MediaWikiServices::getInstance()->getWatchedItemStore()->countWatchers(
361
							new TitleValue( (int)$obj->rc_namespace, $obj->rc_title )
362
						);
363
				}
364
				$rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
365
			}
366
367
			$changeLine = $list->recentChangesLine( $rc, !empty( $obj->wl_user ), $counter );
368
			if ( $changeLine !== false ) {
369
				$rclistOutput .= $changeLine;
370
				--$limit;
371
			}
372
		}
373
		$rclistOutput .= $list->endRecentChangesList();
374
375
		if ( $rows->numRows() === 0 ) {
0 ignored issues
show
Bug introduced by
The method numRows cannot be called on $rows (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
376
			$this->getOutput()->addHTML(
377
				'<div class="mw-changeslist-empty">' .
378
				$this->msg( 'recentchanges-noresult' )->parse() .
379
				'</div>'
380
			);
381
			if ( !$this->including() ) {
382
				$this->getOutput()->setStatusCode( 404 );
383
			}
384
		} else {
385
			$this->getOutput()->addHTML( $rclistOutput );
386
		}
387
	}
388
389
	/**
390
	 * Set the text to be displayed above the changes
391
	 *
392
	 * @param FormOptions $opts
393
	 * @param int $numRows Number of rows in the result to show after this header
394
	 */
395
	public function doHeader( $opts, $numRows ) {
396
		$this->setTopText( $opts );
397
398
		$defaults = $opts->getAllValues();
399
		$nondefaults = $opts->getChangedValues();
400
401
		$panel = [];
402
		$panel[] = $this->makeLegend();
403
		$panel[] = $this->optionsPanel( $defaults, $nondefaults, $numRows );
404
		$panel[] = '<hr />';
405
406
		$extraOpts = $this->getExtraOptions( $opts );
407
		$extraOptsCount = count( $extraOpts );
408
		$count = 0;
409
		$submit = ' ' . Xml::submitButton( $this->msg( 'recentchanges-submit' )->text() );
410
411
		$out = Xml::openElement( 'table', [ 'class' => 'mw-recentchanges-table' ] );
412
		foreach ( $extraOpts as $name => $optionRow ) {
413
			# Add submit button to the last row only
414
			++$count;
415
			$addSubmit = ( $count === $extraOptsCount ) ? $submit : '';
416
417
			$out .= Xml::openElement( 'tr' );
418
			if ( is_array( $optionRow ) ) {
419
				$out .= Xml::tags(
420
					'td',
421
					[ 'class' => 'mw-label mw-' . $name . '-label' ],
422
					$optionRow[0]
423
				);
424
				$out .= Xml::tags(
425
					'td',
426
					[ 'class' => 'mw-input' ],
427
					$optionRow[1] . $addSubmit
428
				);
429
			} else {
430
				$out .= Xml::tags(
431
					'td',
432
					[ 'class' => 'mw-input', 'colspan' => 2 ],
433
					$optionRow . $addSubmit
434
				);
435
			}
436
			$out .= Xml::closeElement( 'tr' );
437
		}
438
		$out .= Xml::closeElement( 'table' );
439
440
		$unconsumed = $opts->getUnconsumedValues();
441
		foreach ( $unconsumed as $key => $value ) {
442
			$out .= Html::hidden( $key, $value );
443
		}
444
445
		$t = $this->getPageTitle();
446
		$out .= Html::hidden( 'title', $t->getPrefixedText() );
447
		$form = Xml::tags( 'form', [ 'action' => wfScript() ], $out );
448
		$panel[] = $form;
449
		$panelString = implode( "\n", $panel );
450
451
		$this->getOutput()->addHTML(
452
			Xml::fieldset(
453
				$this->msg( 'recentchanges-legend' )->text(),
454
				$panelString,
455
				[ 'class' => 'rcoptions' ]
456
			)
457
		);
458
459
		$this->setBottomText( $opts );
460
	}
461
462
	/**
463
	 * Send the text to be displayed above the options
464
	 *
465
	 * @param FormOptions $opts Unused
466
	 */
467 View Code Duplication
	function setTopText( FormOptions $opts ) {
468
		global $wgContLang;
469
470
		$message = $this->msg( 'recentchangestext' )->inContentLanguage();
471
		if ( !$message->isDisabled() ) {
472
			$this->getOutput()->addWikiText(
473
				Html::rawElement( 'div',
474
					[ 'lang' => $wgContLang->getHtmlCode(), 'dir' => $wgContLang->getDir() ],
475
					"\n" . $message->plain() . "\n"
476
				),
477
				/* $lineStart */ true,
478
				/* $interface */ false
479
			);
480
		}
481
	}
482
483
	/**
484
	 * Get options to be displayed in a form
485
	 *
486
	 * @param FormOptions $opts
487
	 * @return array
488
	 */
489
	function getExtraOptions( $opts ) {
490
		$opts->consumeValues( [
491
			'namespace', 'invert', 'associated', 'tagfilter', 'categories', 'categories_any'
492
		] );
493
494
		$extraOpts = [];
495
		$extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
496
497
		if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) {
498
			$extraOpts['category'] = $this->categoryFilterForm( $opts );
499
		}
500
501
		$tagFilter = ChangeTags::buildTagFilterSelector( $opts['tagfilter'] );
502
		if ( count( $tagFilter ) ) {
503
			$extraOpts['tagfilter'] = $tagFilter;
504
		}
505
506
		// Don't fire the hook for subclasses. (Or should we?)
507
		if ( $this->getName() === 'Recentchanges' ) {
508
			Hooks::run( 'SpecialRecentChangesPanel', [ &$extraOpts, $opts ] );
509
		}
510
511
		return $extraOpts;
512
	}
513
514
	/**
515
	 * Add page-specific modules.
516
	 */
517
	protected function addModules() {
518
		parent::addModules();
519
		$out = $this->getOutput();
520
		$out->addModules( 'mediawiki.special.recentchanges' );
521
	}
522
523
	/**
524
	 * Get last modified date, for client caching
525
	 * Don't use this if we are using the patrol feature, patrol changes don't
526
	 * update the timestamp
527
	 *
528
	 * @return string|bool
529
	 */
530
	public function checkLastModified() {
531
		$dbr = $this->getDB();
532
		$lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ );
533
534
		return $lastmod;
535
	}
536
537
	/**
538
	 * Creates the choose namespace selection
539
	 *
540
	 * @param FormOptions $opts
541
	 * @return string
542
	 */
543
	protected function namespaceFilterForm( FormOptions $opts ) {
544
		$nsSelect = Html::namespaceSelector(
545
			[ 'selected' => $opts['namespace'], 'all' => '' ],
546
			[ 'name' => 'namespace', 'id' => 'namespace' ]
547
		);
548
		$nsLabel = Xml::label( $this->msg( 'namespace' )->text(), 'namespace' );
549
		$invert = Xml::checkLabel(
550
			$this->msg( 'invert' )->text(), 'invert', 'nsinvert',
551
			$opts['invert'],
552
			[ 'title' => $this->msg( 'tooltip-invert' )->text() ]
553
		);
554
		$associated = Xml::checkLabel(
555
			$this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated',
556
			$opts['associated'],
557
			[ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ]
558
		);
559
560
		return [ $nsLabel, "$nsSelect $invert $associated" ];
561
	}
562
563
	/**
564
	 * Create an input to filter changes by categories
565
	 *
566
	 * @param FormOptions $opts
567
	 * @return array
568
	 */
569
	protected function categoryFilterForm( FormOptions $opts ) {
570
		list( $label, $input ) = Xml::inputLabelSep( $this->msg( 'rc_categories' )->text(),
571
			'categories', 'mw-categories', false, $opts['categories'] );
572
573
		$input .= ' ' . Xml::checkLabel( $this->msg( 'rc_categories_any' )->text(),
574
			'categories_any', 'mw-categories_any', $opts['categories_any'] );
575
576
		return [ $label, $input ];
577
	}
578
579
	/**
580
	 * Filter $rows by categories set in $opts
581
	 *
582
	 * @param ResultWrapper $rows Database rows
583
	 * @param FormOptions $opts
584
	 */
585
	function filterByCategories( &$rows, FormOptions $opts ) {
586
		$categories = array_map( 'trim', explode( '|', $opts['categories'] ) );
587
588
		if ( !count( $categories ) ) {
589
			return;
590
		}
591
592
		# Filter categories
593
		$cats = [];
594
		foreach ( $categories as $cat ) {
595
			$cat = trim( $cat );
596
			if ( $cat == '' ) {
597
				continue;
598
			}
599
			$cats[] = $cat;
600
		}
601
602
		# Filter articles
603
		$articles = [];
604
		$a2r = [];
605
		$rowsarr = [];
606
		foreach ( $rows as $k => $r ) {
607
			$nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
608
			$id = $nt->getArticleID();
609
			if ( $id == 0 ) {
610
				continue; # Page might have been deleted...
611
			}
612
			if ( !in_array( $id, $articles ) ) {
613
				$articles[] = $id;
614
			}
615
			if ( !isset( $a2r[$id] ) ) {
616
				$a2r[$id] = [];
617
			}
618
			$a2r[$id][] = $k;
619
			$rowsarr[$k] = $r;
620
		}
621
622
		# Shortcut?
623
		if ( !count( $articles ) || !count( $cats ) ) {
624
			return;
625
		}
626
627
		# Look up
628
		$catFind = new CategoryFinder;
629
		$catFind->seed( $articles, $cats, $opts['categories_any'] ? 'OR' : 'AND' );
630
		$match = $catFind->run();
631
632
		# Filter
633
		$newrows = [];
634
		foreach ( $match as $id ) {
635
			foreach ( $a2r[$id] as $rev ) {
636
				$k = $rev;
637
				$newrows[$k] = $rowsarr[$k];
638
			}
639
		}
640
		$rows = $newrows;
641
	}
642
643
	/**
644
	 * Makes change an option link which carries all the other options
645
	 *
646
	 * @param string $title Title
647
	 * @param array $override Options to override
648
	 * @param array $options Current options
649
	 * @param bool $active Whether to show the link in bold
650
	 * @return string
651
	 */
652
	function makeOptionsLink( $title, $override, $options, $active = false ) {
653
		$params = $override + $options;
654
655
		// Bug 36524: false values have be converted to "0" otherwise
656
		// wfArrayToCgi() will omit it them.
657
		foreach ( $params as &$value ) {
658
			if ( $value === false ) {
659
				$value = '0';
660
			}
661
		}
662
		unset( $value );
663
664
		$text = htmlspecialchars( $title );
665
		if ( $active ) {
666
			$text = '<strong>' . $text . '</strong>';
667
		}
668
669
		return Linker::linkKnown( $this->getPageTitle(), $text, [], $params );
670
	}
671
672
	/**
673
	 * Creates the options panel.
674
	 *
675
	 * @param array $defaults
676
	 * @param array $nondefaults
677
	 * @param int $numRows Number of rows in the result to show after this header
678
	 * @return string
679
	 */
680
	function optionsPanel( $defaults, $nondefaults, $numRows ) {
681
		$options = $nondefaults + $defaults;
682
683
		$note = '';
684
		$msg = $this->msg( 'rclegend' );
685
		if ( !$msg->isDisabled() ) {
686
			$note .= '<div class="mw-rclegend">' . $msg->parse() . "</div>\n";
687
		}
688
689
		$lang = $this->getLanguage();
690
		$user = $this->getUser();
691
		$config = $this->getConfig();
692
		if ( $options['from'] ) {
693
			$note .= $this->msg( 'rcnotefrom' )
694
				->numParams( $options['limit'] )
695
				->params(
696
					$lang->userTimeAndDate( $options['from'], $user ),
697
					$lang->userDate( $options['from'], $user ),
698
					$lang->userTime( $options['from'], $user )
699
				)
700
				->numParams( $numRows )
701
				->parse() . '<br />';
702
		}
703
704
		# Sort data for display and make sure it's unique after we've added user data.
705
		$linkLimits = $config->get( 'RCLinkLimits' );
706
		$linkLimits[] = $options['limit'];
707
		sort( $linkLimits );
708
		$linkLimits = array_unique( $linkLimits );
709
710
		$linkDays = $config->get( 'RCLinkDays' );
711
		$linkDays[] = $options['days'];
712
		sort( $linkDays );
713
		$linkDays = array_unique( $linkDays );
714
715
		// limit links
716
		$cl = [];
717 View Code Duplication
		foreach ( $linkLimits as $value ) {
718
			$cl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
719
				[ 'limit' => $value ], $nondefaults, $value == $options['limit'] );
720
		}
721
		$cl = $lang->pipeList( $cl );
722
723
		// day links, reset 'from' to none
724
		$dl = [];
725 View Code Duplication
		foreach ( $linkDays as $value ) {
726
			$dl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
727
				[ 'days' => $value, 'from' => '' ], $nondefaults, $value == $options['days'] );
728
		}
729
		$dl = $lang->pipeList( $dl );
730
731
		// show/hide links
732
		$filters = [
733
			'hideminor' => 'rcshowhideminor',
734
			'hidebots' => 'rcshowhidebots',
735
			'hideanons' => 'rcshowhideanons',
736
			'hideliu' => 'rcshowhideliu',
737
			'hidepatrolled' => 'rcshowhidepatr',
738
			'hidemyself' => 'rcshowhidemine'
739
		];
740
741
		if ( $config->get( 'RCWatchCategoryMembership' ) ) {
742
			$filters['hidecategorization'] = 'rcshowhidecategorization';
743
		}
744
745
		$showhide = [ 'show', 'hide' ];
746
747
		foreach ( $this->getCustomFilters() as $key => $params ) {
748
			$filters[$key] = $params['msg'];
749
		}
750
		// Disable some if needed
751
		if ( !$user->useRCPatrol() ) {
752
			unset( $filters['hidepatrolled'] );
753
		}
754
755
		$links = [];
756
		foreach ( $filters as $key => $msg ) {
757
			// The following messages are used here:
758
			// rcshowhideminor-show, rcshowhideminor-hide, rcshowhidebots-show, rcshowhidebots-hide,
759
			// rcshowhideanons-show, rcshowhideanons-hide, rcshowhideliu-show, rcshowhideliu-hide,
760
			// rcshowhidepatr-show, rcshowhidepatr-hide, rcshowhidemine-show, rcshowhidemine-hide,
761
			// rcshowhidecategorization-show, rcshowhidecategorization-hide.
762
			$linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] );
763
			// Extensions can define additional filters, but don't need to define the corresponding
764
			// messages. If they don't exist, just fall back to 'show' and 'hide'.
765
			if ( !$linkMessage->exists() ) {
766
				$linkMessage = $this->msg( $showhide[1 - $options[$key]] );
767
			}
768
769
			$link = $this->makeOptionsLink( $linkMessage->text(),
770
				[ $key => 1 - $options[$key] ], $nondefaults );
771
			$links[] = "<span class=\"$msg rcshowhideoption\">"
772
				. $this->msg( $msg )->rawParams( $link )->escaped() . '</span>';
773
		}
774
775
		// show from this onward link
776
		$timestamp = wfTimestampNow();
777
		$now = $lang->userTimeAndDate( $timestamp, $user );
778
		$timenow = $lang->userTime( $timestamp, $user );
779
		$datenow = $lang->userDate( $timestamp, $user );
780
		$pipedLinks = '<span class="rcshowhide">' . $lang->pipeList( $links ) . '</span>';
781
782
		$rclinks = '<span class="rclinks">' . $this->msg( 'rclinks' )->rawParams( $cl, $dl, $pipedLinks )
783
			->parse() . '</span>';
784
785
		$rclistfrom = '<span class="rclistfrom">' . $this->makeOptionsLink(
786
			$this->msg( 'rclistfrom' )->rawParams( $now, $timenow, $datenow )->parse(),
787
			[ 'from' => $timestamp ],
788
			$nondefaults
789
		) . '</span>';
790
791
		return "{$note}$rclinks<br />$rclistfrom";
792
	}
793
794
	public function isIncludable() {
795
		return true;
796
	}
797
798
	protected function getCacheTTL() {
799
		return 60 * 5;
800
	}
801
802
}
803