Completed
Branch master (d21abb)
by
unknown
32:24
created

SpecialWatchlist::getSubpagesForPrefixSearch()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Implements Special:Watchlist
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
 * limited to user-defined list of titles.
29
 *
30
 * @ingroup SpecialPage
31
 */
32
class SpecialWatchlist extends ChangesListSpecialPage {
33
	public function __construct( $page = 'Watchlist', $restriction = 'viewmywatchlist' ) {
34
		parent::__construct( $page, $restriction );
35
	}
36
37
	public function doesWrites() {
38
		return true;
39
	}
40
41
	/**
42
	 * Main execution point
43
	 *
44
	 * @param string $subpage
45
	 */
46
	function execute( $subpage ) {
47
		// Anons don't get a watchlist
48
		$this->requireLogin( 'watchlistanontext' );
49
50
		$output = $this->getOutput();
51
		$request = $this->getRequest();
52
		$this->addHelpLink( 'Help:Watching pages' );
53
		$output->addModules( [
54
			'mediawiki.special.changeslist.visitedstatus',
55
		] );
56
57
		$mode = SpecialEditWatchlist::getMode( $request, $subpage );
58
		if ( $mode !== false ) {
59
			if ( $mode === SpecialEditWatchlist::EDIT_RAW ) {
60
				$title = SpecialPage::getTitleFor( 'EditWatchlist', 'raw' );
61
			} elseif ( $mode === SpecialEditWatchlist::EDIT_CLEAR ) {
62
				$title = SpecialPage::getTitleFor( 'EditWatchlist', 'clear' );
63
			} else {
64
				$title = SpecialPage::getTitleFor( 'EditWatchlist' );
65
			}
66
67
			$output->redirect( $title->getLocalURL() );
68
69
			return;
70
		}
71
72
		$this->checkPermissions();
73
74
		$user = $this->getUser();
75
		$opts = $this->getOptions();
76
77
		$config = $this->getConfig();
78
		if ( ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) )
79
			&& $request->getVal( 'reset' )
0 ignored issues
show
Bug Best Practice introduced by
The expression $request->getVal('reset') 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...
80
			&& $request->wasPosted()
81
		) {
82
			$user->clearAllNotifications();
83
			$output->redirect( $this->getPageTitle()->getFullURL( $opts->getChangedValues() ) );
84
85
			return;
86
		}
87
88
		parent::execute( $subpage );
89
	}
90
91
	/**
92
	 * Return an array of subpages that this special page will accept.
93
	 *
94
	 * @see also SpecialEditWatchlist::getSubpagesForPrefixSearch
95
	 * @return string[] subpages
96
	 */
97
	public function getSubpagesForPrefixSearch() {
98
		return [
99
			'clear',
100
			'edit',
101
			'raw',
102
		];
103
	}
104
105
	/**
106
	 * Get a FormOptions object containing the default options
107
	 *
108
	 * @return FormOptions
109
	 */
110
	public function getDefaultOptions() {
111
		$opts = parent::getDefaultOptions();
112
		$user = $this->getUser();
113
114
		$opts->add( 'days', $user->getOption( 'watchlistdays' ), FormOptions::FLOAT );
115
		$opts->add( 'extended', $user->getBoolOption( 'extendwatchlist' ) );
116
		if ( $this->getRequest()->getVal( 'action' ) == 'submit' ) {
117
			// The user has submitted the form, so we dont need the default values
118
			return $opts;
119
		}
120
121
		$opts->add( 'hideminor', $user->getBoolOption( 'watchlisthideminor' ) );
122
		$opts->add( 'hidebots', $user->getBoolOption( 'watchlisthidebots' ) );
123
		$opts->add( 'hideanons', $user->getBoolOption( 'watchlisthideanons' ) );
124
		$opts->add( 'hideliu', $user->getBoolOption( 'watchlisthideliu' ) );
125
		$opts->add( 'hidepatrolled', $user->getBoolOption( 'watchlisthidepatrolled' ) );
126
		$opts->add( 'hidemyself', $user->getBoolOption( 'watchlisthideown' ) );
127
		$opts->add( 'hidecategorization', $user->getBoolOption( 'watchlisthidecategorization' ) );
128
129
		return $opts;
130
	}
