SpecialRecentChanges::outputFeedLinks()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
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
			if ( preg_match( '/^tagfilter=(.*)$/', $bit, $m ) ) {
163
				$opts['tagfilter'] = $m[1];
164
			}
165
		}
166
	}
167
168
	public function validateOptions( FormOptions $opts ) {
169
		$opts->validateIntBounds( 'limit', 0, 5000 );
170
		parent::validateOptions( $opts );
171
	}
172
173
	/**
174
	 * Return an array of conditions depending of options set in $opts
175
	 *
176
	 * @param FormOptions $opts
177
	 * @return array
178
	 */
179
	public function buildMainQueryConds( FormOptions $opts ) {
180
		$dbr = $this->getDB();
181
		$conds = parent::buildMainQueryConds( $opts );
182
183
		// Calculate cutoff
184
		$cutoff_unixtime = time() - ( $opts['days'] * 86400 );
185
		$cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
186
		$cutoff = $dbr->timestamp( $cutoff_unixtime );
187
188
		$fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
189
		if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) {
190
			$cutoff = $dbr->timestamp( $opts['from'] );
191
		} else {
192
			$opts->reset( 'from' );
193
		}
194
195
		$conds[] = 'rc_timestamp >= ' . $dbr->addQuotes( $cutoff );
196
197
		return $conds;
198
	}
199
200
	/**
201
	 * Process the query
202
	 *
203
	 * @param array $conds
204
	 * @param FormOptions $opts
205
	 * @return bool|ResultWrapper Result or false (for Recentchangeslinked only)
206
	 */
207
	public function doMainQuery( $conds, $opts ) {
208
		$dbr = $this->getDB();
209
		$user = $this->getUser();
210
211
		$tables = [ 'recentchanges' ];
212
		$fields = RecentChange::selectFields();
213
		$query_options = [];
214
		$join_conds = [];
215
216
		// JOIN on watchlist for users
217 View Code Duplication
		if ( $user->getId() && $user->isAllowed( 'viewmywatchlist' ) ) {
218
			$tables[] = 'watchlist';
219
			$fields[] = 'wl_user';
220
			$fields[] = 'wl_notificationtimestamp';
221
			$join_conds['watchlist'] = [ 'LEFT JOIN', [
222
				'wl_user' => $user->getId(),
223
				'wl_title=rc_title',
224
				'wl_namespace=rc_namespace'
225
			] ];
226
		}
227
228 View Code Duplication
		if ( $user->isAllowed( 'rollback' ) ) {
229
			$tables[] = 'page';
230
			$fields[] = 'page_latest';
231
			$join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
232
		}
233
234
		ChangeTags::modifyDisplayQuery(
235
			$tables,
236
			$fields,
237
			$conds,
238
			$join_conds,
239
			$query_options,
240
			$opts['tagfilter']
241
		);
242
243
		if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
244
			$opts )
245
		) {
246
			return false;
247
		}
248
249
		// array_merge() is used intentionally here so that hooks can, should
250
		// they so desire, override the ORDER BY / LIMIT condition(s); prior to
251
		// MediaWiki 1.26 this used to use the plus operator instead, which meant
252
		// that extensions weren't able to change these conditions
253
		$query_options = array_merge( [
254
			'ORDER BY' => 'rc_timestamp DESC',
255
			'LIMIT' => $opts['limit'] ], $query_options );
256
		$rows = $dbr->select(
257
			$tables,
258
			$fields,
259
			// rc_new is not an ENUM, but adding a redundant rc_new IN (0,1) gives mysql enough
260
			// knowledge to use an index merge if it wants (it may use some other index though).
261
			$conds + [ 'rc_new' => [ 0, 1 ] ],
262
			__METHOD__,
263
			$query_options,
264
			$join_conds
265
		);
266
267
		// Build the final data
268
		if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) {
269
			$this->filterByCategories( $rows, $opts );
270
		}
271
272
		return $rows;
273
	}