131
132
	/**
133
	 * Get custom show/hide filters
134
	 *
135
	 * @return array Map of filter URL param names to properties (msg/default)
136
	 */
137 View Code Duplication
	protected function getCustomFilters() {
138
		if ( $this->customFilters === null ) {
139
			$this->customFilters = parent::getCustomFilters();
140
			Hooks::run( 'SpecialWatchlistFilters', [ $this, &$this->customFilters ], '1.23' );
141
		}
142
143
		return $this->customFilters;
144
	}
145
146
	/**
147
	 * Fetch values for a FormOptions object from the WebRequest associated with this instance.
148
	 *
149
	 * Maps old pre-1.23 request parameters Watchlist used to use (different from Recentchanges' ones)
150
	 * to the current ones.
151
	 *
152
	 * @param FormOptions $opts
153
	 * @return FormOptions
154
	 */
155
	protected function fetchOptionsFromRequest( $opts ) {
156
		static $compatibilityMap = [
157
			'hideMinor' => 'hideminor',
158
			'hideBots' => 'hidebots',
159
			'hideAnons' => 'hideanons',
160
			'hideLiu' => 'hideliu',
161
			'hidePatrolled' => 'hidepatrolled',
162
			'hideOwn' => 'hidemyself',
163
		];
164
165
		$params = $this->getRequest()->getValues();
166
		foreach ( $compatibilityMap as $from => $to ) {
167
			if ( isset( $params[$from] ) ) {
168
				$params[$to] = $params[$from];
169
				unset( $params[$from] );
170
			}
171
		}
172
173
		// Not the prettiest way to achieve this… FormOptions internally depends on data sanitization
174
		// methods defined on WebRequest and removing this dependency would cause some code duplication.
175
		$request = new DerivativeRequest( $this->getRequest(), $params );
176
		$opts->fetchValuesFromRequest( $request );
177
178
		return $opts;
179
	}
180
181
	/**
182
	 * Return an array of conditions depending of options set in $opts
183
	 *
184
	 * @param FormOptions $opts
185
	 * @return array
186
	 */
187
	public function buildMainQueryConds( FormOptions $opts ) {
188
		$dbr = $this->getDB();
189
		$conds = parent::buildMainQueryConds( $opts );
190
191
		// Calculate cutoff
192
		if ( $opts['days'] > 0 ) {
193
			$conds[] = 'rc_timestamp > ' .
194
				$dbr->addQuotes( $dbr->timestamp( time() - intval( $opts['days'] * 86400 ) ) );
195
		}
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
		# Toggle watchlist content (all recent edits or just the latest)
212
		if ( $opts['extended'] ) {
213
			$limitWatchlist = $user->getIntOption( 'wllimit' );
214
			$usePage = false;
215
		} else {
216
			# Top log Ids for a page are not stored
217
			$nonRevisionTypes = [ RC_LOG ];
218
			Hooks::run( 'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] );
219
			if ( $nonRevisionTypes ) {
220
				$conds[] = $dbr->makeList(
221
					[
222
						'rc_this_oldid=page_latest',
223
						'rc_type' => $nonRevisionTypes,
224
					],
225
					LIST_OR
226
				);
227
			}
228
			$limitWatchlist = 0;
229
			$usePage = true;
230
		}
231
232
		$tables = [ 'recentchanges', 'watchlist' ];
233
		$fields = RecentChange::selectFields();
234
		$query_options = [ 'ORDER BY' => 'rc_timestamp DESC' ];
235
		$join_conds = [
236
			'watchlist' => [
237
				'INNER JOIN',
238
				[
239
					'wl_user' => $user->getId(),
240
					'wl_namespace=rc_namespace',
241
					'wl_title=rc_title'
242
				],
243
			],
244
		];
245
246
		if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
247
			$fields[] = 'wl_notificationtimestamp';
248
		}
249
		if ( $limitWatchlist ) {
250
			$query_options['LIMIT'] = $limitWatchlist;
251
		}
252
253
		$rollbacker = $user->isAllowed( 'rollback' );
254 View Code Duplication
		if ( $usePage || $rollbacker ) {
255
			$tables[] = 'page';
256
			$join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
257
			if ( $rollbacker ) {
258
				$fields[] = 'page_latest';
259
			}
260
		}
261
262
		// Log entries with DELETED_ACTION must not show up unless the user has
263
		// the necessary rights.
264
		if ( !$user->isAllowed( 'deletedhistory' ) ) {
265
			$bitmask = LogPage::DELETED_ACTION;
266
		} elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
267
			$bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
268
		} else {
269
			$bitmask = 0;
270
		}