274
275 View Code Duplication
	protected function runMainQueryHook( &$tables, &$fields, &$conds,
276
		&$query_options, &$join_conds, $opts
277
	) {
278
		return parent::runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts )
279
			&& Hooks::run(
280
				'SpecialRecentChangesQuery',
281
				[ &$conds, &$tables, &$join_conds, $opts, &$query_options, &$fields ],
282
				'1.23'
283
			);
284
	}
285
286
	protected function getDB() {
287
		return wfGetDB( DB_REPLICA, 'recentchanges' );
288
	}
289
290
	public function outputFeedLinks() {
291
		$this->addFeedLinks( $this->getFeedQuery() );
292
	}
293
294
	/**
295
	 * Get URL query parameters for action=feedrecentchanges API feed of current recent changes view.
296
	 *
297
	 * @return array
298
	 */
299
	protected function getFeedQuery() {
300
		$query = array_filter( $this->getOptions()->getAllValues(), function ( $value ) {
301
			// API handles empty parameters in a different way
302
			return $value !== '';
303
		} );
304
		$query['action'] = 'feedrecentchanges';
305
		$feedLimit = $this->getConfig()->get( 'FeedLimit' );
306
		if ( $query['limit'] > $feedLimit ) {
307
			$query['limit'] = $feedLimit;
308
		}
309
310
		return $query;
311
	}
312
313
	/**
314
	 * Build and output the actual changes list.
315
	 *
316
	 * @param ResultWrapper $rows Database rows
317
	 * @param FormOptions $opts
318
	 */
319
	public function outputChangesList( $rows, $opts ) {
320
		$limit = $opts['limit'];
321
322
		$showWatcherCount = $this->getConfig()->get( 'RCShowWatchingUsers' )
323
			&& $this->getUser()->getOption( 'shownumberswatching' );
324
		$watcherCache = [];
325
326
		$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...
327
328
		$counter = 1;
329
		$list = ChangesList::newFromContext( $this->getContext() );
330
		$list->initChangesListRows( $rows );
331
332
		$userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' );
333
		$rclistOutput = $list->beginRecentChangesList();
334
		foreach ( $rows as $obj ) {
335
			if ( $limit == 0 ) {
336
				break;
337
			}
338
			$rc = RecentChange::newFromRow( $obj );
339
340
			# Skip CatWatch entries for hidden cats based on user preference
341
			if (
342
				$rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE &&
343
				!$userShowHiddenCats &&
344
				$rc->getParam( 'hidden-cat' )
345
			) {
346
				continue;
347
			}
348
349
			$rc->counter = $counter++;
350
			# Check if the page has been updated since the last visit
351
			if ( $this->getConfig()->get( 'ShowUpdatedMarker' )
352
				&& !empty( $obj->wl_notificationtimestamp )
353
			) {
354
				$rc->notificationtimestamp = ( $obj->rc_timestamp >= $obj->wl_notificationtimestamp );
355
			} else {
356
				$rc->notificationtimestamp = false; // Default
357
			}
358
			# Check the number of users watching the page
359
			$rc->numberofWatchingusers = 0; // Default
360
			if ( $showWatcherCount && $obj->rc_namespace >= 0 ) {
361
				if ( !isset( $watcherCache[$obj->rc_namespace][$obj->rc_title] ) ) {
362
					$watcherCache[$obj->rc_namespace][$obj->rc_title] =
363
						MediaWikiServices::getInstance()->getWatchedItemStore()->countWatchers(
364
							new TitleValue( (int)$obj->rc_namespace, $obj->rc_title )
365
						);
366
				}
367
				$rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
368
			}
369
370
			$changeLine = $list->recentChangesLine( $rc, !empty( $obj->wl_user ), $counter );
371
			if ( $changeLine !== false ) {
372
				$rclistOutput .= $changeLine;
373
				--$limit;
374
			}
375
		}
376
		$rclistOutput .= $list->endRecentChangesList();
377
378
		if ( $rows->numRows() === 0 ) {
379
			$this->getOutput()->addHTML(
380
				'<div class="mw-changeslist-empty">' .
381
				$this->msg( 'recentchanges-noresult' )->parse() .
382
				'</div>'
383
			);
384
			if ( !$this->including() ) {
385
				$this->getOutput()->setStatusCode( 404 );
386
			}
387
		} else {
388
			$this->getOutput()->addHTML( $rclistOutput );
389
		}
390
	}