271 View Code Duplication
		if ( $bitmask ) {
272
			$conds[] = $dbr->makeList( [
273
				'rc_type != ' . RC_LOG,
274
				$dbr->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
275
			], LIST_OR );
276
		}
277
278
		ChangeTags::modifyDisplayQuery(
279
			$tables,
280
			$fields,
281
			$conds,
282
			$join_conds,
283
			$query_options,
284
			''
285
		);
286
287
		$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts );
288
289
		return $dbr->select(
290
			$tables,
291
			$fields,
292
			$conds,
293
			__METHOD__,
294
			$query_options,
295
			$join_conds
296
		);
297
	}
298
299 View Code Duplication
	protected function runMainQueryHook( &$tables, &$fields, &$conds, &$query_options,
300
		&$join_conds, $opts
301
	) {
302
		return parent::runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts )
303
			&& Hooks::run(
304
				'SpecialWatchlistQuery',
305
				[ &$conds, &$tables, &$join_conds, &$fields, $opts ],
306
				'1.23'
307
			);
308
	}
309
310
	/**
311
	 * Return a IDatabase object for reading
312
	 *
313
	 * @return IDatabase
314
	 */
315
	protected function getDB() {
316
		return wfGetDB( DB_REPLICA, 'watchlist' );
317
	}
318
319
	/**
320
	 * Output feed links.
321
	 */
322
	public function outputFeedLinks() {
323
		$user = $this->getUser();
324
		$wlToken = $user->getTokenFromOption( 'watchlisttoken' );
325
		if ( $wlToken ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $wlToken of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false 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...
326
			$this->addFeedLinks( [
327
				'action' => 'feedwatchlist',
328
				'allrev' => 1,
329
				'wlowner' => $user->getName(),
330
				'wltoken' => $wlToken,
331
			] );
332
		}
333
	}
334
335
	/**
336
	 * Build and output the actual changes list.
337
	 *
338
	 * @param ResultWrapper $rows Database rows
339
	 * @param FormOptions $opts
340
	 */