391
392
	/**
393
	 * Set the text to be displayed above the changes
394
	 *
395
	 * @param FormOptions $opts
396
	 * @param int $numRows Number of rows in the result to show after this header
397
	 */
398
	public function doHeader( $opts, $numRows ) {
399
		$this->setTopText( $opts );
400
401
		$defaults = $opts->getAllValues();
402
		$nondefaults = $opts->getChangedValues();
403
404
		$panel = [];
405
		$panel[] = $this->makeLegend();
406
		$panel[] = $this->optionsPanel( $defaults, $nondefaults, $numRows );
407
		$panel[] = '<hr />';
408
409
		$extraOpts = $this->getExtraOptions( $opts );
410
		$extraOptsCount = count( $extraOpts );
411
		$count = 0;
412
		$submit = ' ' . Xml::submitButton( $this->msg( 'recentchanges-submit' )->text() );
413
414
		$out = Xml::openElement( 'table', [ 'class' => 'mw-recentchanges-table' ] );
415
		foreach ( $extraOpts as $name => $optionRow ) {
416
			# Add submit button to the last row only
417
			++$count;
418
			$addSubmit = ( $count === $extraOptsCount ) ? $submit : '';
419
420
			$out .= Xml::openElement( 'tr' );
421
			if ( is_array( $optionRow ) ) {
422
				$out .= Xml::tags(
423
					'td',
424
					[ 'class' => 'mw-label mw-' . $name . '-label' ],
425
					$optionRow[0]
426
				);
427
				$out .= Xml::tags(
428
					'td',
429
					[ 'class' => 'mw-input' ],
430
					$optionRow[1] . $addSubmit
431
				);
432
			} else {
433
				$out .= Xml::tags(
434
					'td',
435
					[ 'class' => 'mw-input', 'colspan' => 2 ],
436
					$optionRow . $addSubmit
437
				);
438
			}
439
			$out .= Xml::closeElement( 'tr' );
440
		}
441
		$out .= Xml::closeElement( 'table' );
442
443
		$unconsumed = $opts->getUnconsumedValues();
444
		foreach ( $unconsumed as $key => $value ) {
445
			$out .= Html::hidden( $key, $value );
446
		}
447
448
		$t = $this->getPageTitle();
449
		$out .= Html::hidden( 'title', $t->getPrefixedText() );
450
		$form = Xml::tags( 'form', [ 'action' => wfScript() ], $out );
451
		$panel[] = $form;
452
		$panelString = implode( "\n", $panel );
453
454
		$this->getOutput()->addHTML(
455
			Xml::fieldset(
456
				$this->msg( 'recentchanges-legend' )->text(),
457
				$panelString,
458
				[ 'class' => 'rcoptions' ]
459
			)
460
		);
461
462
		$this->setBottomText( $opts );
463
	}
464
465
	/**
466
	 * Send the text to be displayed above the options
467
	 *
468
	 * @param FormOptions $opts Unused
469
	 */
470 View Code Duplication
	function setTopText( FormOptions $opts ) {
471
		global $wgContLang;
472
473
		$message = $this->msg( 'recentchangestext' )->inContentLanguage();
474
		if ( !$message->isDisabled() ) {
475
			$this->getOutput()->addWikiText(
476
				Html::rawElement( 'div',
477
					[ 'lang' => $wgContLang->getHtmlCode(), 'dir' => $wgContLang->getDir() ],
478
					"\n" . $message->plain() . "\n"
479
				),
480
				/* $lineStart */ true,
481
				/* $interface */ false
482
			);
483
		}
484
	}
485
486
	/**
487
	 * Get options to be displayed in a form
488
	 *
489
	 * @param FormOptions $opts
490
	 * @return array
491
	 */
492
	function getExtraOptions( $opts ) {
493
		$opts->consumeValues( [
494
			'namespace', 'invert', 'associated', 'tagfilter', 'categories', 'categories_any'
495
		] );
496
497
		$extraOpts = [];
498
		$extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
499
500
		if ( $this->getConfig()->get( 'AllowCategorizedRecentChanges' ) ) {
501
			$extraOpts['category'] = $this->categoryFilterForm( $opts );
502
		}
503
504
		$tagFilter = ChangeTags::buildTagFilterSelector( $opts['tagfilter'] );
505
		if ( count( $tagFilter ) ) {
506
			$extraOpts['tagfilter'] = $tagFilter;
507
		}
508
509
		// Don't fire the hook for subclasses. (Or should we?)
510
		if ( $this->getName() === 'Recentchanges' ) {
511
			Hooks::run( 'SpecialRecentChangesPanel', [ &$extraOpts, $opts ] );
512
		}
513
514
		return $extraOpts;
515
	}
516
517
	/**
518
	 * Add page-specific modules.
519
	 */
520
	protected function addModules() {
521
		parent::addModules();
522
		$out = $this->getOutput();
523
		$out->addModules( 'mediawiki.special.recentchanges' );
524
	}
525
526
	/**
527
	 * Get last modified date, for client caching
528
	 * Don't use this if we are using the patrol feature, patrol changes don't
529
	 * update the timestamp
530
	 *
531
	 * @return string|bool
532
	 */
533
	public function checkLastModified() {
534
		$dbr = $this->getDB();
535
		$lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ );
536
537
		return $lastmod;
538
	}
539
540
	/**
541
	 * Creates the choose namespace selection
542
	 *
543
	 * @param FormOptions $opts
544
	 * @return string
545
	 */
546
	protected function namespaceFilterForm( FormOptions $opts ) {
547
		$nsSelect = Html::namespaceSelector(
548
			[ 'selected' => $opts['namespace'], 'all' => '' ],
549
			[ 'name' => 'namespace', 'id' => 'namespace' ]
550
		);
551
		$nsLabel = Xml::label( $this->msg( 'namespace' )->text(), 'namespace' );
552
		$invert = Xml::checkLabel(
553
			$this->msg( 'invert' )->text(), 'invert', 'nsinvert',
554
			$opts['invert'],
555
			[ 'title' => $this->msg( 'tooltip-invert' )->text() ]
556
		);
557
		$associated = Xml::checkLabel(
558
			$this->msg( 'namespace_association' )->text(), 'associated', 'nsassociated',
559
			$opts['associated'],
560
			[ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ]
561
		);
562
563
		return [ $nsLabel, "$nsSelect $invert $associated" ];
564
	}
565
566
	/**
567
	 * Create an input to filter changes by categories
568
	 *
569
	 * @param FormOptions $opts
570
	 * @return array
571
	 */
572
	protected function categoryFilterForm( FormOptions $opts ) {
573
		list( $label, $input ) = Xml::inputLabelSep( $this->msg( 'rc_categories' )->text(),
574
			'categories', 'mw-categories', false, $opts['categories'] );
575
576
		$input .= ' ' . Xml::checkLabel( $this->msg( 'rc_categories_any' )->text(),
577
			'categories_any', 'mw-categories_any', $opts['categories_any'] );
578
579
		return [ $label, $input ];
580
	}
581
582
	/**
583
	 * Filter $rows by categories set in $opts
584
	 *
585
	 * @param ResultWrapper $rows Database rows
586
	 * @param FormOptions $opts
587
	 */