341
	public function outputChangesList( $rows, $opts ) {
342
		$dbr = $this->getDB();
343
		$user = $this->getUser();
344
		$output = $this->getOutput();
345
346
		# Show a message about replica DB lag, if applicable
347
		$lag = wfGetLB()->safeGetLag( $dbr );
0 ignored issues
show
Deprecated Code introduced by
The function wfGetLB() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancer() or MediaWikiServices::getDBLoadBalancerFactory() instead.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
Bug introduced by
It seems like $dbr defined by $this->getDB() on line 342 can be null; however, LoadBalancer::safeGetLag() 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...
348
		if ( $lag > 0 ) {
349
			$output->showLagWarning( $lag );
0 ignored issues
show
Bug introduced by
It seems like $lag defined by wfGetLB()->safeGetLag($dbr) on line 347 can also be of type boolean; however, OutputPage::showLagWarning() does only seem to accept integer, 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...
350
		}
351
352
		# If no rows to display, show message before try to render the list
353
		if ( $rows->numRows() == 0 ) {
354
			$output->wrapWikiMsg(
355
				"<div class='mw-changeslist-empty'>\n$1\n</div>", 'recentchanges-noresult'
356
			);
357
			return;
358
		}
359
360
		$dbr->dataSeek( $rows, 0 );
361
362
		$list = ChangesList::newFromContext( $this->getContext() );
363
		$list->setWatchlistDivs();
364
		$list->initChangesListRows( $rows );
365
		$dbr->dataSeek( $rows, 0 );
366
367
		if ( $this->getConfig()->get( 'RCShowWatchingUsers' )
368
			&& $user->getOption( 'shownumberswatching' )
369
		) {
370
			$watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
371
		}
372
373
		$s = $list->beginRecentChangesList();
374
		$userShowHiddenCats = $this->getUser()->getBoolOption( 'showhiddencats' );
375
		$counter = 1;
376
		foreach ( $rows as $obj ) {
377
			# Make RC entry
378
			$rc = RecentChange::newFromRow( $obj );
379
380
			# Skip CatWatch entries for hidden cats based on user preference
381
			if (
382
				$rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE &&
383
				!$userShowHiddenCats &&
384
				$rc->getParam( 'hidden-cat' )
385
			) {
386
				continue;
387
			}
388
389
			$rc->counter = $counter++;
390
391
			if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
392
				$updated = $obj->wl_notificationtimestamp;
393
			} else {
394
				$updated = false;
395
			}
396
397
			if ( isset( $watchedItemStore ) ) {
398
				$rcTitleValue = new TitleValue( (int)$obj->rc_namespace, $obj->rc_title );
399
				$rc->numberofWatchingusers = $watchedItemStore->countWatchers( $rcTitleValue );
400
			} else {
401
				$rc->numberofWatchingusers = 0;
402
			}
403
404
			$changeLine = $list->recentChangesLine( $rc, $updated, $counter );
405
			if ( $changeLine !== false ) {
406
				$s .= $changeLine;
407
			}
408
		}
409
		$s .= $list->endRecentChangesList();
410
411
		$output->addHTML( $s );
412
	}
413
414
	/**
415
	 * Set the text to be displayed above the changes
416
	 *
417
	 * @param FormOptions $opts
418
	 * @param int $numRows Number of rows in the result to show after this header
419
	 */
420
	public function doHeader( $opts, $numRows ) {
421
		$user = $this->getUser();
422
		$out = $this->getOutput();
423
424
		// if the user wishes, that the watchlist is reloaded, whenever a filter changes,
425
		// add the module for that
426
		if ( $user->getBoolOption( 'watchlistreloadautomatically' ) ) {
427
			$out->addModules( [ 'mediawiki.special.watchlist' ] );
428
		}
429
430
		$out->addSubtitle(
431
			$this->msg( 'watchlistfor2', $user->getName() )
432
				->rawParams( SpecialEditWatchlist::buildTools(
433
					$this->getLanguage(),
434
					$this->getLinkRenderer()
435
				) )
436
		);
437
438
		$this->setTopText( $opts );
439
440
		$lang = $this->getLanguage();
441
		if ( $opts['days'] > 0 ) {
442
			$days = $opts['days'];
443
		} else {
444
			$days = $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 );
445
		}
446
		$timestamp = wfTimestampNow();
447
		$wlInfo = $this->msg( 'wlnote' )->numParams( $numRows, round( $days * 24 ) )->params(
448
			$lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user )
449
		)->parse() . "<br />\n";
450
451
		$nondefaults = $opts->getChangedValues();
452
		$cutofflinks = $this->msg( 'wlshowtime' ) . ' ' . $this->cutoffselector( $opts );
453
454
		# Spit out some control panel links
455
		$filters = [
456
			'hideminor' => 'wlshowhideminor',
457
			'hidebots' => 'wlshowhidebots',
458
			'hideanons' => 'wlshowhideanons',
459
			'hideliu' => 'wlshowhideliu',
460
			'hidemyself' => 'wlshowhidemine',
461
			'hidepatrolled' => 'wlshowhidepatr'
462
		];