588
	function filterByCategories( &$rows, FormOptions $opts ) {
589
		$categories = array_map( 'trim', explode( '|', $opts['categories'] ) );
590
591
		if ( !count( $categories ) ) {
592
			return;
593
		}
594
595
		# Filter categories
596
		$cats = [];
597
		foreach ( $categories as $cat ) {
598
			$cat = trim( $cat );
599
			if ( $cat == '' ) {
600
				continue;
601
			}
602
			$cats[] = $cat;
603
		}
604
605
		# Filter articles
606
		$articles = [];
607
		$a2r = [];
608
		$rowsarr = [];
609
		foreach ( $rows as $k => $r ) {
610
			$nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
611
			$id = $nt->getArticleID();
612
			if ( $id == 0 ) {
613
				continue; # Page might have been deleted...
614
			}
615
			if ( !in_array( $id, $articles ) ) {
616
				$articles[] = $id;
617
			}
618
			if ( !isset( $a2r[$id] ) ) {
619
				$a2r[$id] = [];
620
			}
621
			$a2r[$id][] = $k;
622
			$rowsarr[$k] = $r;
623
		}
624
625
		# Shortcut?
626
		if ( !count( $articles ) || !count( $cats ) ) {
627
			return;
628
		}
629
630
		# Look up
631
		$catFind = new CategoryFinder;
632
		$catFind->seed( $articles, $cats, $opts['categories_any'] ? 'OR' : 'AND' );
633
		$match = $catFind->run();
634
635
		# Filter
636
		$newrows = [];
637
		foreach ( $match as $id ) {
638
			foreach ( $a2r[$id] as $rev ) {
639
				$k = $rev;
640
				$newrows[$k] = $rowsarr[$k];
641
			}
642
		}
643
		$rows = $newrows;
644
	}
645
646
	/**
647
	 * Makes change an option link which carries all the other options
648
	 *
649
	 * @param string $title Title
650
	 * @param array $override Options to override
651
	 * @param array $options Current options
652
	 * @param bool $active Whether to show the link in bold
653
	 * @return string
654
	 */
655
	function makeOptionsLink( $title, $override, $options, $active = false ) {
656
		$params = $override + $options;
657
658
		// Bug 36524: false values have be converted to "0" otherwise
659
		// wfArrayToCgi() will omit it them.
660
		foreach ( $params as &$value ) {
661
			if ( $value === false ) {
662
				$value = '0';
663
			}
664
		}
665
		unset( $value );
666
667
		$text = htmlspecialchars( $title );
668
		if ( $active ) {
669
			$text = '<strong>' . $text . '</strong>';
670
		}
671
672
		return Linker::linkKnown( $this->getPageTitle(), $text, [], $params );
673
	}
674
675
	/**
676
	 * Creates the options panel.
677
	 *
678
	 * @param array $defaults
679
	 * @param array $nondefaults
680
	 * @param int $numRows Number of rows in the result to show after this header
681
	 * @return string
682
	 */
683
	function optionsPanel( $defaults, $nondefaults, $numRows ) {
684
		$options = $nondefaults + $defaults;
685
686
		$note = '';
687
		$msg = $this->msg( 'rclegend' );
688
		if ( !$msg->isDisabled() ) {
689
			$note .= '<div class="mw-rclegend">' . $msg->parse() . "</div>\n";
690
		}
691
692
		$lang = $this->getLanguage();
693
		$user = $this->getUser();
694
		$config = $this->getConfig();
695
		if ( $options['from'] ) {
696
			$note .= $this->msg( 'rcnotefrom' )
697
				->numParams( $options['limit'] )
698
				->params(
699
					$lang->userTimeAndDate( $options['from'], $user ),
700
					$lang->userDate( $options['from'], $user ),
701
					$lang->userTime( $options['from'], $user )
702
				)
703
				->numParams( $numRows )
704
				->parse() . '<br />';
705
		}
706
707
		# Sort data for display and make sure it's unique after we've added user data.
708
		$linkLimits = $config->get( 'RCLinkLimits' );
709
		$linkLimits[] = $options['limit'];
710
		sort( $linkLimits );
711
		$linkLimits = array_unique( $linkLimits );
712
713
		$linkDays = $config->get( 'RCLinkDays' );
714
		$linkDays[] = $options['days'];
715
		sort( $linkDays );
716
		$linkDays = array_unique( $linkDays );
717
718
		// limit links
719
		$cl = [];
720 View Code Duplication
		foreach ( $linkLimits as $value ) {
721
			$cl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
722
				[ 'limit' => $value ], $nondefaults, $value == $options['limit'] );
723
		}
724
		$cl = $lang->pipeList( $cl );
725
726
		// day links, reset 'from' to none
727
		$dl = [];
728 View Code Duplication
		foreach ( $linkDays as $value ) {
729
			$dl[] = $this->makeOptionsLink( $lang->formatNum( $value ),
730
				[ 'days' => $value, 'from' => '' ], $nondefaults, $value == $options['days'] );
731
		}
732
		$dl = $lang->pipeList( $dl );
733
734
		// show/hide links
735
		$filters = [
736
			'hideminor' => 'rcshowhideminor',
737
			'hidebots' => 'rcshowhidebots',
738
			'hideanons' => 'rcshowhideanons',
739
			'hideliu' => 'rcshowhideliu',
740
			'hidepatrolled' => 'rcshowhidepatr',
741
			'hidemyself' => 'rcshowhidemine'
742
		];
743
744
		if ( $config->get( 'RCWatchCategoryMembership' ) ) {
745
			$filters['hidecategorization'] = 'rcshowhidecategorization';
746
		}
747
748
		$showhide = [ 'show', 'hide' ];
749
750
		foreach ( $this->getCustomFilters() as $key => $params ) {
751
			$filters[$key] = $params['msg'];
752
		}
753
		// Disable some if needed
754
		if ( !$user->useRCPatrol() ) {
755
			unset( $filters['hidepatrolled'] );
756
		}
757
758
		$links = [];
759
		foreach ( $filters as $key => $msg ) {
760
			// The following messages are used here:
761
			// rcshowhideminor-show, rcshowhideminor-hide, rcshowhidebots-show, rcshowhidebots-hide,
762
			// rcshowhideanons-show, rcshowhideanons-hide, rcshowhideliu-show, rcshowhideliu-hide,
763
			// rcshowhidepatr-show, rcshowhidepatr-hide, rcshowhidemine-show, rcshowhidemine-hide,
764
			// rcshowhidecategorization-show, rcshowhidecategorization-hide.
765
			$linkMessage = $this->msg( $msg . '-' . $showhide[1 - $options[$key]] );
766
			// Extensions can define additional filters, but don't need to define the corresponding
767
			// messages. If they don't exist, just fall back to 'show' and 'hide'.
768
			if ( !$linkMessage->exists() ) {
769
				$linkMessage = $this->msg( $showhide[1 - $options[$key]] );
770
			}
771
772
			$link = $this->makeOptionsLink( $linkMessage->text(),
773
				[ $key => 1 - $options[$key] ], $nondefaults );
774
			$links[] = "<span class=\"$msg rcshowhideoption\">"
775
				. $this->msg( $msg )->rawParams( $link )->escaped() . '</span>';
776
		}
777
778
		// show from this onward link
779
		$timestamp = wfTimestampNow();
780
		$now = $lang->userTimeAndDate( $timestamp, $user );
781
		$timenow = $lang->userTime( $timestamp, $user );
782
		$datenow = $lang->userDate( $timestamp, $user );
783
		$pipedLinks = '<span class="rcshowhide">' . $lang->pipeList( $links ) . '</span>';
784
785
		$rclinks = '<span class="rclinks">' . $this->msg( 'rclinks' )->rawParams( $cl, $dl, $pipedLinks )
786
			->parse() . '</span>';
787
788
		$rclistfrom = '<span class="rclistfrom">' . $this->makeOptionsLink(
789
			$this->msg( 'rclistfrom' )->rawParams( $now, $timenow, $datenow )->parse(),
790
			[ 'from' => $timestamp ],
791
			$nondefaults
792
		) . '</span>';
793
794
		return "{$note}$rclinks<br />$rclistfrom";
795
	}
796
797
	public function isIncludable() {
798
		return true;
799
	}
800
801
	protected function getCacheTTL() {
802
		return 60 * 5;
803
	}
804
805
}
806