463
464
		if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) {
465
			$filters['hidecategorization'] = 'wlshowhidecategorization';
466
		}
467
468
		foreach ( $this->getCustomFilters() as $key => $params ) {
469
			$filters[$key] = $params['msg'];
470
		}
471
		// Disable some if needed
472
		if ( !$user->useRCPatrol() ) {
473
			unset( $filters['hidepatrolled'] );
474
		}
475
476
		$links = [];
477
		foreach ( $filters as $name => $msg ) {
478
			$links[] = $this->showHideCheck( $nondefaults, $msg, $name, $opts[$name] );
479
		}
480
481
		$hiddenFields = $nondefaults;
482
		$hiddenFields['action'] = 'submit';
483
		unset( $hiddenFields['namespace'] );
484
		unset( $hiddenFields['invert'] );
485
		unset( $hiddenFields['associated'] );
486
		unset( $hiddenFields['days'] );
487
		foreach ( $filters as $key => $value ) {
488
			unset( $hiddenFields[$key] );
489
		}
490
491
		# Create output
492
		$form = '';
493
494
		# Namespace filter and put the whole form together.
495
		$form .= $wlInfo;
496
		$form .= $cutofflinks;
497
		$form .= $this->msg( 'watchlist-hide' ) .
498
			$this->msg( 'colon-separator' )->escaped() .
499
			implode( ' ', $links );
500
		$form .= "\n<br />\n";
501
		$form .= Html::namespaceSelector(
502
			[
503
				'selected' => $opts['namespace'],
504
				'all' => '',
505
				'label' => $this->msg( 'namespace' )->text()
506
			], [
507
				'name' => 'namespace',
508
				'id' => 'namespace',
509
				'class' => 'namespaceselector',
510
			]
511
		) . "\n";
512
		$form .= '<span class="mw-input-with-label">' . Xml::checkLabel(
513
			$this->msg( 'invert' )->text(),
514
			'invert',
515
			'nsinvert',
516
			$opts['invert'],
517
			[ 'title' => $this->msg( 'tooltip-invert' )->text() ]
518
		) . "</span>\n";
519
		$form .= '<span class="mw-input-with-label">' . Xml::checkLabel(
520
			$this->msg( 'namespace_association' )->text(),
521
			'associated',
522
			'nsassociated',
523
			$opts['associated'],
524
			[ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ]
525
		) . "</span>\n";
526
		$form .= Xml::submitButton( $this->msg( 'watchlist-submit' )->text() ) . "\n";
527
		foreach ( $hiddenFields as $key => $value ) {
528
			$form .= Html::hidden( $key, $value ) . "\n";
529
		}
530
		$form .= Xml::closeElement( 'fieldset' ) . "\n";
531
		$form .= Xml::closeElement( 'form' ) . "\n";
532
		$this->getOutput()->addHTML( $form );
533
534
		$this->setBottomText( $opts );
535
	}
536
537
	function cutoffselector( $options ) {
538
		// Cast everything to strings immediately, so that we know all of the values have the same
539
		// precision, and can be compared with '==='. 2/24 has a few more decimal places than its
540
		// default string representation, for example, and would confuse comparisons.
541
542
		// Misleadingly, the 'days' option supports hours too.
543
		$days = array_map( 'strval', [ 1/24, 2/24, 6/24, 12/24, 1, 3, 7 ] );
544
545
		$userWatchlistOption = (string)$this->getUser()->getOption( 'watchlistdays' );
546
		// add the user preference, if it isn't available already
547
		if ( !in_array( $userWatchlistOption, $days ) && $userWatchlistOption !== '0' ) {
548
			$days[] = $userWatchlistOption;
549
		}
550
551
		$maxDays = (string)( $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
552
		// add the maximum possible value, if it isn't available already
553
		if ( !in_array( $maxDays, $days ) ) {
554
			$days[] = $maxDays;
555
		}
556
557
		$selected = (string)$options['days'];
558
		if ( $selected <= 0 ) {
559
			$selected = $maxDays;
560
		}
561
562
		// add the currently selected value, if it isn't available already
563
		if ( !in_array( $selected, $days ) ) {
564
			$days[] = $selected;
565
		}
566
567
		$select = new XmlSelect( 'days', 'days', $selected );
568
569
		asort( $days );
570
		foreach ( $days as $value ) {
571
			if ( $value < 1 ) {
572
				$name = $this->msg( 'hours' )->numParams( $value * 24 )->text();
573
			} else {
574
				$name = $this->msg( 'days' )->numParams( $value )->text();
575
			}
576
			$select->addOption( $name, $value );
577
		}
578
579
		return $select->getHTML() . "\n<br />\n";
580
	}
581
582
	function setTopText( FormOptions $opts ) {
583
		$nondefaults = $opts->getChangedValues();
584
		$form = "";
585
		$user = $this->getUser();
586
587
		$numItems = $this->countItems();
588
		$showUpdatedMarker = $this->getConfig()->get( 'ShowUpdatedMarker' );
589
590
		// Show watchlist header
591
		$form .= "<p>";
592
		if ( $numItems == 0 ) {
593
			$form .= $this->msg( 'nowatchlist' )->parse() . "\n";
594
		} else {
595
			$form .= $this->msg( 'watchlist-details' )->numParams( $numItems )->parse() . "\n";
596
			if ( $this->getConfig()->get( 'EnotifWatchlist' )
597
				&& $user->getOption( 'enotifwatchlistpages' )
598
			) {
599
				$form .= $this->msg( 'wlheader-enotif' )->parse() . "\n";
600
			}
601
			if ( $showUpdatedMarker ) {
602
				$form .= $this->msg( 'wlheader-showupdated' )->parse() . "\n";
603
			}
604
		}
605
		$form .= "</p>";
606
607
		if ( $numItems > 0 && $showUpdatedMarker ) {
608
			$form .= Xml::openElement( 'form', [ 'method' => 'post',
609
				'action' => $this->getPageTitle()->getLocalURL(),
610
				'id' => 'mw-watchlist-resetbutton' ] ) . "\n" .
611
			Xml::submitButton( $this->msg( 'enotif_reset' )->text(), [ 'name' => 'dummy' ] ) . "\n" .
612
			Html::hidden( 'reset', 'all' ) . "\n";
613
			foreach ( $nondefaults as $key => $value ) {
614
				$form .= Html::hidden( $key, $value ) . "\n";
615
			}
616
			$form .= Xml::closeElement( 'form' ) . "\n";
617
		}
618
619
		$form .= Xml::openElement( 'form', [
620
			'method' => 'get',
621
			'action' => wfScript(),
622
			'id' => 'mw-watchlist-form'
623
		] );
624
		$form .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
625
		$form .= Xml::fieldset(
626
			$this->msg( 'watchlist-options' )->text(),
627
			false,
628
			[ 'id' => 'mw-watchlist-options' ]
629
		);
630
631
		$form .= $this->makeLegend();
632
633
		$this->getOutput()->addHTML( $form );
634
	}
635
636
	protected function showHideCheck( $options, $message, $name, $value ) {
637
		$options[$name] = 1 - (int)$value;
638
639
		return '<span class="mw-input-with-label">' . Xml::checkLabel(
640
			$this->msg( $message, '' )->text(),
641
			$name,
642
			$name,
643
			(int)$value
644
		) . '</span>';
645
	}
646
647
	/**
648
	 * Count the number of paired items on a user's watchlist.
649
	 * The assumption made here is that when a subject page is watched a talk page is also watched.
650
	 * Hence the number of individual items is halved.
651
	 *
652
	 * @return int
653
	 */
654
	protected function countItems() {
655
		$store = MediaWikiServices::getInstance()->getWatchedItemStore();
656
		$count = $store->countWatchedItems( $this->getUser() );
657
		return floor( $count / 2 );
658
	}
659
}
660